Debugging Issues with Claude Code Hook Execution: A Complete Troubleshooting Guide

When Claude Code hooks fail, the root cause is almost always a mismatch in the JSON configuration, an unexecutable script, or incorrect stdin/JSON handling—fixable by verifying the matcher pattern, chmod +x permissions, and exit codes in ~/.claude/settings.json.

Claude Code hooks are small, independent programs triggered by events like UserPromptSubmit or PostToolUse that automate workflows and enforce policies. If you are debugging issues with Claude Code hook execution, the luongnv89/claude-howto repository provides a production-ready reference implementation in the 06-hooks/ folder that demonstrates proper patterns for token tracking, permission adaptation, and security scanning.

Understanding the Three Layers of Hook Failures

Hook failures typically occur in one of three distinct layers. Isolating which layer is causing your issue is the first step in debugging.

Layer 1: Hook Configuration in settings.json

The configuration layer resides in ~/.claude/settings.json (or project-level .claude/settings.json). This JSON file tells Claude which events fire which commands.

Common failure points include:

  • Wrong matcher patterns: Using "matcher": "Bash" when you intended "matcher": "*" to catch all tools
  • Missing type declaration: Forgetting "type": "command" or providing an incorrect command path
  • Global disable: Accidentally setting "disableAllHooks": true

Layer 2: Hook Script Implementation

The script layer is your executable file (Python, Bash, or other) that Claude launches. These scripts must read JSON from stdin and write JSON (or simply exit) to stdout.

Critical implementation errors include:

  • Missing executable permissions: The file lacks chmod +x
  • Wrong input source: Reading command-line arguments instead of sys.stdin in Python
  • Incorrect exit codes: Using exit code 2 (blocking error) instead of 0 (success) when you want the tool to proceed
  • Silent failures: Swallowing exceptions so Claude assumes the hook succeeded

Layer 3: Runtime Environment

The sandbox layer supplies environment variables like CLAUDE_PROJECT_DIR and CLAUDE_ENV_FILE, along with temporary files.

Typical runtime issues:

  • Missing environment variables: The command string does not reference $CLAUDE_PROJECT_DIR where needed
  • Unavailable tooling: External dependencies like prettier, black, or tiktoken are not on the PATH
  • Permission errors: Restricted access to temp files under /tmp/claude-context-…

How Claude Code Executes Hooks (Architectural Flow)

According to the source code in luongnv89/claude-howto, Claude processes hooks through the following pipeline:

  1. Event reception: Claude receives an event (e.g., user types /optimize or a tool finishes).
  2. Matcher resolution: Claude scans the hooks object for entries whose matcher matches the tool name (Bash, Write, *, etc.).
  3. Command preparation: Environment variables are expanded, a timeout is applied, and the command launches inside the sandbox.
  4. JSON I/O: The hook reads a JSON payload from stdin (containing fields like hook_event_name, session_id, and transcript_path) and may write a JSON response to stdout.
  5. Exit-code handling:
    • 0 indicates success and continues execution
    • 2 indicates a blocking error, causing Claude to display the message and stop the tool
    • Any other non-zero exit code results in a non-blocking warning (visible only in verbose mode)
  6. Logging persistence: Hooks can write debug information to files under $HOME/.claude for post-mortem analysis.

Common Debugging Scenarios and Solutions

Symptom Diagnosis Fix
Hook never runs (no output, no error) The matcher does not match the tool name, or the hook list is empty. Verify the matcher field accepts exact strings, regex, or *. Use claude --debug to see which hooks were considered.
Hook runs but Claude still blocks the tool The hook exits with code 2 or writes a JSON permissionDecision: "deny". Ensure the script exits with 0 unless you intentionally want to block. Check stderr output—it becomes the block message.
Hook crashes silently An unhandled exception in Python or a missing command in Bash. Add set -euo pipefail to Bash scripts, wrap Python logic in try/except, and use the log() helper from auto-adapt-mode.py to write errors to file.
Token-tracker shows weird numbers The context-tracker.py hook could not read the transcript or the temporary state file was overwritten. Confirm that transcript_path exists (ls $TRANSCRIPT). Verify the temp file name claude-context-<session>.json is unique per session.
Permissions not being added automatically The auto-adapt-mode.py hook did not seed the baseline because the marker file exists, but settings.json is not writable. Ensure ~/.claude/settings.json is a regular file and writable. Delete ~/.claude/.auto-adapt-mode-initialized to force reseeding.

Step-by-Step Debugging Workflow

