Skip to content

About

Universal hook test harness for Claude Code — auto-discover, mock payloads, CI-friendly

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Universal Hook Test Harness for Claude Code

Python 3.10+ License: MIT GitHub release

Test your Claude Code hooks offline, before they break a real session.

The Problem

Claude Code hooks are powerful -- they intercept tool calls, inject context, block dangerous commands, and automate post-session cleanup. But there is no built-in way to test them.

You write a Python hook, wire it into settings.local.json, and pray it works. If it has a syntax error, a missing import, or an unexpected exit code, you find out mid-session when Claude stalls or your hook silently fails. There is no --dry-run. There is no test harness. Until now.

The Solution

This tool sends mock JSON payloads to each hook's stdin -- exactly like Claude Code does at runtime -- and validates:

  • Exit codes (0 = pass, 2 = block, anything else = crash)
  • Crash-free execution (syntax errors, import failures, unhandled exceptions)
  • JSON output validity (if your hook returns JSON)
  • Execution time (flags hooks slower than 5 seconds)
  • Event mapping (auto-discovers which lifecycle event each hook handles)

Quick Start

Copy test-hooks.py into your project, or clone this repo:

git clone https://github.com/VoxCore84/claude-code-hook-tester.git
cd claude-code-hook-tester

# Test hooks in the current project (auto-discovers .claude/hooks/)
python test-hooks.py

# Test hooks in a specific project
python test-hooks.py --project-dir /path/to/my-project

# Point directly at a hooks directory
python test-hooks.py --hooks-dir /path/to/.claude/hooks

No dependencies. Pure Python 3.10+. No pip install needed.

What It Does

  1. Auto-discovers all .py files in .claude/hooks/
  2. Reads settings.local.json (or settings.json) to determine which lifecycle event each hook handles
  3. Falls back to hook-manifest.json or filename heuristics if no settings file is found
  4. Sends the correct mock payload to each hook via stdin
  5. Reports pass/fail/warn with colorized terminal output
==================================================================
  Hook Test Harness for Claude Code
==================================================================

  [ OK ] compact-reinject.py                SessionStart             exit=0     42ms
  [ OK ] cpp-build-reminder.py              PostToolUse              exit=0     38ms
  [ OK ] edit-verifier.py                   PostToolUse              exit=0     51ms
  [ OK ] large-file-guard.py                PostToolUse              exit=0     35ms
  [ OK ] notification-toast.py              Notification             exit=0     29ms
  [ OK ] precompact-snapshot.py             PreCompact               exit=0     44ms
  [ OK ] prompt-context-injector.py         UserPromptSubmit         exit=0     67ms
  [ OK ] session-stats.py                   PostToolUse              exit=0     33ms
  [WARN] sql-safety.py                      PreToolUse               exit=2     41ms
         -> Hook BLOCKED with reason: SQL contains DROP TABLE
  [ OK ] stop-verify.py                     Stop                     exit=0     39ms
  [ OK ] subagent-complete.py               SubagentStop             exit=0     31ms
  [ OK ] sync-on-git.py                     PostToolUse              exit=0     48ms

  Results: 11 passed, 0 failed, 1 warnings out of 12 hooks

Features

Auto-Discovery from Settings

The harness reads your settings.local.json to understand hook wiring. Given this config:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/sql-safety.py\""
          }
        ]
      }
    ]
  }
}

It automatically maps sql-safety.py to the PreToolUse event and sends the correct mock payload.

Custom Payloads

Override any event's test payload by creating a payloads.json:

{
  "PreToolUse": {
    "hook_event_name": "PreToolUse",
    "tool_name": "Bash",
    "tool_input": {
      "command": "rm -rf /important-data"
    },
    "session_id": "test-dangerous-commands"
  }
}
python test-hooks.py --payloads payloads.json

This lets you test specific scenarios -- like verifying your safety hook blocks rm -rf.

CI Integration with JSON Output

python test-hooks.py --json
{
  "version": "1.0.0",
  "total": 12,
  "passed": 11,
  "failed": 0,
  "warnings": 1,
  "results": [
    {
      "script": "sql-safety.py",
      "event": "PreToolUse",
      "status": "warn",
      "exit_code": 2,
      "duration_ms": 41.3,
      "blocked": true,
      "errors": [],
      "warnings": ["Hook BLOCKED with reason: SQL contains DROP TABLE"]
    }
  ]
}

Use in GitHub Actions:

- name: Test Claude Code hooks
  run: |
    python tools/test-hooks.py --json > hook-results.json
    python -c "
    import json, sys
    r = json.load(open('hook-results.json'))
    if r['failed'] > 0:
        print(f'FAILED: {r[\"failed\"]} hooks broken')
        sys.exit(1)
    print(f'All {r[\"total\"]} hooks passed')
    "

Verbose Mode

python test-hooks.py --verbose

Shows stdout and stderr from each hook, useful for debugging why a hook produces unexpected output.

Hook Manifest (Manual Mapping)

If you don't use settings.local.json, create a hook-manifest.json in your hooks directory:

{
  "sql-safety.py": ["PreToolUse"],
  "edit-verifier.py": ["PostToolUse"],
  "my-custom-hook.py": ["Stop", "SessionEnd"]
}

A hook can be tested against multiple events by listing them in the array.

Usage

python test-hooks.py [OPTIONS]

Options:
  --hooks-dir PATH       Path to hooks directory (default: .claude/hooks/)
  --settings PATH        Path to settings.local.json for hook wiring
  --manifest PATH        Path to hook-manifest.json for manual mapping
  --payloads PATH        Path to custom payloads JSON file
  --project-dir PATH     Project root (sets CLAUDE_PROJECT_DIR for hooks)
  --timeout SECONDS      Per-hook timeout (default: 10)
  --json                 Output results as JSON for CI
  --verbose, -v          Show hook stdout/stderr
  --version              Show version

What It Catches

Issue How It Surfaces
Syntax errors FAIL -- exit code 1, stderr shows traceback
Missing imports FAIL -- exit code 1, ModuleNotFoundError in stderr
Unexpected exit code FAIL -- any exit code other than 0 or 2
Unintended blocks WARN -- exit code 2 means the hook would reject the tool call
Timeouts FAIL -- hook exceeded the timeout threshold
Slow execution WARN -- hook took longer than 5 seconds
Invalid JSON output Noted in verbose mode (not an error, since hooks can print plain text)
Missing event mapping GUES -- no settings file found, event inferred from filename

Mock Payload Format

Each lifecycle event receives a specific JSON structure on stdin. The built-in defaults cover all 10 Claude Code events:

Event Key Fields
PreToolUse tool_name, tool_input
PostToolUse tool_name, tool_input, tool_response
PostToolUseFailure tool_name, tool_input, error
UserPromptSubmit prompt
PreCompact trigger
SessionStart source
Stop stop_reason, transcript_suffix
Notification message
SubagentStop (minimal)
SessionEnd reason

All payloads include hook_event_name and session_id. See payloads.json for the complete default set.

Event Inference Priority

When determining which event a hook handles, the harness uses this priority chain:

  1. settings.local.json / settings.json -- authoritative, parsed from your real hook wiring
  2. hook-manifest.json -- manual override file you create
  3. Filename heuristics -- keywords like safety map to PreToolUse, toast maps to Notification, etc.

The harness tells you when it falls back to heuristics (shown as GUES in the status column).

Requirements

  • Python 3.10+
  • No external dependencies

License

MIT License. See LICENSE.


Built by VoxCore84

About

Universal hook test harness for Claude Code — auto-discover, mock payloads, CI-friendly

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages