Files
gh-glittercowboy-taches-cc-…/skills/create-hooks/references/troubleshooting.md
2025-11-29 18:28:37 +08:00

9.4 KiB

Troubleshooting

Common issues and solutions when working with hooks.

Hook Not Triggering

Symptom

Hook never executes, even when expected event occurs.

Diagnostic steps

1. Enable debug mode

claude --debug

Look for:

[DEBUG] Getting matching hook commands for PreToolUse with query: Bash
[DEBUG] Found 0 hooks

2. Check hook file location

Hooks must be in:

  • Project: .claude/hooks.json
  • User: ~/.claude/hooks.json
  • Plugin: {plugin}/hooks.json

Verify:

cat .claude/hooks.json
# or
cat ~/.claude/hooks.json

3. Validate JSON syntax

Invalid JSON is silently ignored:

jq . .claude/hooks.json

If error: fix JSON syntax.

4. Check matcher pattern

Common mistakes:

Case sensitivity

{
  "matcher": "bash"  // Won't match "Bash"
}

Fix

{
  "matcher": "Bash"
}

Missing escape for regex

{
  "matcher": "mcp__memory__*"  // Literal *, not wildcard
}

Fix

{
  "matcher": "mcp__memory__.*"  // Regex wildcard
}

5. Test matcher in isolation

node -e "console.log(/Bash/.test('Bash'))"  # true
node -e "console.log(/bash/.test('Bash'))"  # false

Solutions

Missing hook file: Create .claude/hooks.json or ~/.claude/hooks.json

Invalid JSON: Use jq to validate and format:

jq . .claude/hooks.json > temp.json && mv temp.json .claude/hooks.json

Wrong matcher: Check tool names with --debug and update matcher

No matcher specified: If you want to match all tools, omit the matcher field entirely:

{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [...]  // No matcher = all tools
      }
    ]
  }
}

Command Hook Failing

Symptom

Hook executes but fails with error.

Diagnostic steps

1. Check debug output

[DEBUG] Hook command completed with status 1: <error message>

Status 1 = command failed.

2. Test command directly

Copy the command and run in terminal:

echo '{"session_id":"test","tool_name":"Bash"}' | /path/to/your/hook.sh

3. Check permissions

ls -l /path/to/hook.sh
chmod +x /path/to/hook.sh  # If not executable

4. Verify dependencies

Does the command require tools?

which jq  # Check if jq is installed
which osascript  # macOS only

Common issues

Missing executable permission

chmod +x /path/to/hook.sh

Missing dependencies

Install required tools:

# macOS
brew install jq

# Linux
apt-get install jq

Bad path

Use absolute paths:

{
  "command": "/Users/username/.claude/hooks/script.sh"
}

Or use environment variables:

{
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/script.sh"
}

Timeout

If command takes too long:

{
  "command": "/path/to/slow-script.sh",
  "timeout": 120000  // 2 minutes
}

Prompt Hook Not Working

Symptom

Prompt hook blocks everything or doesn't block when expected.

Diagnostic steps

1. Check LLM response format

Debug output shows:

[DEBUG] Hook command completed with status 0: {"decision": "approve", "reason": "ok"}

Verify JSON is valid.

2. Check prompt structure

Ensure prompt is clear:

{
  "prompt": "Evaluate: $ARGUMENTS\n\nReturn JSON: {\"decision\": \"approve\" or \"block\", \"reason\": \"why\"}"
}

3. Test prompt manually

Submit similar prompt to Claude directly to see response format.

Common issues

Ambiguous instructions

Vague

{
  "prompt": "Is this ok? $ARGUMENTS"
}

Clear

{
  "prompt": "Check if this command is safe: $ARGUMENTS\n\nBlock if: contains 'rm -rf', 'mkfs', or force push to main\n\nReturn: {\"decision\": \"approve\" or \"block\", \"reason\": \"explanation\"}"
}

Missing $ARGUMENTS

No placeholder

{
  "prompt": "Validate this command"
}

With placeholder

{
  "prompt": "Validate this command: $ARGUMENTS"
}

Invalid JSON response

The LLM must return valid JSON. If it returns plain text, the hook fails.

Add explicit formatting instructions:

IMPORTANT: Return ONLY valid JSON, no other text:
{
  "decision": "approve" or "block",
  "reason": "your explanation"
}

Hook Blocks Everything

Symptom

Hook blocks all operations, even safe ones.

Diagnostic steps

1. Check hook logic

Review the script/prompt logic. Is the condition too broad?

2. Test with known-safe input

echo '{"tool_name":"Read","tool_input":{"file_path":"test.txt"}}' | /path/to/hook.sh

Expected: {"decision": "approve"}

3. Check for errors in script

Add error output:

#!/bin/bash
set -e  # Exit on error
input=$(cat)
# ... rest of script

Solutions

Logic error

Review conditions:

# Before (blocks everything)
if [[ "$command" != "safe_command" ]]; then
  block
fi

# After (blocks dangerous commands)
if [[ "$command" == *"dangerous"* ]]; then
  block
fi

Default to approve

If logic is complex, default to approve on unclear cases:

# Default
decision="approve"
reason="ok"

# Only change if dangerous
if [[ "$command" == *"rm -rf"* ]]; then
  decision="block"
  reason="Dangerous command"
fi

echo "{\"decision\": \"$decision\", \"reason\": \"$reason\"}"

Infinite Loop in Stop Hook

Symptom

Stop hook runs repeatedly, Claude never stops.

Cause

Hook blocks stop without checking stop_hook_active flag.

Solution

Always check the flag:

#!/bin/bash
input=$(cat)
stop_hook_active=$(echo "$input" | jq -r '.stop_hook_active')

# If hook already active, don't block again
if [[ "$stop_hook_active" == "true" ]]; then
  echo '{"decision": undefined}'
  exit 0
fi

# Your logic here
if [ tests_passing ]; then
  echo '{"decision": "approve", "reason": "Tests pass"}'
else
  echo '{"decision": "block", "reason": "Tests failing"}'
fi

Or in prompt hooks:

{
  "prompt": "Evaluate stopping: $ARGUMENTS\n\nIMPORTANT: If stop_hook_active is true, return {\"decision\": undefined}\n\nOtherwise check if tasks complete..."
}

Hook Output Not Visible

Symptom

Hook runs but output not shown to user.

Cause

suppressOutput: true or output goes to stderr.

Solutions

Don't suppress output:

{
  "decision": "approve",
  "reason": "ok",
  "suppressOutput": false
}

Use systemMessage:

{
  "decision": "approve",
  "reason": "ok",
  "systemMessage": "This message will be shown to user"
}

Write to stdout, not stderr:

echo "This is shown" >&1
echo "This is hidden" >&2

Permission Errors

Symptom

Hook script can't read files or execute commands.

Solutions

Make script executable:

chmod +x /path/to/hook.sh

Check file ownership:

ls -l /path/to/hook.sh
chown $USER /path/to/hook.sh

Use absolute paths:

# Instead of
command="./script.sh"

# Use
command="$CLAUDE_PROJECT_DIR/.claude/hooks/script.sh"

Hook Timeouts

Symptom

[DEBUG] Hook command timed out after 60000ms

Solutions

Increase timeout:

{
  "type": "command",
  "command": "/path/to/slow-script.sh",
  "timeout": 300000  // 5 minutes
}

Optimize script:

  • Reduce unnecessary operations
  • Cache results when possible
  • Run expensive operations in background

Run in background:

#!/bin/bash
# Start long operation in background
/path/to/long-operation.sh &

# Return immediately
echo '{"decision": "approve", "reason": "ok"}'

Matcher Conflicts

Symptom

Multiple hooks triggering when only one expected.

Cause

Tool name matches multiple matchers.

Diagnostic

[DEBUG] Matched 3 hooks for query "Bash"

Solutions

Be more specific:

// Instead of
{"matcher": ".*"}  // Matches everything

// Use
{"matcher": "Bash"}  // Exact match

Check overlapping patterns:

{
  "hooks": {
    "PreToolUse": [
      {"matcher": "Bash", ...},        // Matches Bash
      {"matcher": "Bash.*", ...},      // Also matches Bash!
      {"matcher": ".*", ...}           // Also matches everything!
    ]
  }
}

Remove overlaps or make them mutually exclusive.


Environment Variables Not Working

Symptom

$CLAUDE_PROJECT_DIR or other variables are empty.

Solutions

Check variable spelling:

  • $CLAUDE_PROJECT_DIR (correct)
  • $CLAUDE_PROJECT_ROOT (wrong)

Use double quotes:

{
  "command": "$CLAUDE_PROJECT_DIR/hooks/script.sh"
}

In shell scripts, use from input:

#!/bin/bash
input=$(cat)
cwd=$(echo "$input" | jq -r '.cwd')
cd "$cwd" || exit 1

Debugging Workflow

Step 1: Enable debug mode

claude --debug

Step 2: Look for hook execution logs

[DEBUG] Executing hooks for PreToolUse:Bash
[DEBUG] Found 1 hook matchers
[DEBUG] Executing hook command: /path/to/script.sh
[DEBUG] Hook command completed with status 0

Step 3: Test hook in isolation

echo '{"test":"data"}' | /path/to/hook.sh

Step 4: Check script with set -x

#!/bin/bash
set -x  # Print each command before executing
# ... your script

Step 5: Add logging

#!/bin/bash
echo "Hook started" >> /tmp/hook-debug.log
input=$(cat)
echo "Input: $input" >> /tmp/hook-debug.log
# ... your logic
echo "Decision: $decision" >> /tmp/hook-debug.log

Step 6: Verify JSON output

echo '{"decision":"approve","reason":"test"}' | jq .

If jq fails, JSON is invalid.