claude-code - 💡(How to fix) Fix permission allowlist over-escapes Bash() patterns containing parens/quotes/dollars, producing 'unhandled node type: string' prompts mid-session [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#57784Fetched 2026-05-11 03:25:29
View on GitHub
Comments
1
Participants
2
Timeline
6
Reactions
0
Timeline (top)
labeled ×5commented ×1

When Claude Code persists a Bash(...) permission allowlist entry via the "always allow" prompt — particularly for commands containing shell metacharacters (), ', ", or $ — the stored pattern is over-escaped to a form that the matcher's AST parser cannot handle. The matcher then emits unhandled node type: string and falls back to prompting the user for permission on every subsequent invocation that should match, even though the user explicitly authorized the command earlier in the session. In long-running or autonomous sessions this can pile up dozens of spurious prompts and effectively kill unattended work.

Root Cause

I'm running a Stop hook that strips entries with these escape patterns from ~/.claude/settings.local.json after each turn. Source: https://gist.github.com/... (happy to share if useful — it's ~80 lines of Python). This stops the prompts from recurring but doesn't address the root cause.

Fix Action

Workaround

I'm running a Stop hook that strips entries with these escape patterns from ~/.claude/settings.local.json after each turn. Source: https://gist.github.com/... (happy to share if useful — it's ~80 lines of Python). This stops the prompts from recurring but doesn't address the root cause.

Code Example

curl -s "https://api.example.com/foo" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('foo'))"

---

Bash(curl -s "https://api.example.com/foo" | python3 -c "import sys,json; d=json.load\(sys.stdin\); print\(d.get\('foo'\)\)")

---

Bash(curl -sL "https://api.github.com/repos/jj-vcs/jj/releases/latest" | python3 -c "import sys,json; r=json.load\(sys.stdin\); ...")
Bash(python3 -c "import sys,json; d=json.load\(sys.stdin\); [print\(m.get\('id','?'\)\) for m in d.get\('data',[]\)]")
Bash(grep -oE "\(Buy Now|Make an Offer|Purchase Price|asking|\\\\\\$[0-9,]+\)" /tmp/wiz-hd.html)
Bash(python3 -c "import json;print\(len\(json.load\(open\('cdx-full.json'\)\)\)-1\)")
Bash(grep -oiE 'price[^<]{0,80}|\\$[0-9,]+')
RAW_BUFFERClick to expand / collapse

Permission allowlist over-escapes Bash() patterns containing parens/quotes/dollars, producing unhandled node type: string prompts mid-session

Summary

When Claude Code persists a Bash(...) permission allowlist entry via the "always allow" prompt — particularly for commands containing shell metacharacters (), ', ", or $ — the stored pattern is over-escaped to a form that the matcher's AST parser cannot handle. The matcher then emits unhandled node type: string and falls back to prompting the user for permission on every subsequent invocation that should match, even though the user explicitly authorized the command earlier in the session. In long-running or autonomous sessions this can pile up dozens of spurious prompts and effectively kill unattended work.

Versions

  • claude 2.1.122 (current installed; bug also present in 2.1.119)
  • Linux x64, Node 22.x via @anthropic-ai/claude-code npm package
  • Settings file: ~/.claude/settings.local.json (user scope)

Reproduction

  1. Run a command containing shell metacharacters that triggers a permission prompt, e.g.

    curl -s "https://api.example.com/foo" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('foo'))"
  2. Choose "always allow" or its equivalent in the prompt UI.

  3. Observe the entry in ~/.claude/settings.local.json — it appears as something like:

    Bash(curl -s "https://api.example.com/foo" | python3 -c "import sys,json; d=json.load\(sys.stdin\); print\(d.get\('foo'\)\)")

    Note the literal backslashes before each (, ), and '. (Variants observed in the wild also include \\$, \\\\\\$, \", etc.)

  4. Re-run the same command. Instead of matching the allowlist, the matcher errors with unhandled node type: string and prompts again.

Impact

I hit this during an autonomous overnight session driving a long-running ETL. After ~1 hour, accumulated bad entries triggered enough false prompts to interrupt the run. Real entries observed in my settings.local.json after a single session — sample (15 found at peak):

Bash(curl -sL "https://api.github.com/repos/jj-vcs/jj/releases/latest" | python3 -c "import sys,json; r=json.load\(sys.stdin\); ...")
Bash(python3 -c "import sys,json; d=json.load\(sys.stdin\); [print\(m.get\('id','?'\)\) for m in d.get\('data',[]\)]")
Bash(grep -oE "\(Buy Now|Make an Offer|Purchase Price|asking|\\\\\\$[0-9,]+\)" /tmp/wiz-hd.html)
Bash(python3 -c "import json;print\(len\(json.load\(open\('cdx-full.json'\)\)\)-1\)")
Bash(grep -oiE 'price[^<]{0,80}|\\$[0-9,]+')

Each of these is a legitimate command that I authorized once and would need to re-authorize on every retry — they don't match the allowlist as written.

Workaround

I'm running a Stop hook that strips entries with these escape patterns from ~/.claude/settings.local.json after each turn. Source: https://gist.github.com/... (happy to share if useful — it's ~80 lines of Python). This stops the prompts from recurring but doesn't address the root cause.

Suggested fix

Two options I can think of, in order of preference:

  1. Persist commands as typed (no extra escaping) — the matcher already handles raw shell metacharacters fine in the un-escaped form, since the user-typed form is exactly what the matcher needs to compare against later invocations.
  2. Fall back to a glob pattern like Bash(python3 -c *) when the literal form contains shell metacharacters, with a UI hint explaining the broadening.

Option 1 is presumably what users expect — they typed the command, it should be stored verbatim. Option 2 is safer if there's a reason the literal form needs to round-trip through some serialization that the current code is already escaping for.

Telemetry

If anyone wants to repro: open a session that runs Bash commands with quoted Python -c payloads or grep patterns containing parens, choose "always allow" once, then watch the prompts return.

Happy to provide the full sanitized list of bad entries from my settings.local.json if a maintainer wants the raw data.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING