Claude Code Keeps Asking Permission for the Same Command

Claude Code re-prompts for a Bash or Edit you already allowed. Usually the allow pattern is too narrow, lives in the wrong settings scope, or the command is compound — here is the exact fix.

You answered “Yes, and don’t ask again” to a Bash(npm test) prompt, but two minutes later the same prompt reappears. You add an allowlist entry, and the next Bash(npm run build) still asks. Every Edit, every shell command demands a click, and your “agent” has become a key-holder asking permission a hundred times per session.

Fastest fix: run /permissions inside Claude Code, look at which rules are loaded and from which file, and confirm your command actually matches one. If it does not, the usual culprits (as of June 2026) are: the command is compound (Claude Code splits on &&, |, ; and needs a rule per subcommand), your pattern is too specific to match trailing flags, or the rule sits in a settings scope this project does not read. If you just want the prompts to stop for a trusted project, press Shift+Tab to cycle into a more permissive permission mode, or set "defaultMode": "acceptEdits". The rest of this guide turns those one-liners into a reliable fix.

How the permission system works (read this first)

Three things govern every prompt:

  1. Three rule lists. permissions.allow, permissions.ask, and permissions.deny in your settings files. Rules are evaluated in the order deny, then ask, then allow — the first match wins, and a more specific allow rule does not beat a matching ask or deny rule.
  2. Permission mode. default prompts on first use of each tool; acceptEdits auto-accepts edits and common filesystem commands; plan is read-only; bypassPermissions skips almost everything. Cycle modes live with Shift+Tab, or pin one with "defaultMode" in settings.
  3. Settings precedence. Managed settings > command-line flags > .claude/settings.local.json > .claude/settings.json > ~/.claude/settings.json. A deny at any level cannot be overridden by an allow at a lower one.

Run /permissions to see the live, merged rule set and the source file for each rule. That single command resolves most “why is it still asking” confusion.

Which bucket are you in

SymptomMost likely causeJump to
Looping command contains &&, |, ;, or a subshellCompound command — needs a rule per subcommandCause 1
Rule reads Bash(npm test) but command is npm test --watchPattern too narrow (no trailing wildcard)Cause 2
Rule exists but /permissions does not list itWrong settings scope or invalid JSONCause 3
Edit(/Users/you/...) never matchesPath anchor wrong — that is project-root-relative, not absoluteCause 4
Allowed, but a hook still forces the promptPreToolUse hook returning ask/exit 2Cause 5
Allowed in settings but still asks every runMode is default and you keep clicking “Just this time”Cause 6

Common causes and fixes

Cause 1: The command is compound (the most common surprise)

Claude Code is shell-aware. It splits a command on &&, ||, ;, |, |&, &, and newlines, then requires a rule that matches each subcommand independently. So Bash(cd packages/web && npm test) is not covered by Bash(npm test*) alone, and an old-style rule like Bash(cd * && npm *) does not grant the compound either.

Fix: add a rule for each subcommand. When you approve a compound command with “Yes, and don’t ask again”, Claude Code already does this for you — it saves a separate rule per subcommand (up to 5), e.g. approving git status && npm test saves a rule for npm test. To pre-author it by hand:

{
  "permissions": {
    "allow": [
      "Bash(npm test:*)",
      "Bash(npm run build:*)"
    ]
  }
}

A bare cd into a subdirectory of your working directory is treated as read-only and does not need its own rule. Note one exception: a compound command that combines cd with git always prompts regardless of target directory.

Cause 2: The allow pattern is more specific than the command

Bash(npm test) is an exact match. The moment Claude runs npm test --watch or npm test src/, the rule misses.

Use a trailing wildcard to cover the family. Two equivalent forms:

  • Bash(npm test *) — note the space before *. The space enforces a word boundary: Bash(ls *) matches ls -la but not lsof.
  • Bash(npm test:*) — the :* suffix is the same as a trailing * and is what the permission dialog tends to write.

Wildcards can sit anywhere: Bash(git * main) matches both git push origin main and git merge main. A single * spans multiple arguments, including spaces.

Process wrappers are stripped automatically, so Bash(npm test *) also covers timeout 30 npm test. The stripped set is fixed: timeout, time, nice, nohup, stdbuf, and flag-less xargs. Environment runners are not strippednpx, docker exec, mise exec, direnv exec, and devbox run execute their arguments, so write a rule that names both the runner and the inner command, e.g. Bash(docker exec mycontainer npm test).

Cause 3: The rule is in a scope this project does not read (or the JSON is broken)

There are three writable layers. Pick by intent:

Scope of ruleFile to edit
Personal default across all projects~/.claude/settings.json
Shared with the team via git.claude/settings.json in repo root
Personal in this project, not in git.claude/settings.local.json

For project-specific but personal commands like npm test, prefer settings.local.json so the rule does not land in teammates’ setups.

How to confirm: run /permissions. If your rule is not listed there, Claude Code is not reading it — either it is in the wrong scope, or the JSON is malformed (a single trailing comma can silently void the file). Validate with python3 -m json.tool < .claude/settings.local.json. Settings are re-read live in current builds, but if you are on an older version, restart Claude Code after editing.

Cause 4: The path anchor on an Edit/Read rule is wrong

This trips up nearly everyone. Read and Edit rules follow gitignore semantics with four anchor styles, and a leading single slash is not an absolute path:

PatternAnchorExample
//pathAbsolute, from filesystem rootEdit(//Users/you/proj/src/**)
~/pathHome directoryRead(~/.zshrc)
/pathProject root, not the filesystem rootEdit(/src/**)
path or ./pathCurrent directoryRead(*.env)

So a rule like Edit(/Users/you/proj/src/**) is interpreted as <project-root>/Users/you/proj/src/** and matches nothing. Use Edit(/src/**) for project-relative, or Edit(//Users/you/proj/src/**) (double slash) for a true absolute path. A bare filename like Read(.env) matches at any depth, identical to Read(**/.env).

Cause 5: A PreToolUse hook is forcing the prompt

A PreToolUse hook can return an ask decision, or exit with code 2 to block, and that takes precedence over your allow rules. No allowlist entry will silence it.

Find it:

grep -rn "PreToolUse" ~/.claude/settings.json .claude/settings.json .claude/settings.local.json

If a hook with matcher: "Bash" runs an interactive check, it re-prompts on every Bash regardless of allow rules. Narrow the matcher to the destructive commands you actually care about, or remove the hook. (For the inverse problem — a hook blocking edits you wanted — see the related guide on hook blocks below.)

Cause 6: You are in default mode and keep clicking “Just this time”

“Just this time” never persists, by design. Only “Yes, and don’t ask again” writes a rule. If you find yourself clicking the transient option repeatedly, either click the persistent one once, or switch the session’s mode.

The fast path for a trusted project

For your own repos where you trust Claude’s judgment, do not fight individual rules — change the mode. Press Shift+Tab to cycle (defaultacceptEditsplan → back), or set it persistently:

{
  "permissions": {
    "defaultMode": "acceptEdits",
    "deny": [
      "Bash(rm -rf *)",
      "Bash(git push --force*)",
      "Bash(curl * | bash*)"
    ]
  }
}

acceptEdits auto-accepts file edits and common filesystem commands (mkdir, touch, mv, cp) in your working directory while still prompting on riskier Bash. Deny rules still win, so the destructive patterns above stay blocked. For fully unattended runs in a container or VM you can launch with claude --dangerously-skip-permissions (or --permission-mode bypassPermissions), but never do that on a machine where the agent could cause real damage — rm -rf / and rm -rf ~ still prompt as a circuit breaker, but nothing else does.

If you want allow-everything with targeted exceptions in standard mode, the cleanest pattern is a bare Bash allow plus a PreToolUse hook that rejects the few commands you want blocked, rather than trying to enumerate every safe command.

How to confirm it’s fixed

  • Run /permissions and confirm the rule you added is listed, sourced from the file you expected.
  • Trigger each previously-looping command manually and confirm no prompt appears.
  • Run a clearly-destructive command (rm -rf /tmp/test-fixture) and confirm it still prompts or is denied — your deny rules are working.
  • Run a 10-minute exploratory session and count prompts. After the fix you should see <= 5 (ideally 0-2).

Common pitfalls

  • Writing one rule for a compound command. Split it: one rule per subcommand around each &&, |, ;.
  • Using regex. Claude Code uses simple globs (* and gitignore-style **), not full regex.
  • Forgetting the space before *. Bash(ls*) also matches lsof; Bash(ls *) does not.
  • Anchoring an Edit rule with a single leading slash and expecting an absolute path. Use // for absolute.
  • Trying to constrain curl URLs with a Bash pattern. It is fragile (flags, redirects, variables all dodge it). Deny curl/wget and use WebFetch(domain:...) allow rules instead.
  • Editing the wrong scope file, then concluding “settings does not work.” Run /permissions to see what actually loaded.

FAQ

Q: What is the difference between “Yes, and don’t ask again”, “Yes”, and “No”? A: “Yes, and don’t ask again” writes a rule to settings (persistent). A plain “Yes” allows just this call. Misclicking the non-persistent option is the single most common source of repeat prompts.

Q: I approved a command with && and it still asks. Why? A: Compound commands are split per subcommand. Approving once saves a rule for each piece (up to 5), but if the exact subcommand text varies between runs the saved rule may miss. Pre-author a wildcard rule like Bash(npm test:*) for each subcommand.

Q: Can I use full regex in allow patterns? A: No. Bash rules use globs with *; Read/Edit rules use gitignore patterns with * and **. For anything more complex, use a PreToolUse hook.

Q: How do I make Claude Code stop asking entirely for one project? A: Set "defaultMode": "acceptEdits" in that project’s .claude/settings.json, or Shift+Tab into it per session. For unattended container runs, --dangerously-skip-permissions. Keep destructive deny rules in place either way.

Q: Why does my deny rule not block a command I expected it to block? A: Deny rules use the same pattern shapes. Bash(rm -rf /) does not match Bash(rm -rf /tmp/x) — use Bash(rm -rf *) for the family. Remember deny is evaluated before allow, so it always wins on a match.

Q: Are MCP server tools governed by the same system? A: Yes. MCP calls appear as mcp__server__tool and can be allowed or denied the same way, e.g. mcp__puppeteer__* for every tool from the puppeteer server.

External references: Claude Code permissions docs and permission modes.

Tags: #Claude Code #agent #Troubleshooting