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

> Fix Claude Code hook execution errors by verifying JSON configuration, script executability, and stdin/JSON handling. Learn troubleshooting steps for common issues.

- Repository: [Luong NGUYEN/claude-howto](https://github.com/luongnv89/claude-howto)
- Tags: how-to-guide
- Published: 2026-03-30

---

**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`](https://github.com/luongnv89/claude-howto/blob/main/.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`](https://github.com/luongnv89/claude-howto/blob/main/auto-adapt-mode.py) to write errors to file. |
| **Token-tracker shows weird numbers** | The [`context-tracker.py`](https://github.com/luongnv89/claude-howto/blob/main/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`](https://github.com/luongnv89/claude-howto/blob/main/auto-adapt-mode.py) hook did not seed the baseline because the marker file exists, but [`settings.json`](https://github.com/luongnv89/claude-howto/blob/main/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**:
   ```bash
   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:
   ```bash
   echo $?
   ```

5. **Review hook logs** if the script uses the `log()` helper from [`auto-adapt-mode.py`](https://github.com/luongnv89/claude-howto/blob/main/auto-adapt-mode.py):
   ```bash
   cat ~/.claude/auto-adapt-mode.log
   ```

6. **Run the hook manually** with a mock JSON payload to isolate logic errors:
   ```bash
   echo '{"hook_event_name":"UserPromptSubmit","session_id":"test","transcript_path":"./dummy.jsonl"}' | python3 ~/.claude/hooks/context-tracker.py
   ```

7. **Verify file permissions**:
   ```bash
   ls -l ~/.claude/hooks/
   chmod +x ~/.claude/hooks/*.sh
   ```

8. **Confirm external dependencies** are available:
   ```bash
   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:

```python
#!/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`](https://github.com/luongnv89/claude-howto/blob/main/settings.json):

```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`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/auto-adapt-mode.py) into your scripts:

```python
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:

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

```

Monitor in real-time:

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

```

### Debugging the Context Tracker

Test the token calculation logic from [`06-hooks/context-tracker.py`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/context-tracker.py) with a mock transcript:

Create `sample-transcript.jsonl`:

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

```

Run manually:

```bash
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:

```json
{
  "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:

- **[`06-hooks/README.md`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/README.md)** – Documents the hook model, event list (`UserPromptSubmit`, `PostToolUse`, `Stop`), JSON contract, and configuration schema
- **[`06-hooks/context-tracker.py`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/context-tracker.py)** – Demonstrates reading transcripts, estimating token usage, and writing temporary state files for `UserPromptSubmit` and `Stop` events
- **[`06-hooks/context-tracker-tiktoken.py`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/context-tracker-tiktoken.py)** – Enhanced version using the `tiktoken` library for higher accuracy token counting
- **[`06-hooks/auto-adapt-mode.py`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/auto-adapt-mode.py)** – Shows how to auto-generate permission rules after successful tool use, including the `log()` helper and baseline seeding logic
- **[`06-hooks/security-scan.sh`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/security-scan.sh)** – Template for scanning written files for secrets before completion
- **[`06-hooks/format-code.sh`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/format-code.sh)** – Example of auto-formatting code before `Write` or `Edit` tool execution
- **[`06-hooks/pre-commit.sh`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/pre-commit.sh)** – Reference for running tests or validations before allowing tool use

## Summary

- **Hook execution depends on three layers**: JSON configuration in [`settings.json`](https://github.com/luongnv89/claude-howto/blob/main/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`](https://github.com/luongnv89/claude-howto/blob/main/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`](https://github.com/luongnv89/claude-howto/blob/main/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`](https://github.com/luongnv89/claude-howto/blob/main/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`](https://github.com/luongnv89/claude-howto/blob/main/context-tracker.py) for token usage monitoring, [`auto-adapt-mode.py`](https://github.com/luongnv89/claude-howto/blob/main/auto-adapt-mode.py) for permission rule learning, and [`security-scan.sh`](https://github.com/luongnv89/claude-howto/blob/main/security-scan.sh) for secret detection. The [`README.md`](https://github.com/luongnv89/claude-howto/blob/main/README.md) in that folder provides the authoritative specification for the JSON input/output contract and event types.