# How to Debug Hook Timeouts and Failures in Claude Code Harness Execution

> Debug hook timeouts and failures in Claude Code Harness execution by checking configuration files, timeout settings, and JSON output. Resolve common issues fast.

- Repository: [Chachamaru/claude-code-harness](https://github.com/Chachamaru127/claude-code-harness)
- Tags: how-to-guide
- Published: 2026-05-28

---

**Most hook timeouts and failures in claude-code-harness stem from unsynchronized configuration files, insufficient timeout values in [`hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json)** – The master configuration file editable by developers ([source](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json))
- **[`.claude-plugin/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json)** – The plugin-bundled copy loaded by the host before cache builds ([source](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json))

After editing the master file, you must run [`./scripts/sync-plugin-cache.sh`](https://github.com/Chachamaru127/claude-code-harness/blob/main/./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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.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:

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

```bash
diff hooks/hooks.json .claude-plugin/hooks.json

```

If differences exist, run the sync script:

```bash
./scripts/sync-plugin-cache.sh

```

### 3. Validate JSON Output Manually

Execute the hook command directly (found in the `"command"` field of [`hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks.json)) to inspect output:

```bash
./scripts/session-cleanup.sh

```

The script should emit clean JSON like:

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

```bash
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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks.json):

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

```bash
export CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=45000  # 45 seconds

```

Remember to sync files after editing [`hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks.json).

### 6. Run the Doctor Report

Use the built-in diagnostic to audit configuration:

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

```json
{
  "hooks": [
    {
      "type": "command",
      "command": "/bin/bash -c 'go test ./...' _ hook validate",
      "timeout": 300,
      "async": true
    }
  ]
}

```

Edit both [`hooks/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json) and [`.claude-plugin/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json), then run [`./scripts/sync-plugin-cache.sh`](https://github.com/Chachamaru127/claude-code-harness/blob/main/./scripts/sync-plugin-cache.sh).

### Adding Runtime Safety Guards

Add internal timing checks to hook scripts to prevent runaway processes:

```bash
#!/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:

```bash
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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json) | Master hook configuration table | [View](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json) |
| [`.claude-plugin/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json) | Runtime plugin copy (must stay synced) | [View](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json) |
| [`.claude/rules/hooks-editing.md`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude/rules/hooks-editing.md) | Timeout guidelines and editing policy | [View](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude/rules/hooks-editing.md) |
| [`scripts/sync-plugin-cache.sh`](https://github.com/Chachamaru127/claude-code-harness/blob/main/scripts/sync-plugin-cache.sh) | Syncs source and plugin configurations | [View](https://github.com/Chachamaru127/claude-code-harness/blob/main/scripts/sync-plugin-cache.sh) |
| [`scripts/session-cleanup.sh`](https://github.com/Chachamaru127/claude-code-harness/blob/main/scripts/session-cleanup.sh) | Example `SessionEnd` hook implementation | [View](https://github.com/Chachamaru127/claude-code-harness/blob/main/scripts/session-cleanup.sh) |

## Summary

- **Synchronize configurations**: Always run [`./scripts/sync-plugin-cache.sh`](https://github.com/Chachamaru127/claude-code-harness/blob/main/./scripts/sync-plugin-cache.sh) after editing [`hooks/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks/hooks.json) to update [`.claude-plugin/hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.claude-plugin/hooks.json)
- **Adjust timeouts**: Increase `"timeout"` values (seconds) in [`hooks.json`](https://github.com/Chachamaru127/claude-code-harness/blob/main/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`](https://github.com/Chachamaru127/claude-code-harness/blob/main/hooks.json). Set this variable to 45000 (45 seconds) or higher before starting your session, as documented in [`.claude/rules/hooks-editing.md`](https://github.com/Chachamaru127/claude-code-harness/blob/main/.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.