Skip to main content
OrchestKit v6.7.1 — 67 skills, 38 agents, 77 hooks with Opus 4.6 support
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