claude-code - 💡(How to fix) Fix [BUG] PreToolUse "if: Bash(foo*)" falsely matches Bash commands containing $() [2 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#48722Fetched 2026-04-16 06:52:49
View on GitHub
Comments
2
Participants
2
Timeline
7
Reactions
0
Author
Timeline (top)
labeled ×5commented ×2

PreToolUse hook if patterns falsely match Bash commands that contain $() command substitution in certain positions. #38017 / #32876 were about literal ( ) characters in quoted arguments and were fixed in v2.1.89; this is a different pattern-matcher path that handles $() command substitution syntax and is still broken on v2.1.109.

Previously filed as #42457, auto-closed as a duplicate of #38017 in error and auto-locked before re-triage.

Error Message

Previously filed as #42457, auto-closed as a duplicate of #38017 in error and auto-locked before re-triage.

Root Cause

PreToolUse hook if patterns falsely match Bash commands that contain $() command substitution in certain positions. #38017 / #32876 were about literal ( ) characters in quoted arguments and were fixed in v2.1.89; this is a different pattern-matcher path that handles $() command substitution syntax and is still broken on v2.1.109.

Previously filed as #42457, auto-closed as a duplicate of #38017 in error and auto-locked before re-triage.

Code Example

$ claude --version
2.1.109 (Claude Code)

---

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(foo*)",
            "command": "echo 'HOOK_FIRED' >&2; exit 2"
          }
        ]
      }
    ]
  },
  "permissions": { "allow": ["Bash(*)"] }
}

---

claude -p --permission-mode acceptEdits 'Use the Bash tool with command: echo $(date)'
RAW_BUFFERClick to expand / collapse

Summary

PreToolUse hook if patterns falsely match Bash commands that contain $() command substitution in certain positions. #38017 / #32876 were about literal ( ) characters in quoted arguments and were fixed in v2.1.89; this is a different pattern-matcher path that handles $() command substitution syntax and is still broken on v2.1.109.

Previously filed as #42457, auto-closed as a duplicate of #38017 in error and auto-locked before re-triage.

Environment

$ claude --version
2.1.109 (Claude Code)
  • OS: macOS (Darwin 24.6.0)

Configuration (.claude/settings.json)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(foo*)",
            "command": "echo 'HOOK_FIRED' >&2; exit 2"
          }
        ]
      }
    ]
  },
  "permissions": { "allow": ["Bash(*)"] }
}

The hook is meant to fire only for commands starting with foo. Observation point: when it fires, HOOK_FIRED appears on stderr and the tool call is rejected with exit code 2.

Results on v2.1.109

Reproduced in both bypassPermissions and acceptEdits permission modes. In default mode, $() is pre-rejected by a separate safety check before the hook is evaluated, so this specific hook-matching bug is not directly observable there.

#CommandBash(foo*) match?
1fooYes ✅ (expected yes)
2echo helloNo ✅
3echo "text $(echo hello) text"No ✅
4cat 'hello().txt' (control: #38017 case)No ✅
5echo $(date)Yes ❌
6echo --flag="$(echo value)"Yes ❌
7echo "a" "$(echo b)"Yes ❌
8ls "$(echo /tmp)"Yes ❌

Row 4 is the control case: cat 'hello().txt' contains literal ( ) in a quoted argument — exactly the #38017 / #32876 class — and correctly does not match Bash(foo*) on v2.1.109. Rows 5–8 contain $() command substitution and incorrectly match the same pattern under the same config / same version. This isolates the bug from #38017 / #32876: literal ( ) in quoted arguments no longer match Bash(foo*), but $() command substitution still does.

Positional pattern

  • $() inside a single double-quoted interpolated string (row 3) → correctly not matched
  • $() as a standalone argument (row 5), after = (row 6), or as a separate quoted arg (rows 7–8) → incorrectly matched

Impact

Benign commands containing $() (e.g. ls "$(git rev-parse --show-toplevel)") are incorrectly blocked in acceptEdits / bypassPermissions when hooks use patterns such as Bash(foo*).

Reproduction

  1. Create a scratch dir with .claude/settings.json containing the config above.
  2. Run:
    claude -p --permission-mode acceptEdits 'Use the Bash tool with command: echo $(date)'
  3. Observe HOOK_FIRED on stderr and the tool call rejected — despite echo $(date) clearly not starting with foo.
  4. Replace the command with cat 'hello().txt' to confirm the #38017 control: same config, same version, no match, no block.

I have a dual-hook logging harness (records every attempted Bash command + whether each if condition matched) I can share on request.

Related observation

Using "matcher": "Bash(foo*)" at the outer level (instead of "matcher": "Bash" + inner "if": "Bash(foo*)") causes the hook to never fire, not even for bare foo. If matcher is intentionally tool-name-only, this may be a docs / load-time-validation gap rather than a pattern-matcher bug.

extent analysis

TL;DR

The issue can be worked around by modifying the if pattern in the PreToolUse hook to correctly handle Bash commands with $() command substitution.

Guidance

  • Review the if pattern in the PreToolUse hook to ensure it correctly handles Bash commands with $() command substitution.
  • Consider using a more specific pattern that excludes commands with $() substitution, such as Bash(^foo), to match only commands that start with foo.
  • Test the modified hook with various commands, including those with $() substitution, to verify the correct behavior.
  • If the issue persists, consider sharing the dual-hook logging harness to further investigate the problem.

Example

No code snippet is provided as the issue is related to the pattern-matcher path and not a specific code implementation.

Notes

The issue is specific to the PreToolUse hook and the Bash(foo*) pattern, and may not affect other hooks or patterns. The workaround may need to be adjusted based on the specific requirements of the hook.

Recommendation

Apply a workaround by modifying the if pattern in the PreToolUse hook to correctly handle Bash commands with $() command substitution, as the root cause is related to the pattern-matcher path and not a specific version issue.

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

claude-code - 💡(How to fix) Fix [BUG] PreToolUse "if: Bash(foo*)" falsely matches Bash commands containing $() [2 comments, 2 participants]