TL;DR When Claude Code asks for permission on something you already approved, look at the other side of the
&&: that part is the one without a rule. Write a rule for each part (Bash(npm run build)next toBash(npm test)), or pick option 2 on the prompt, which saves one rule per part to.claude/settings.local.json, and check what it wrote.
Claude Code never matches your rules against the whole line. It splits the command at its operators (&&, ||, ;, |, |&, & and newlines) and each part has to match an allow rule on its own, or be one of the read-only commands it already runs without asking (cd, ls, grep, tail, git status...). With Bash(npm test) allowed, npm test runs without a prompt, but npm run build && npm test prompts again: npm run build has no rule. It cuts the other way too: a deny or ask rule fires when it matches any part.
What happens to each form
Tested on Claude Code v2.1.310 in a scratch project, with claude -p --allowedTools "Bash(npm test)" and no other rules. The denied ones come from permission_denials:
| Command | Result | Why |
|---|---|---|
npm test |
Runs | Matches the rule |
npm run build && npm test |
Prompts | npm run build has no rule |
npm run lint ; npm test |
Prompts | npm run lint has no rule |
npm test | tail -1 |
Runs | tail is read-only |
git status && npm test |
Runs | git status is read-only |
cd web && npm test |
Runs | cd inside the project is read-only |
timeout 30 npm test |
Runs | timeout is a wrapper, stripped before matching |
NODE_ENV=test npm test |
Runs | NODE_ENV is on the safe variable list |
FOO=1 npm test |
Prompts | An unknown variable in front breaks the allow |
npm test && |
Prompts | A && with nothing after it can't be split |
The problem is almost never the part you approved. It's the other one. Claude chains commands out of habit (build then test, install then start), and one part without a rule is enough to bring the prompt back.
What "Yes, and don't ask again" saves
In an interactive session with no rules, the prompt for npm run build && npm test already tells you it will save two things:
Bash command
Run build and tests
npm run build && npm test
This command requires approval
Do you want to proceed?
❯ 1. Yes
2. Yes, and don't ask again for npm run and npm test commands in /path/to/project
3. Yes, and switch to auto mode · auto mode handles these prompts for you
4. No
Option 2 writes one rule for each part that needed approval, never one for the whole line:
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(npm test *)"
]
}
}
From then on, npm run lint ; npm test | tail -1 ran without a prompt in the same session. And with npm test already covered, the prompt for npm ci && npm test offered only Yes, and don't ask again for: npm ci *: the part that has a rule isn't saved again. The docs cap it at five rules per command.
How to stop approving it
1. Read which part the prompt names
Option 2 lists the parts that have no rule. Those are the ones to cover, not the full command shown above them.
2. Check what option 2 saved
It saves the prefix with a *, so approving npm run build leaves you Bash(npm run *): every script in package.json, a deploy script included. If you only want that one, change the line in .claude/settings.local.json to the exact form:
"Bash(npm run build)"
3. Write one rule per part, not per line
A rule like Bash(npm run build && npm test) does nothing, because nothing is ever matched against the whole line. What works is one rule per piece, in .claude/settings.json for the whole team or in .claude/settings.local.json for you alone:
{
"permissions": {
"allow": [
"Bash(npm run build)",
"Bash(npm run lint)",
"Bash(npm test *)"
]
}
}
The * goes after the subcommand: Bash(npm *) would approve npm publish too. The full syntax is in deny rules, modes and wildcards.
4. Know what will keep asking
Some combinations no prefix allow rule can approve, and knowing them saves you hunting for a rule that doesn't exist:
cd web && git status: acdinto another folder followed bygitalways prompts, becausegitcan run that folder's hooks.cd /tmp && npm test: acdoutside the project needs its own read permission for that path, even withnpm testallowed.FOO=1 npm test: a variable that isn't on the safe list, in front of the command.
5. In a cloned repo, accept the trust dialog first
Allow rules in a project's .claude/settings.json don't apply until you accept the trust dialog for that folder. In claude -p it warns at startup and ignores them:
Ignoring 1 permissions.allow entry from .claude/settings.json: this workspace has not been trusted. Run Claude Code interactively here once and accept the trust dialog, or set projects["/path/to/project"].hasTrustDialogAccepted: true in /root/.claude.json.
Deny and ask rules apply without trust, since they only restrict.
If what repeats is always the same standalone commands, the automatic allowlist turns them into rules from your history. And when rules aren't enough, the Claude Code permissions guide lays out the other levers, from rules to auto mode.
Official docs: Configure permissions