Troubleshooting Hooks
When a hook isn't behaving as expected, work down this list.
Hook Output Not Reaching Claude
Problem: SessionStart hook runs but Claude doesn't receive the injected context.
Cause: Output format is wrong or not reaching stdout.
Fix: SessionStart hooks must write to stdout and exit 0. See SessionStart Context Injection for both plain text and JSON formats. Minimal example:
cat << EOF
VERY IMPORTANT: Check .agents/steering/
EOF
exit 0Timeout Exceeded
Problem: Hook runs but is skipped silently.
Cause: Hook execution exceeded its timeout threshold.
Fix: Add timeout to hook config (2-5 seconds for SessionStart, 5-10 for PostToolUse):
{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hook.sh", "timeout": 5}Path Resolution Failed
Problem: Hook script can't find files; ${CLAUDE_PLUGIN_ROOT} is wrong or empty.
Cause: Variable not expanded by Claude Code, usually because hook is not in a plugin.
Fix: Ensure the hook config lives in plugins/<name>/hooks/hooks.json. Test expansion:
echo "CLAUDE_PLUGIN_ROOT=$CLAUDE_PLUGIN_ROOT" >&2
ls -la "$CLAUDE_PLUGIN_ROOT"
exit 0Hook Fails Silently
Problem: Hook runs but output is lost and session continues.
Cause: Hook exited with non-zero code.
Fix: Always exit 0, even on error:
if ! [command]; then
echo "Failed" >&2
fi
exit 0Check Hook Logs
Problem: Need to see what your hook is doing.
Fix: Hook stderr is logged (stdout is injected). Tail logs:
tail -f ~/.claude/logs/claude-code.logDebug output goes to stderr; inject content goes to stdout.
Matcher Issues
Problem: Hook with a matcher never fires.
Cause: Matcher is case-sensitive and must match tool name exactly (e.g., Write, Bash, Edit).
Fix: Verify the matcher in hooks.json matches the tool that triggers it. Omit matcher to run on all tools for that event.
Related
- Hooks & Lifecycle — Hook architecture and lifecycle events
- Component Model — Plugins, commands, skills
- Plugins Overview — Installation and marketplace