[BUG] "Don't ask again for similar commands" saves verbatim command strings, so near-identical commands re-prompt every time
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Environment — Windows 11 (26100), "defaultMode": "acceptEdits" in project settings.
Summary — The dialog offers "Yes, and don't ask again for similar commands in <project-dir>". It does not create a rule matching similar commands; it appends the verbatim command string, absolute paths included, to permissions.allow. Any command differing by one argument re-prompts and appends another single-use entry. One session: ~20 prompts, ~20 rules, none of which ever matched again.
Repro — Approve node a.mjs <dir> via the "similar commands" option → run node b.mjs <dir> → prompted again → repeat for c, d, e → inspect settings.local.json.
Expected — A rule generalising over the executable (optionally + first arg). Or, if only exact matches are possible, a label that says "this exact command" so the option isn't clicked 20 times expecting accumulation.
Actual — Sanitised excerpt showing five entries for one script pattern and four curl entries against one localhost port differing only by a timeout flag. Also notes that piped/heredoc commands appear ungeneralisable and re-prompt unconditionally.
Secondary — 2: acceptEdits excludes Bash with no UI indication; in this session every single prompt was Bash, so the enabled mode was silently irrelevant to all of them. 3: Bash rules take command prefixes but not directory scopes, so "allow node, but only in thi— the only prompt-stopping rule is a machine-wide grant for that binary.
Suggested fixes — generalise the rule or relabel; surface that acceptEdits doesy scoping for Bash.
Impact — Approval fatigue with no accumulating benefit; the natural escape is -s, a materially worse posture than the correctly-scoped rule the model can'texpress.
What Should Happen?
A rule generalising over the executable (optionally + first arg). Or, if only exact matches are possible, a label that says "this exact command" so the option isn't clicked 20 times expecting accumulation.
Error Messages/Logs
Steps to Reproduce
Approve node a.mjs <dir> via the "similar commands" option → run node b.mjs <dir> → prompted again → repeat for c, d, e → inspect settings.local.json.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.228
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
VS Code integrated terminal
Additional Information
When I asked for a fix explicitly refusing to run claude --dangerously-skip-permissions as this whole procedure feels like privilage extortion Opus 4.8 suggested i paste:
{
"permissions": {
"allow": [
"Bash(node:)", "Bash(npm:)", "Bash(git:*)",
"Bash(ls:)", "Bash(cat:)", "Bash(grep:)", "Bash(sed:)",
"Bash(curl:)", "Bash(python:)", "Bash(mkdir:*)",
"Read(//c/Users/<my_username>/)"
],
"deny": ["Read(.env)", "Read(./secrets/)", "Bash(rm -rf:)", "Bash(git push:)"],
"defaultMode": "acceptEdits"
}
}
This feels extremely wrong - using Claude in VS terminal without allowing full access is impossible with 50+ conformation requests each prompt.
3 Comments
I reproduced this on 2.1.228 (same version you reported), Linux, isolated
HOME. Non-interactiveclaude -p, so a command that isn't pre-authorized by a rule simply fails instead of prompting — that makes "did the rule match?" directly observable.Your core observation holds: the verbatim entry does not generalise.
| rule in
permissions.allow|node a.mjs|node b.mjs|node c.mjs||---|---|---|---|
|
Bash(node a.mjs)— the shape the dialog writes | ran | blocked | — ||
Bash(node:*)— prefix form | ran | ran | ran || (no rule — control) | blocked | — | — |
The control matters: without it, "blocked" could just mean the harness was broken. It isn't — one hand-written prefix rule covers every argument variation, so the ~20 accumulated entries collapse into one.
The whole replacement for those ~20 entries is this, in the project's
.claude/settings.local.json:Runnable repro, if you want to check it on your own box before trusting it:
-pis doing real work here: with no TTY there is nothing to approve, so an unauthorised command fails instead of prompting. That turns "did the rule match?" into a yes/no you can read off stdout.One premise in the report is worth correcting, because it changes your escape route.
You wrote that Bash rules take command prefixes but not directory scopes, so the only prompt-stopping rule is a machine-wide grant for that binary. The scoping isn't in the rule syntax — it's in which settings file holds the rule. I put
Bash(node:*)in one project's.claude/settings.local.jsonand left a second project without it, same machine, sameHOME, same binary, samenode a.mjs:| where the command ran | result |
|---|---|
| project with the rule in its
.claude/settings.local.json| ran || project without it | blocked |
So "allow
node, but only in this directory" is expressible today. It just isn't what the dialog produces, and nothing in the UI points at it.Your secondary point also reproduces: with
"defaultMode": "acceptEdits"and an empty allow-list,node a.mjswas still blocked.acceptEditsdoes not cover Bash, and nothing says so at the point where it matters.Practical consequence. The thing that makes this worth fixing isn't the extra clicks — it's that the accumulating-but-never-matching entries teach people the permission system doesn't work, and the obvious next step is
--dangerously-skip-permissions, which drops every Bash guard machine-wide. A correctly-scoped prefix rule in the project'ssettings.local.jsongets the same quiet session with a far smaller blast radius. Writing one is currently something you have to already know how to do.This is reproducible in Claude 1.40609.0 (f65e38) 2026-08-27T16:19:51.000Z on my Windows 11 machine and it just happened. Despite the Bash allow list covering the command, it still prompts. As I understand it, Claude Code understands separators like &&, ||, ;, |, and each subcommand must match independently. That means for cd /path && git commit, Claude Code splits it and checks each part separately against my rules. Bash(*) should still match each subcommand, but if the Windows implementation isn't splitting and matching correctly, that would explain exactly what I've been seeing lately.
@ZeoStarDelta Your splitting model matches what I measure on Linux, so what you are hitting is probably not "Windows fails to split" — it is more likely that one part of your compound is not covered by any rule. Here is the measurement, plus a way to see exactly which string your machine is matching against.
Claude Code 2.1.278, Linux, isolated
HOME,claude -p(no TTY, so anything unauthorised fails instead of prompting — that makes "did the rule match?" readable). APreToolUsehook loggedtool_input.commandverbatim and exited 0, so the table below is based on the string that actually reached the permission check, not on what I asked for.Same command in every row:
touch zzz.txt && node a.mjs. The hook confirmed that exact string arrived each time.|
permissions.allow| compound ran? |zzz.txtcreated? ||---|---|---|
|
["Bash(node:*)"]| no | no ||
["Bash(touch:*)"]| no | yes ||
["Bash(node:*)","Bash(touch:*)"]| yes | yes ||
[]— control | no | no |Rows 1 and 2 are the informative ones. In both, the compound itself was refused, and the agent then retried the two halves as separate tool calls — the hook log shows
touch zzz.txt && node a.mjsfollowed by the halves on their own. Whichever half had a rule then ran: in row 1node a.mjsalone succeeded; in row 2touch zzz.txtalone succeeded and left the file behind. Only row 3, where both halves are covered, ran the compound in one call.Row 4 is the control. Without it, "no" in the other rows would be unreadable — it could just mean my harness was broken.
One caveat that cost me a wrong conclusion. Before this run I tested with
echo HI && node a.mjsandcd . && node a.mjs, and both ran with only["Bash(node:*)"]in the allow-list. I nearly wrote that as "the whole compound passes if one part matches". It is not that —echoandcdsimply do not need a rule. If your failing example starts withcd, that part is not what is being refused. Measure with a part that leaves a visible side effect, or you cannot tell the two explanations apart.Related trap in the same run: the agent retries. My first pass scored "did the expected stdout appear?", and it appeared even in row 1 — produced by a later, different command. Score a side effect, not the output.
The diagnostic for your machine. The thing the prompt does not show you is the exact string being matched. Log it:
It exits 0, so it decides nothing — it only records. Reproduce the prompt, then read
/tmp/cc-cmds.txtand check every part of the logged string against your allow-list, not just the part you were thinking of. On Windows also check what the shell wrapper adds: if the logged string is not byte-for-byte what you typed, the rule you wrote may not be matching what is actually being evaluated.If every part is covered verbatim and it still prompts, that is a platform-specific bug worth its own report — and you would have the exact string to attach to it.
Before trusting any of this on your box, confirm the instrument can fail: empty the allow-list and check the same command is refused. If it still runs, the settings file you are editing is not the one in effect.