How to Debug Hook Timeouts and Failures in Claude Code Harness Execution
Most hook timeouts and failures in claude-code-harness stem from unsynchronized configuration files, insufficient timeout values in hooks.json, or malformed JSON output from hook scripts.
Debugging hook execution in the claude-code-harness requires understanding how the runtime dispatches events through hooks/hooks.json and manages process timeouts. Whether you're troubleshooting a hanging SessionEnd cleanup script or a PreToolUse validation that exits with code 2, the root causes typically involve configuration mismatches or timing constraints. This guide walks through the exact steps to diagnose and resolve these issues using the actual source implementation from Chachamaru127/claude-code-harness.
Understanding Hook Architecture and Timeouts
The harness implements an event-driven hook system where the runtime reads configuration at session start and dispatches commands, prompts, or agents based on events like PreToolUse and SessionEnd.
The Dual-File Configuration System
Hook definitions live in two locations that must remain synchronized:
hooks/hooks.json– The master configuration file editable by developers (source).claude-plugin/hooks.json– The plugin-bundled copy loaded by the host before cache builds (source)
After editing the master file, you must run ./scripts/sync-plugin-cache.sh to propagate changes. Failure to sync causes the host to run stale hook definitions or skip hooks entirely.
Timeout Mechanisms
The harness implements two distinct timeout controls:
- Per-hook timeouts – Defined in
hooks.jsonas"timeout": 10(value in seconds) - Global SessionEnd timeout – Controlled by the environment variable
CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS(value in milliseconds)
According to .claude/rules/hooks-editing.md (lines 75-84), versions prior to CC v2.1.74 enforced a hard 1500 ms limit for SessionEnd hooks regardless of configuration. Current versions respect the environment variable, which defaults to 1500 ms if unset.
Common Failure Modes and Root Causes
Hook Timeout Errors
When you see hook XYZ timed out after N s in session logs, the "timeout" value in hooks.json is insufficient for the work performed. This commonly affects:
- Long-running test suites invoked by
PreToolUsehooks - Network-dependent cleanup operations in
SessionEndhooks - Heavy agent prompts that process large codebases
Exit Code 2 Not Blocking
If a hook exits with code 2 but the session proceeds without blocking, verify your Claude Code version. The block-pattern recognition was fixed in CC v2.1.90; earlier versions treat exit code 2 as a general failure rather than an intentional block signal.
JSON Validation Failures
Hooks must write valid JSON to stdout. The harness expects either {"ok":true} or {"ok":false,"reason":"..."} for prompt/agent hooks, or JSON containing hookSpecificOutput for other types. Common causes of validation failures include:
- Debug
echostatements polluting stdout - Color codes or formatting characters in output
- Missing required keys like
ok
Silent Hook Skips
If a hook never executes and the session proceeds normally, check that the hook entry exists in .claude-plugin/hooks.json. The host reads from the plugin copy, not the master source, so an out-of-sync configuration renders hooks invisible to the runtime.
Step-by-Step Debugging Workflow
1. Collect Verbose Hook Logs
Run the harness with debug output enabled to capture hook execution details:
CLAUDE_CODE_DEBUG=1 bin/harness [your-command]
Search the transcript for lines starting with hook to identify which event fired and whether it returned JSON or an error.
2. Verify Configuration Synchronization
Check both configuration files for discrepancies:
diff hooks/hooks.json .claude-plugin/hooks.json
If differences exist, run the sync script:
./scripts/sync-plugin-cache.sh
3. Validate JSON Output Manually
Execute the hook command directly (found in the "command" field of hooks.json) to inspect output:
./scripts/session-cleanup.sh
The script should emit clean JSON like:
{"continue": true, "message": "Session cleanup completed"}
If you see stray text or color codes, redirect them to stderr or remove them entirely.
4. Test with Mock Input
Many hooks expect JSON payloads via stdin. Simulate the harness invocation:
echo '{"event":"SessionEnd"}' | ./scripts/session-cleanup.sh
Confirm the script completes within the expected time window.
5. Adjust Timeout Values
For heavy operations, increase the per-hook timeout in hooks.json:
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/bin/bash -c '...' _ hook auto-test",
"timeout": 180,
"async": true
}
]
}
For SessionEnd hooks specifically, set the environment variable before starting Claude Code:
export CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=45000 # 45 seconds
Remember to sync files after editing hooks.json.
6. Run the Doctor Report
Use the built-in diagnostic to audit configuration:
bin/harness doctor --hook-report
This flags out-of-sync files and timeouts below recommended thresholds.
Configuration Examples and Fixes
Increasing Timeout for Heavy Test Hooks
When PreToolUse hooks invoke long test suites, increase the timeout and consider async execution:
{
"hooks": [
{
"type": "command",
"command": "/bin/bash -c 'go test ./...' _ hook validate",
"timeout": 300,
"async": true
}
]
}
Edit both hooks/hooks.json and .claude-plugin/hooks.json, then run ./scripts/sync-plugin-cache.sh.
Adding Runtime Safety Guards
Add internal timing checks to hook scripts to prevent runaway processes:
#!/usr/bin/env bash
set -euo pipefail
START=$(date +%s)
# Heavy work here
go test ./...
# Safety check
ELAPSED=$(( $(date +%s) - START ))
MAX=120
if (( ELAPSED > MAX )); then
echo '{"ok":false,"reason":"hook exceeded safe runtime"}' >&2
exit 1
fi
echo '{"ok":true}'
Setting Persistent SessionEnd Timeout
Add to your shell profile to apply globally:
export CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=45000
This prevents the harness from killing cleanup scripts that need time to remove temporary files or upload artifacts.
Key Source Files for Debugging
| File | Purpose | GitHub Link |
|---|---|---|
hooks/hooks.json |
Master hook configuration table | View |
.claude-plugin/hooks.json |
Runtime plugin copy (must stay synced) | View |
.claude/rules/hooks-editing.md |
Timeout guidelines and editing policy | View |
scripts/sync-plugin-cache.sh |
Syncs source and plugin configurations | View |
scripts/session-cleanup.sh |
Example SessionEnd hook implementation |
View |
Summary
- Synchronize configurations: Always run
./scripts/sync-plugin-cache.shafter editinghooks/hooks.jsonto update.claude-plugin/hooks.json - Adjust timeouts: Increase
"timeout"values (seconds) inhooks.jsonfor heavy operations, and setCLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS(milliseconds) for session cleanup - Validate output: Ensure hook scripts emit clean JSON without debug text or color codes
- Check versioning: Use Claude Code v2.1.90 or later for proper exit code 2 blocking behavior
- Diagnose systematically: Use
CLAUDE_CODE_DEBUG=1andbin/harness doctor --hook-reportto identify configuration drift and timing issues
Frequently Asked Questions
Why does my SessionEnd hook timeout even with a high timeout value in hooks.json?
The SessionEnd event uses a separate global timeout controlled by the CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS environment variable (measured in milliseconds), not the per-hook timeout in hooks.json. Set this variable to 45000 (45 seconds) or higher before starting your session, as documented in .claude/rules/hooks-editing.md.
How do I fix "JSON validation failed" errors from my hook?
Run the hook script manually and inspect stdout. The harness requires valid JSON without additional output. Remove any echo statements, color codes, or logging that pollutes stdout. Ensure the output contains required fields like ok for prompt/agent hooks or hookSpecificOutput for other types.
What is the difference between exit code 2 and other non-zero exits in hooks?
Exit code 2 signals an intentional block that should halt the current operation (like preventing a tool use), while other non-zero codes indicate failure. However, this block pattern only works reliably in Claude Code v2.1.90 and later. Earlier versions treat exit code 2 as a generic failure without blocking.
How can I test a hook without running the full harness workflow?
Invoke the hook command directly with a mock JSON payload piped to stdin. For example: echo '{"tool":"Write","file":"example.go"}' | ./your-hook-script.sh. This validates both the JSON output format and the execution time without triggering the full harness lifecycle.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →