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:
- Reads
hooks.jsonto find matching hooks for the event - Loads the compiled bundle from
dist/ - Calls the exported function with a
HookInputobject - 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 reasonStep 1: Verify Hooks Are Loaded
/ork:doctorThe 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:
- The hook entry exists for the lifecycle event you expect
- The
commandfield points to a valid file indist/ - The
enabledfield is not set tofalse - The
eventfield 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.mjsThe 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.mjsA 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 -uTimeout
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:
- Open
src/hooks/hooks.json - Find the hook entry
- Add
"enabled": falseto the hook object - 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" -lFire-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
| Check | Command | Expected |
|---|---|---|
| hooks.json exists | ls src/hooks/hooks.json | File present |
| Bundles compiled | ls src/hooks/dist/ | 11 bundle directories |
| Hook entry for event | grep "EventName" src/hooks/hooks.json | Entry found |
| Hook enabled | Check hooks.json entry | No "enabled": false |
| Bundle file exists | ls <path from hooks.json> | File present |
| Hook returns valid JSON | Run hook directly with mock input | {"continue": true/false} |
| TypeScript compiles | cd src/hooks && npm run typecheck | No errors |
| Build is current | cd src/hooks && npm run build | Build succeeds |
Troubleshooting
Solutions for the 10 most common OrchestKit issues.
FAQ
Frequently asked questions about OrchestKit.
Last updated on