How to Debug Hook and Plugin Issues in Claude Code: Complete Troubleshooting Guide

Debug hook and plugin issues in Claude Code by verifying rule file loading in config_loader.py, evaluating rule matches through RuleEngine.evaluate_rules(), and testing hooks manually via JSON payloads piped to the script with ENABLE_SECURITY_REMINDER=1 enabled.

Claude Code's extensibility relies on a sophisticated hook and plugin system managed by the hookify framework. When you need to debug hook and plugin issues in Claude Code, understanding the execution flow from JSON payload generation to rule evaluation in plugins/hookify/core/rule_engine.py is essential. This guide walks through the exact diagnostic steps used in the anthropics/claude-code repository to identify why hooks block tools silently, fail to load rules, or crash during execution.

Understanding the Hook and Plugin Architecture

Claude Code processes hooks and plugins as separate Python processes that intercept tool invocations through a standardized JSON interface. The execution follows a strict pipeline from payload generation to rule-based decision making.

Hook Discovery and Rule Loading

The system scans for rule files matching .claude/hookify.*.local.md via the load_rules() function in plugins/hookify/core/config_loader.py. Each discovered file is parsed into Rule objects using Rule.from_dict, which compiles conditions into Condition objects capturing the field, operator, and pattern. If enabled: false appears in the YAML frontmatter, the loader skips that file entirely.

Rule Evaluation Logic

The RuleEngine class in plugins/hookify/core/rule_engine.py evaluates every loaded rule against incoming payloads through evaluate_rules(). When a payload matches a rule with action: "block", the engine returns a deny decision for PreToolUse/PostToolUse hooks or a block response for Stop events. Non-blocking matches return only a systemMessage warning or an empty dictionary for allow decisions.

Security Hook Reference Implementation

The built-in security reminder hook at plugins/security-guidance/hooks/security_reminder_hook.py demonstrates production patterns for file-editing operations. It reads JSON from stdin, extracts file paths and content via extract_content_from_input(), then calls check_patterns() against path-based or substring-based rules. The script maintains per-session state in ~/.claude/security_warnings_state_<session_id>.json through load_state() and save_state() to prevent duplicate warnings, emitting diagnostics to /tmp/security-warnings-log.txt via the debug_log helper.

Common Symptoms and Diagnostic Targets

When hooks malfunction, symptoms map directly to specific failure points in the architecture.

Symptom Likely Cause Where to Look
No warning appears while a rule should match Rule file not loaded, wrong event filter, or enabled: false load_rules()load_rule_file() in config_loader.py
Hook blocks unexpectedly A rule's action is "block" or the hook returns exit code 2 RuleEngine.evaluate_rules() decision logic
Hook crashes silently Uncaught exception in main(); JSON parsing failure Check debug_log output or stderr in security_reminder_hook.py
State file not updating Permission issues or JSON corruption load_state() / save_state() in security_reminder_hook.py
Performance lag Large number of rule files or heavy regexes Regex compilation (compile_regex uses lru_cache) in rule_engine.py

Step-by-Step Debugging Techniques

Enable Verbose Logging

Set the environment variable ENABLE_SECURITY_REMINDER=1 (enabled by default) and monitor the debug log:

tail -f /tmp/security-warnings-log.txt

This captures JSON parsing errors, rule match details, and state file operations from security_reminder_hook.py.

Test Hooks Manually with JSON Payloads

Simulate a PreToolUse hook invocation by piping a JSON payload directly to the hook script:

payload='{
  "session_id": "test123",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "scripts/deploy.yml",
    "content": "run: echo \"${{ github.event.issue.title }}\""
  }
}'
echo "$payload" | python3 plugins/security-guidance/hooks/security_reminder_hook.py

If the payload matches a blocking rule, the script prints the warning to stderr and exits with status 2. Non-blocking matches exit with status 0 after printing warnings.

Inspect Active Rule Sets Programmatically

Add temporary debugging output to plugins/hookify/core/config_loader.py to verify which rules are loaded:

import json
from hookify.core.config_loader import load_rules

# Load only 'bash' event rules for faster iteration

rules = load_rules(event='bash')
print(json.dumps([r.__dict__ for r in rules], indent=2))

Run any Claude Code command to execute this code and dump the active rule set, confirming that your .claude/hookify.*.local.md files are actually being parsed.

Reset Per-Session State

Delete state files to reset warning counters when testing repeated operations:

session="my-session"
state_file=$(python3 -c "import os; print(os.path.expanduser(f'~/.claude/security_warnings_state_{session}.json'))")
rm -f "$state_file"
echo "State reset for session $session"

This forces security_reminder_hook.py to treat the next matching operation as the first occurrence.

Validate Custom Rule Files

Create .claude/hookify.insecure.local.md to test the blocking mechanism:

---
name: dangerous-curl
enabled: true
event: bash
action: block
tool_matcher: Bash
conditions:
  - field: command
    operator: contains
    pattern: "curl http://"
---
⚠️ Using `curl` over an insecure HTTP endpoint can expose credentials. Use HTTPS or sanitize the URL.

When you run a Bash tool containing curl http://, the hook blocks the command and displays the warning, validating that your rule file is correctly loaded and evaluated.

Summary

  • Hook execution relies on JSON payloads passed via stdin to Python scripts in plugins/, with exit code 2 triggering blocks in PreToolUse hooks.
  • Rule debugging requires verifying .claude/hookify.*.local.md files are loaded by load_rules() in config_loader.py and parsed into valid Rule objects.
  • State management happens in ~/.claude/security_warnings_state_<session_id>.json; delete these files to reset per-session warning counters.
  • Manual testing via piped JSON payloads to security_reminder_hook.py provides immediate feedback without running full Claude Code sessions.
  • Performance issues stem from unoptimized regex patterns in rule_engine.py or excessive rule file counts affecting compile_regex with lru_cache.

Frequently Asked Questions

Why does my hook silently fail to block dangerous commands?

Silent failures typically indicate JSON parsing exceptions in security_reminder_hook.py or the hook exiting with status 0 instead of 2. Check /tmp/security-warnings-log.txt for debug_log output, and verify your rule uses action: block rather than just emitting a warning message. Ensure the event: filter in your YAML matches the hook type (PreToolUse, PostToolUse, or bash).

How do I check which rules are currently loaded?

Import load_rules from hookify.core.config_loader and print the rule dictionary representations. Run this in a Python script or add it temporarily to config_loader.py: print(json.dumps([r.__dict__ for r in load_rules()], indent=2)). This reveals whether your .claude/hookify.*.local.md files are discovered and whether enabled: false is preventing loading.

What causes the "security_warnings_state" file errors?

State file errors occur when load_state() or save_state() in security_reminder_hook.py encounter permission denied errors on ~/.claude/ or JSON decoding failures from corrupted state files. Ensure the directory exists with write permissions, or delete the specific security_warnings_state_<session_id>.json file to force regeneration.

Can I disable hooks temporarily for testing?

Set ENABLE_SECURITY_REMINDER=0 (or remove the environment variable) to disable the security reminder hook entirely. For other hooks, rename or move the corresponding .claude/hookify.*.local.md rule files, or set enabled: false in the YAML frontmatter to prevent load_rules() from including them in the RuleEngine evaluation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →