Field note · 9 September 2026 · Daniel Wright

A PreToolUse hook with matcher bash looks finished and never runs. Official docs: matching is case-sensitive, and the tool name is Bash.

A cream card printed with lowercase claude lies on a dark wooden desk beside a brass nameplate embossed with capitalised Claude
The lowercase card sits in front of the formal brass plate: same letters, different case.

The annoyance

I set a PreToolUse hook with "matcher": "bash" so it would fire on every shell call. /hooks listed it. The JSON looked finished. I ran a Bash command and nothing happened.

Same miss with "matcher": "edit". The hook sits in settings, the menu can show it, and the tool name never matches. I had the usual suspects: a script that failed to start, a permissions miss, a path I had typed wrong. Then I capitalised the matcher and the same command fired first time.

It is not a flaky script. It is the documented contract.

What Anthropic actually says

From the official Debug your configuration page, common-causes table:

Symptom: “Hook never fires”
Cause: “matcher value is lowercase, for example `"bash"`”
Fix: “Matching is case-sensitive. Tool names are capitalized: `Bash`, `Edit`, `Write`, `Read`.”

The next row in the same table is the sibling trap:

Symptom: “Hook never fires”
Cause: “matcher is a JSON array instead of a string”
Fix: “Use a single string with `|` to match multiple tools, for example `"Edit|Write"`. See matcher patterns.”

The Check hooks section is sharper on the array. It is not a quiet no-match. Official text:

“An array value is a schema error: Claude Code shows a settings error notice and rejects the whole user, project, or local settings file, `claude doctor` reports the validation failure, and no hook from that file appears in `/hooks`.”

So lowercase can still appear in /hooks and never fire. An array can take the whole settings file with it.

The hooks reference is the same rule. Matchers on PreToolUse and PostToolUse filter on the tool name. Official examples use "Bash" and "Edit|Write", not lowercase and not a JSON array:

“Bash matches only the Bash tool; Edit|Write and Edit, Write each match either tool exactly; code-reviewer matches only that agent type”

The fix

Here is a minimal settings.json snippet. The matcher is a single string, capitalised to the tool name:

settings.json · matcher Bash
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Bash hook fired\" >&2"
          }
        ]
      }
    ]
  }
}

To cover two write tools, one string with a pipe, not an array:

settings.json · matcher Edit|Write
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

These two shapes look finished and never fire: "matcher": "bash", and "matcher": ["Edit", "Write"]. The first is the case miss. The second is the schema miss.

Prove it

Prove Claude Code loaded the capitalised matcher, then prove it is on the match path.

Self-check · /hooks then a Bash call
# In-session: /hooks
# Confirm the matcher reads Bash, not bash
# Then trigger any Bash call
# Optional: claude --debug and read the log for which matchers were checked

Official debug text:

“If `/hooks` shows the hook but it still does not fire, the next step is to watch hook evaluation live. Start a session with `claude --debug` and trigger the tool call. The debug log records each event, which matchers were checked, and the hook's exit code and output.”

A lowercase "bash" never appears on that match path, because the tool name is Bash. If /hooks does not list the handler at all, you are on the array miss, or the hook is still in a standalone file, not under the hooks key in a settings file.

Checklist

  1. Use a single string matcher, not a JSON array.
  2. Capitalise the tool name: Bash, Edit, Write, Read.
  3. Join several tools with |, for example Edit|Write.
  4. Run /hooks and confirm the matcher shown is the capitalised one.
  5. Trigger the tool. If you need the match path, start with claude --debug.

Why it fails

I type tool names the way I type shell commands. bash is the binary. Bash is the Claude Code tool. Matching is case-sensitive, so the lowercase string is a matcher that matches nothing, and /hooks can still show the hook.

Same family as Exit 1 Does Not Block — the hook looks installed and the documented contract is a different one. Top-Level additionalContext Is Silent is the other silent miss: valid JSON, and the field still does nothing. Hook Paths Don't Follow the Worktree is the path version of the same class: the hook runs, and it still hits the wrong place.

If you want to run this with other operators rather than on your own, we are doing that in the public Skool community, and that is https://www.skool.com/navaigate.

Sources

Make the next AI decision concrete.

NavAIgate helps leadership teams identify high-value AI opportunities, prove them safely and turn the winners into working systems.