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:

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:

  1. Per-hook timeouts – Defined in hooks.json as "timeout": 10 (value in seconds)
  2. 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 PreToolUse hooks
  • Network-dependent cleanup operations in SessionEnd hooks
  • 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 echo statements 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.sh after editing hooks/hooks.json to update .claude-plugin/hooks.json
  • Adjust timeouts: Increase "timeout" values (seconds) in hooks.json for heavy operations, and set CLAUDE_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=1 and bin/harness doctor --hook-report to 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:

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 →