Why your Claude Code allow rules keep asking anyway

You allowed npm test. It is still asking. The reason is almost never the rule you wrote — it is that Claude Code splits a compound command and every subcommand has to match a rule of its own, and that several shapes of command can never be auto-approved by a prefix rule at all.

Correction, 7 September 2026. The first version of this page claimed an allow rule is a prefix match against the whole command string, and that cd frontend && npm test therefore fails because the string starts with cd. That is wrong. Claude Code is aware of shell operators and checks each subcommand independently. I had verified the behaviour against my own app's matcher and written it up as though it were Claude Code's — which is exactly the mistake this page now warns about. The corrected explanation is below, and it is a more useful one.

The order the rules are evaluated in

There are three kinds of permission rule, not two, and the third is the one people forget they set. They are evaluated in a fixed order and the first match wins:

RuleEffect
denyBlocks the call.
askPrompts you, even in modes where nothing would otherwise prompt.
allowRuns with no prompt.

Specificity does not change that order, which produces two results worth internalising:

A broad deny cannot carry exceptions. Bash(aws *) in deny blocks aws s3 ls even if you also allow exactly that. If you need exceptions, do not write the deny.
An ask rule beats your allow rule. If something still prompts and you cannot see why, look for an ask entry before you touch the allow list. A matching ask prompts even when a more specific allow rule also matches.

Each subcommand is matched on its own

This is the actual answer to the question in the title. Claude Code recognises &&, ||, ;, |, |&, & and newlines as command separators, splits on them, and requires that every resulting subcommand match a rule. A chain is allowed only if all of its parts are.

So the chain that started this page does work, as long as each half qualifies:

cd packages/api && ls        ✅ cd into your working directory is read-only,
                              and ls qualifies on its own

cd packages/api && npm test  ✅ with Bash(npm test *) — the cd half is
                              read-only, the npm half matches your rule

git status && npm publish    ❌ npm publish is not covered by anything,
                              so the whole chain prompts

One uncovered part is enough to make the whole line prompt. That is why adding rules one at a time feels like whack-a-mole: you are covering the part that prompted, and the next chain has a different uncovered part.

The same splitting works in your favour for deny and ask, which apply when any subcommand matches — including one nested in a subshell, a command substitution, or a loop body. An ask rule of Bash(git clean *) still prompts for cd /tmp && git clean -f and for echo "$(git clean -f)".

The pattern syntax, and its three sharp edges

RuleMatchesDoes not match
Bash(npm test *)npm test, npm test --watchnpm run build
Bash(npm test:*)the same thing — :* is an equivalent way to write a trailing wildcard
Bash(ls *)ls, ls -lalsof
Bash(ls*)ls -la and lsof

Environment assignments

A leading assignment of a known-safe environment variable is stripped before an allow rule is matched, so Bash(npm test *) does match NODE_ENV=test npm test. An allow rule will not match past an assignment of any other variable. Deny and ask match past any leading assignment, so Bash(rm *) in deny still catches FOO=bar rm -rf tmp/.

Shapes that a prefix rule can never approve

If a command keeps prompting no matter what you add to the allow list, it is probably one of these. Nothing you write as a prefix rule will cover them:

For an exec wrapper you actually want, the only option is an exact-match rule for the full command string.

Where the rules are actually read from

Allow, ask and deny rules are merged from more places than most people expect, and a rule in one file will not show up when you look in another:

That last file explains a specific confusion: you clicked "don't ask again" once, months ago, and now cannot find the rule anywhere in your settings. It is in ~/.claude.json, keyed by the project's absolute path — so it also silently stops applying if you move or rename the directory, or reach it through a different symlink.

Worth knowing what that click actually writes: approving a compound command saves a separate rule for each subcommand that needed approval, up to five, rather than one rule for the whole line. Approving git status && npm test saves a rule for npm test, which is then recognised regardless of what precedes it next time. A cd into a subdirectory generates its own Read rule for that path.

Hooks are a separate gate, and they do not override the rules

A PreToolUse hook runs before the permission prompt, and its output can deny the call, force a prompt, or let it through. Two things about it are commonly got backwards — I had one of them backwards myself:

For an ordinary "no opinion" outcome you want exit 0, with either no output or a JSON decision. Printing {} and exiting 0 leaves the normal permission flow exactly as it would have been.

Fail open, never closed. If your hook cannot reach whatever it consults, exit 0 and let the normal flow happen. Given that exit 2 blocks, a script that dies with a non-zero status turns a bug in your shell into a session that cannot run anything.

On timeouts, be careful with numbers you read anywhere including here: the default for a command hook is 600 seconds on most events, lower on a few (30 for UserPromptSubmit, 10 for MessageDisplay). A hook that waits on a human should set its own timeout in the config and give its own work a slightly shorter deadline than that, so it always answers rather than being killed mid-thought. Ours sets "timeout": 55 and uses curl -m 52 inside it — those are our numbers, not Claude Code's defaults.

A worked example: making a destructive command impossible

Deny is evaluated first and applies when any subcommand matches, so this holds in auto mode and inside chains and subshells, where no prompt would otherwise have appeared:

{
  "permissions": {
    "deny": [
      "Bash(sudo date *)",
      "Bash(sudo systemsetup *)",
      "Bash(rm -rf / *)"
    ]
  }
}

Worth knowing the limit of this, though, because it is easy to over-trust: deny rules constrain the agent's own tool calls. They do not constrain a script the agent wrote and then ran. If the agent writes a Python file that reads your .env and connects to production, that is Python reading a file it is allowed to read, and no rule here sees it. The boundary that holds in that case is what the credential itself is permitted to do — a read-only database user, a reader-role service principal — not what the agent is permitted to type.