[FEATURE] Support argument-level matching in PreToolUse hook matchers (e.g. Bash commands)

Resolved 💬 4 comments Opened Feb 1, 2026 by jugrajsingh Closed Mar 2, 2026

Feature Request

Problem

Currently, PreToolUse hook matchers can only match on the tool name (e.g., Bash, Edit, Read). There is no way to filter hooks based on the tool's input arguments — for example, matching only git add commands within the Bash tool.

This means every Bash command (including ls, pwd, cat, git status) triggers the hook, and the hook script must filter internally. This adds unnecessary overhead and complexity for plugin developers.

Tested Matcher Patterns (None Work)

I conducted thorough testing with 21 different matcher patterns. All argument-level patterns fail silently — no error, no warning, hooks simply never fire.

| Matcher | Intent | Result |
|---------|--------|--------|
| Bash(git add.*) | Regex in parens | Never fires |
| Bash(git add *) | Glob in parens | Never fires |
| Bash(git add .+) | Regex+ in parens | Never fires |
| git add | Direct command string | Never fires |
| bash (lowercase) | Case-insensitive attempt | Never fires |
| BASH (uppercase) | Case-insensitive attempt | Never fires |

Working Patterns (Tool Name Only)

| Matcher | Description | Works? |
|---------|-------------|--------|
| Bash | Exact tool name | ✅ |
| Edit | Exact tool name | ✅ |
| Edit\|Write | Pipe-separated | ✅ |
| Bas.* | Regex on tool name | ✅ |
| [BE].* | Character class | ✅ |
| ^Bash$ | Anchored regex | ✅ |
| .* | Match-all regex | ✅ |
| * | Wildcard catch-all | ✅ |
| "" (empty) | Empty matcher | ✅ |

Test Methodology

  • 21 PreToolUse hooks configured in .claude/settings.local.json
  • Each hook runs a logging script that records matcher label, tool name, and input to /tmp/hook-test-results.log
  • Tools triggered: Bash (git status, git add ., cat), Read, Glob, Grep, Write, Edit
  • Same behavior observed in both project-level hooks (.claude/settings.local.json) and plugin-level hooks (hooks/hooks.json)

<details>
<summary>Test logging script</summary>

#!/bin/bash
LOGFILE="/tmp/hook-test-results.log"
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // .tool_input.file_path // .tool_input.pattern // "none"')
echo "[$(date +%H:%M:%S)] FIRED matcher='$1' tool=$TOOL input='$CMD'" >> "$LOGFILE"
exit 0

</details>

<details>
<summary>Sample log output showing only tool-name matchers fire</summary>

[00:09:21] FIRED matcher='T09-regex-Bas-star' tool=Bash input='git status'
[00:09:21] FIRED matcher='T11-star-wildcard' tool=Bash input='git status'
[00:09:21] FIRED matcher='T19-anchored-Bash' tool=Bash input='git status'
[00:09:21] FIRED matcher='T12-empty-string' tool=Bash input='git status'
[00:09:21] FIRED matcher='T10-regex-match-all' tool=Bash input='git status'
[00:09:21] FIRED matcher='T18-charclass-BE' tool=Bash input='git status'
[00:09:21] FIRED matcher='T20-pipe-all-tools' tool=Bash input='git status'
[00:09:21] FIRED matcher='T01-exact-Bash' tool=Bash input='git status'

Note: T13 (Bash(git add.*)), T14 (Bash(git add *)), T15 (Bash(git add .+)), T16 (bash), T17 (BASH), T21 (git add) — none appear in any log output.

</details>

Proposed Solution

Support an optional argument pattern in matchers using the ToolName(pattern) syntax:

{
  "matcher": "Bash(^git add)",
  "hooks": [{
    "type": "command",
    "command": "./hooks/validate-git-add.sh",
    "timeout": 5
  }]
}

Where the pattern inside parentheses is matched against tool_input.command (for Bash) or tool_input.file_path (for Edit/Write/Read).

Additional patterns that would be useful:

"Bash(^git (add|commit))"     // Match git add and git commit
"Bash(^npm |^yarn |^pnpm )"  // Match package manager commands
"Edit(.*\\.py$)"              // Match Python file edits only
"Read(.*\\.env.*)"            // Match reading env files

Current Workaround

Use broad "Bash" matcher and filter inside the hook script:

#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

# Fast exit for non-matching commands
if ! echo "$CMD" | grep -qE "^git add"; then
  exit 0
fi

# Run actual validation
# ...

This works but has downsides:

  • Performance: Hook fires on every Bash command (~200+ times per session)
  • Complexity: Every hook script must implement its own command filtering
  • Plugin overhead: Plugin hooks like guard scripts run on every ls, cat, pwd

Secondary Issue: Silent Failure

Invalid matchers like Bash(git add.*) produce no error or warning at session start or at hook evaluation time. This makes debugging extremely difficult. A warning would help:

Warning: Hook matcher "Bash(git add.*)" contains unsupported argument pattern. 
Matchers only support tool name regex. See docs: https://code.claude.com/docs/en/hooks

Environment

  • Platform: Linux (Fedora 6.18.7-200.fc43.x86_64)
  • Tested in: Project-level hooks (.claude/settings.local.json) and plugin-level hooks (hooks/hooks.json)

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