← Alle Playbooks
Playbook· setup

Debugging hooks when nothing fires

Ten steps to work out why your Claude Code hook never kicks in. From the JSON typo to the matcher pattern.

You've set the hook up. You edit a file, you run Bash, you type a prompt. And nothing happens. No block, no log, not a single sign that your hook even exists. This is the most common failure case with hooks, and usually it comes down to one of ten things. I'll go through them in order.

This assumes you've been through the hook playbook (Hooks against hallucinations) once already and have a hook sitting in ~/.claude/settings.json. If not, start there and come back here when it doesn't fire.

1. Check the settings file for valid JSON

First item, and unfortunately the most common one too. A missing comma, one closing brace too many, a smart quote instead of a normal quotation mark. Claude Code doesn't log that loudly, it silently ignores the hooks section.

cat ~/.claude/settings.json | jq .

If jq throws a parse error, you've found your bug. If jq runs through, move on. On macOS and Linux settings.json always lives in ~/.claude/, on Windows it sits in the AppData Roaming path.

2. The hook path has to be absolute

Relative paths in the command property don't work reliably, because Claude Code gets started from all kinds of working directories. Write the full path in, with $HOME resolved:

{
  "type": "command",
  "command": "/home/du/.claude/hooks/read-before-edit.sh"
}

Not ./hooks/read-before-edit.sh and not ~/.claude/hooks/read-before-edit.sh without shell expansion. A tilde does not get expanded inside JSON.

3. The hook script has to be executable

I forget this one regularly myself. The script exists, the path is right, but it isn't executable and Claude Code throws a permission denied in the background that you never get to see.

chmod +x ~/.claude/hooks/*.sh
ls -la ~/.claude/hooks/

The x flags have to be there. If you just created your script, they're often missing.

4. Does the matcher really match the tool

Hooks have a matcher that runs as a regex against the tool name. Edit|Write|MultiEdit is a different tool set from mcp__filesystem__write_file. If your hook targets file edits but your model happens to be writing over MCP, the hook doesn't fire. As of May 2026 the native tools are Read, Write, Edit, MultiEdit, Bash, Glob, Grep. Anything with an mcp__ prefix is MCP and needs its own matcher.

You can test that with the regex tool of your choice, or simply with a bypass hook that matches on .* and does nothing but log which tools show up during a run. Which leads straight to point 7.

5. With PreToolUse the exit code is what counts

PreToolUse hooks block tool execution when the exit code is anything other than 0. If your script crashes internally but still ends on exit 0 (say because of a set +e at the top, or because the last statement happened to succeed), Claude Code assumes everything is fine and runs the tool.

A pattern that works: set -e at the top, then targeted checks with an explicit exit 1 and a stderr message. Example:

#!/usr/bin/env bash
set -e

INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ -z "$FILE" ]]; then
  exit 0  # nicht unser Tool
fi

if ! grep -q "$FILE" "$CLAUDE_SESSION_LOG"; then
  echo "Blocked: $FILE wurde noch nicht gelesen" >&2
  exit 1
fi

Stderr becomes visible in the Claude Code console, stdout travels into the tool output pipeline. Mix the two up and Claude Code takes your blocking message for a tool result.

6. Stdin carries the tool input as JSON

Claude Code hands the complete tool call over as JSON on stdin. If your script doesn't do a cat or a read, it never sees that input and can't decide anything. Check with:

#!/usr/bin/env bash
cat > /tmp/hook-debug.json
exit 0

Trigger your hook, then look inside /tmp/hook-debug.json. Everything you need is in there, tool_name, tool_input, session_id and more. If the file is empty, your hook isn't being called at all, and then the problem sits further up.

7. Turn on a logfile

Once you've set this step up, next time round you save yourself the first six points. Build a mini log that every hook fires at the start:

#!/usr/bin/env bash
echo "[$(date -Iseconds)] $0 fired with TOOL=$CLAUDE_TOOL_NAME" >> ~/.claude/hooks.log

Let a Claude Code session run, do a few things, then tail -f ~/.claude/hooks.log. You see immediately which hooks fire and which don't. 90 percent of my hook debugging gets solved this way.

8. Trigger a settings reload

Claude Code reads settings.json at startup. If you edit the file while a session is running, the new hook doesn't go live right away. There are two reliable ways round it: start a new session, or use the settings reload command if there is one.

My workflow: I edit the settings, I close the running session with Ctrl+C or /exit, I start claude again. Only then do I test the hook. I once spent an hour debugging why my new hook wasn't firing, until I realised the session still had the old settings file in memory.

9. Hook order when you have several entries

If you've defined several PreToolUse hooks, they run in the order they appear in the array. As soon as one ends with exit code 1, that's the end of it, the hooks after it never get called. That's usually what you want, but it can confuse you when your third hook never seems to fire.

Check the order in settings.json, and above all check whether the first hook does an exit 0 when it isn't responsible (good) or whether it blocks with exit 1 on every single tool (bad). Standard pattern: a filter on the tool name right at the top, and an immediate exit 0 when it doesn't match.

10. Nuclear option, test in isolation

If nothing helps, run a minimal test. Write a hook script that does nothing but write a log, wire it in as a PreToolUse with matcher .*, start a fresh Claude Code session, and type a prompt that triggers a tool.

{
  "hooks": {
    "PreToolUse": [{
      "matcher": ".*",
      "hooks": [{
        "type": "command",
        "command": "/bin/bash -c 'echo HOOK_FIRED_$(date +%s) >> /tmp/hook-test.log; exit 0'"
      }]
    }]
  }
}

If this bare-minimum hook doesn't fire, you have a setup problem that has nothing to do with your actual hook. Check the Claude Code version (claude --version), check whether settings.json is even in the right place, check whether you maybe have a project-specific .claude/settings.json that overrides the global one.

What next

If the hook fires now but does the wrong thing, head into Hooks against hallucinations and use the patterns tested there as a template. If you want to couple hooks with MCP tools, look at MCP tool hooks. And if you're only just getting started with Claude Code, Level 4 Lesson 4 (Hooks and skills) is worth doing first as groundwork.

Source

Hook system documented at https://code.claude.com/docs/en/hooks. As of May 2026. Patterns verified on Claude Code 2.1.143.

Debugging hooks when nothing fires — StudioMeyer Academy