Skip to main content
OrchestKit v7.43.0 — 104 skills, 36 agents, 173 hooks · Claude Code 2.1.105+
OrchestKit

Hook Debugging

How to diagnose and fix hook execution issues.

Hooks are TypeScript functions that execute at specific points in the Claude Code lifecycle. When a hook misbehaves -- not firing, firing incorrectly, or producing errors -- this guide walks through systematic debugging.

How Hooks Execute

Hooks are defined in src/hooks/hooks.json and compiled to JavaScript bundles in src/hooks/dist/. When Claude Code hits a lifecycle event (session start, tool use, prompt submit, etc.), it:

  1. Reads hooks.json to find matching hooks for the event
  2. Loads the compiled bundle from dist/
  3. Calls the exported function with a HookInput object
  4. Receives a HookResult: {"continue": true} to proceed or {"continue": false} to block
Lifecycle Event
    |
    v
hooks.json (find matching hooks)
    |
    v
dist/bundle.mjs (load compiled code)
    |
    v
exported function(HookInput) -> HookResult
    |
    v
continue: true  --> proceed
continue: false --> block with reason

Step 1: Verify Hooks Are Loaded

/ork:doctor

The doctor command checks whether hooks.json is readable and whether the dist/ directory contains compiled bundles. If it reports hooks as unhealthy, the issue is in compilation, not logic.

Step 2: Check hooks.json

Open src/hooks/hooks.json and verify:

  1. The hook entry exists for the lifecycle event you expect
  2. The command field points to a valid file in dist/
  3. The enabled field is not set to false
  4. The event field matches the lifecycle event (e.g., PreToolUse, SessionStart)

Example hook entry:

{
  "event": "PreToolUse",
  "hooks": [
    {
      "type": "command",
      "command": "node dist/pre-tool-use/dangerous-command-blocker.mjs",
      "timeout": 5000
    }
  ]
}

Step 3: Run the Hook Directly

You can test a hook in isolation by calling it with a mock input:

echo '{"event":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | \
  node src/hooks/dist/pre-tool-use/dangerous-command-blocker.mjs

The hook should return a JSON object. If it throws an error, the issue is in the hook code itself.

Step 4: Check the Compiled Bundle

If the hook source (src/hooks/src/) looks correct but the behavior is wrong, the compiled bundle might be stale:

# Rebuild all hooks
cd src/hooks && npm run build

# Verify the bundle was updated
ls -la dist/pre-tool-use/dangerous-command-blocker.mjs

A common mistake is editing TypeScript source without rebuilding. The dist/ directory is what Claude Code actually executes. Always run cd src/hooks && npm run build after editing hook source files.

Common Hook Errors

Schema Validation Failure

Error: "Hook output does not match expected schema"

Cause: The hook returns an object that is not a valid HookResult. Every hook must return:

{"continue": true}

or:

{"continue": false, "reason": "Why it was blocked"}

Extra fields are ignored, but missing continue will fail validation.

Export Format Issues

Error: "Cannot find default export" or "module is not a function"

Cause: The hook file exports its function incorrectly. Hooks must use a named export that matches the pattern expected by the hook runner:

// Correct
export async function dangerousCommandBlocker(input: HookInput): Promise<HookResult> {
  return { continue: true };
}

// Incorrect -- default export
export default async function(input: HookInput): Promise<HookResult> {
  return { continue: true };
}

The hook runner uses module[Object.keys(module)[0]] to find the exported function, so the first named export is what gets called.

Bundle Not Found

Error: "ENOENT: no such file or directory, dist/..."

Cause: The hook references a bundle file that does not exist. Either the build failed or the command field in hooks.json has a typo.

# List all bundles
ls src/hooks/dist/**/*.mjs

# Compare with hooks.json references
grep -o 'dist/[^"]*' src/hooks/hooks.json | sort -u

Timeout

Error: "Hook timed out after 5000ms"

Cause: The hook takes too long to execute. This can happen with hooks that make network requests or read large files.

Fix: Increase the timeout in hooks.json, or convert the hook to use the fire-and-forget pattern for non-blocking execution.

Hook Toggle System

To isolate whether a specific hook is causing problems, disable it temporarily:

  1. Open src/hooks/hooks.json
  2. Find the hook entry
  3. Add "enabled": false to the hook object
  4. Test whether the issue resolves

This is faster than commenting out code and rebuilding. Remember to re-enable the hook after debugging.

Fire-and-Forget Hooks

OrchestKit has 6 hooks that use a fire-and-forget async pattern. These hooks dispatch background work (analytics, network I/O, startup tasks) without blocking the main Claude Code thread. If a fire-and-forget hook fails silently, check:

# Look for error logs from async dispatchers
grep -r "error" src/hooks/dist/ --include="*.mjs" -l

Fire-and-forget hooks intentionally do not return errors to Claude Code. If they fail, the failure is logged but does not block the user's workflow.

Debugging Checklist

CheckCommandExpected
hooks.json existsls src/hooks/hooks.jsonFile present
Bundles compiledls src/hooks/dist/11 bundle directories
Hook entry for eventgrep "EventName" src/hooks/hooks.jsonEntry found
Hook enabledCheck hooks.json entryNo "enabled": false
Bundle file existsls <path from hooks.json>File present
Hook returns valid JSONRun hook directly with mock input{"continue": true/false}
TypeScript compilescd src/hooks && npm run typecheckNo errors
Build is currentcd src/hooks && npm run buildBuild succeeds
Edit on GitHub

Last updated on