Follow this systematic approach to isolate hook failures:

  1. Enable debug logging:

    claude --debug
  2. Reproduce the failing action (e.g., run a Bash tool that should fire a PostToolUse hook).

  3. Inspect the debug console for:

    • The event name that fired
    • The matcher that was applied
    • The exact command line executed
  4. Check the hook’s exit code manually:

    echo $?
  5. Review hook logs if the script uses the log() helper from auto-adapt-mode.py:

    cat ~/.claude/auto-adapt-mode.log
  6. Run the hook manually with a mock JSON payload to isolate logic errors:

    echo '{"hook_event_name":"UserPromptSubmit","session_id":"test","transcript_path":"./dummy.jsonl"}' | python3 ~/.claude/hooks/context-tracker.py
  7. Verify file permissions:

    ls -l ~/.claude/hooks/
    chmod +x ~/.claude/hooks/*.sh
  8. Confirm external dependencies are available:

    which tiktoken
    python3 -c "import tiktoken"

Code Examples for Hook Debugging

Minimal Hello-World Hook (Python)

Create a basic hook to verify your configuration works:

#!/usr/bin/env python3
import json
import sys

def main():
    data = json.load(sys.stdin)
    output = {
        "hookSpecificOutput": {
            "hookEventName": data.get("hook_event_name", ""),
            "systemMessage": "Hello from my hook!"
        }
    }
    print(json.dumps(output))
    sys.exit(0)

if __name__ == "__main__":
    main()

Save as ~/.claude/hooks/hello.py, make it executable with chmod +x, and register it in settings.json:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$HOME/.claude/hooks/hello.py\""
          }
        ]
      }
    ]
  }
}

Adding Verbose Logging

Insert the logging helper from 06-hooks/auto-adapt-mode.py into your scripts:

from pathlib import Path
from datetime import datetime

def log(message: str):
    try:
        with open(Path.home() / ".claude" / "hook-debug.log", "a") as f:
            f.write(f"[{datetime.now().isoformat()}] {message}\n")
    except Exception:
        pass

Strategic instrumentation:

log(f"Received event {data.get('hook_event_name')}")
log(f"Tool name: {data.get('tool_name')}")

Monitor in real-time:

tail -f ~/.claude/hook-debug.log

Debugging the Context Tracker

Test the token calculation logic from 06-hooks/context-tracker.py with a mock transcript:

Create sample-transcript.jsonl:

{"message":{"content":"User asked a question"}}
{"message":{"content":"Claude responded with an answer"}}

Run manually:

echo '{"hook_event_name":"Stop","session_id":"debug","transcript_path":"./sample-transcript.jsonl"}' \
  | python3 ~/.claude/hooks/context-tracker.py

If the output shows 0 tokens, the read_transcript function likely failed to locate the "content" fields in the JSONL structure.

Safely Disabling a Problematic Hook

Bypass a failing hook without editing the script by replacing the command with a no-op:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "once": true,
        "hooks": [
          {
            "type": "command",
            "command": "true"
          }
        ]
      }
    ]
  }
}

The true command always exits 0, effectively neutralizing the original blocking script.

Essential Reference Files in luongnv89/claude-howto

The 06-hooks/ directory contains the definitive reference implementations:

Summary

  • Hook execution depends on three layers: JSON configuration in settings.json, executable script logic handling stdin/stdout, and runtime environment variables
  • Exit codes control behavior: 0 continues execution, 2 blocks the tool, and other non-zero codes show warnings
  • Debug systematically: Use claude --debug to verify matchers, manually test scripts with mock JSON payloads, and implement the log() helper from auto-adapt-mode.py for persistent tracing
  • Reference implementations: The luongnv89/claude-howto repository provides production-ready examples for token tracking, permission adaptation, and security scanning in the 06-hooks/ folder

Frequently Asked Questions

Why is my Claude Code hook not running at all?

The matcher pattern in settings.json likely does not match the current tool name, or you have "disableAllHooks": true set globally. Run claude --debug to see exactly which hooks Claude evaluates for each event. Verify your matcher value—use "*" to match all tools, or the exact tool name like "Bash" or "Write".

Why does Claude block the tool even when my hook succeeds?

Your script is exiting with code 2 or returning a JSON payload containing "permissionDecision": "deny". Check that your Python script calls sys.exit(0) and that Bash scripts use exit 0. Any output to stderr from a hook exiting with code 2 becomes the block message displayed in the Claude UI.

How do I debug a hook that crashes silently?

Add robust error handling and logging. In Python, wrap logic in try/except blocks and use the log() function pattern from 06-hooks/auto-adapt-mode.py to write to ~/.claude/hook-debug.log. For Bash, add set -euo pipefail at the top of scripts to ensure failures propagate. Then run the hook manually with a mock JSON payload piped to stdin to reproduce the crash outside of Claude's environment.

Where are the reference implementations for production-ready hooks?

The luongnv89/claude-howto repository maintains a complete hook framework in the 06-hooks/ directory. Key files include context-tracker.py for token usage monitoring, auto-adapt-mode.py for permission rule learning, and security-scan.sh for secret detection. The README.md in that folder provides the authoritative specification for the JSON input/output contract and event types.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →