Skip to content

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:

bash
cat << EOF
VERY IMPORTANT: Check .agents/steering/
EOF
exit 0

Timeout 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):

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

bash
echo "CLAUDE_PLUGIN_ROOT=$CLAUDE_PLUGIN_ROOT" >&2
ls -la "$CLAUDE_PLUGIN_ROOT"
exit 0

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

bash
if ! [command]; then
  echo "Failed" >&2
fi
exit 0

Check Hook Logs

Problem: Need to see what your hook is doing.

Fix: Hook stderr is logged (stdout is injected). Tail logs:

bash
tail -f ~/.claude/logs/claude-code.log

Debug 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.

Built with VitePress and powered by Claude Code