How to Use Security Guidance Hooks for Detecting Dangerous Code Patterns in Claude Code
Security guidance hooks for detecting dangerous code patterns intercept PreToolUse operations in Claude Code by scanning file paths and content against configurable rules, emitting warnings to stderr and exiting with code 2 to block risky edits until acknowledged.
The security guidance plugin ships with Claude Code as a built-in safeguard that runs before file-modifying tools execute. Located in the anthropics/claude-code repository, the hook system evaluates edits against a JSON-defined ruleset without requiring separate linting steps or manual code review.
Architecture of the Security Guidance System
The plugin consists of four tightly integrated components that handle registration, pattern matching, state persistence, and maintenance.
| Component | Role | Source Path |
|---|---|---|
hooks.json |
Declares the hook type (PreToolUse) and maps it to the executable handler (security_reminder_hook.py). When Claude Code dispatches an edit request, it reads this registry to determine which hooks to invoke. |
plugins/security-guidance/hooks/hooks.json |
security_reminder_hook.py |
The Python implementation that parses the JSON payload from stdin, extracts the session_id, tool name, target file path, and proposed content. It iterates over hardcoded SECURITY_PATTERNS—checking path_check lambdas first, then substrings matches—and prints warnings to stderr before exiting with code 2 to abort the operation. |
plugins/security-guidance/hooks/security_reminder_hook.py |
State management (load_state, save_state) |
Persists a set of previously shown warnings to ~/.claude/security_warnings_state_<session_id>.json. This deduplication prevents the same security reminder from spamming the user repeatedly during a single Claude Code session. |
Same file as above |
Debug logging (debug_log) |
Writes timestamped diagnostic messages to /tmp/security-warnings-log.txt for plugin developers troubleshooting hook execution. |
Same file as above |
Cleanup routine (cleanup_old_state_files) |
Removes state files older than 30 days (10% chance per run) to prevent stale data buildup. | Same file as above |
Environment toggle (ENABLE_SECURITY_REMINDER) |
When set to "0", the hook short-circuits immediately and returns exit code 0, effectively disabling all security checks for that session. |
Same file as above |
How the Hook Intercepts Edits
When Claude Code receives a request such as Edit file X, the execution flow follows this pipeline:
- Hook Discovery: Claude Code reads
plugins/security-guidance/hooks/hooks.jsonto discover thePreToolUsehook type and locate the handler command. - Payload Delivery: It executes
python3 ${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.pywith a JSON payload written to stdin containing thesession_id,tool_name,tool_input.file_path, andtool_input.content(ortool_input.new_stringforWrite/MultiEditoperations). - Pattern Evaluation: The hook loads
SECURITY_PATTERNS—an array of rule objects where each entry contains aruleName, optionalpath_check(lambda function),substrings(list of dangerous tokens), and areminderstring. - Match Detection: For each rule, it first evaluates the
path_checkagainst the file path, then scans the content for anysubstringsentry. If the rule specifiespath_check: ".github/workflows/"and the content includes${{ github.event.issue.title }}, the match triggers. - Deduplication Check: The hook constructs a unique key (
<file_path>-<rule_name>), loads the session-specific warning set from~/.claude/security_warnings_state_<session_id>.json, and skips printing if the key exists. - Blocking Output: On a new match, it prints the
remindertext to stderr, saves the updated state, and exits with code 2. This non-zero exit code aborts the tool operation, forcing the user to address the warning before proceeding.
Configuration and Custom Rules
The hooks.json Registration File
The hook registers itself through a declarative JSON file that Claude Code parses at startup:
{
"hooks": [
{
"hook": "PreToolUse",
"commands": [
"python3",
"${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py"
]
}
]
}
This registration ensures the hook runs before any Write, Edit, or MultiEdit tool executes, providing real-time security guidance without manual linting steps.
Modifying SECURITY_PATTERNS
To extend detection capabilities, edit security_reminder_hook.py and append a new dictionary to the SECURITY_PATTERNS list:
SECURITY_PATTERNS = [
# ... existing rules ...
{
"ruleName": "hardcoded_secret",
"substrings": ["API_KEY=", "SECRET="],
"reminder": "⚠️ Hardcoded secret detected. Move secrets to a secret manager or environment variable."
}
]
Rules support two detection mechanisms:
- Path-based filtering: Use the
path_checkkey with a lambda string (e.g.,".github/workflows/") to restrict the rule to specific directories or file extensions. - Content scanning: The
substringsarray lists literal tokens that, when found in the proposed edit content, trigger the warning.
State Management and Deduplication
The hook maintains session-specific state to avoid overwhelming users with repetitive warnings. When a rule matches:
- The hook computes a stable key from the file path and rule name.
- It loads existing keys from
~/.claude/security_warnings_state_<session_id>.json. - If the key exists, the hook exits silently with code 0, allowing the edit to proceed.
- If the key is new, the hook persists it to disk and prints the warning, ensuring one reminder per unique pattern per session.
The cleanup_old_state_files function periodically purges state files older than 30 days, preventing unbounded disk usage across long-running Claude Code installations.
Enabling and Disabling the Hook
The security guidance hook is enabled by default. Control it via environment variable:
# Explicitly enable (default behavior)
export ENABLE_SECURITY_REMINDER=1
# Disable for the current session (allows dangerous edits without warnings)
export ENABLE_SECURITY_REMINDER=0
When ENABLE_SECURITY_REMINDER is set to "0", the hook exits immediately with code 0, bypassing all pattern checks. Use this only in isolated environments where you deliberately accept the risk.
Debugging Hook Execution
To inspect hook behavior manually, pipe a test payload into the script:
cat <<EOF | python3 plugins/security-guidance/hooks/security_reminder_hook.py
{
"session_id": "debug-session-001",
"tool_name": "Write",
"tool_input": {
"file_path": ".github/workflows/deploy.yml",
"content": "run: echo ${{ github.event.issue.title }}"
}
}
EOF
This outputs the matching rule's reminder to stderr and exits with code 2, simulating the blocking behavior Claude Code enforces during actual edits.
Summary
- Security guidance hooks for detecting dangerous code patterns operate as
PreToolUseinterceptors in Claude Code, evaluating file paths and content against configurableSECURITY_PATTERNSbefore edits apply. - The system comprises
hooks.jsonfor registration,security_reminder_hook.pyfor pattern matching, and session-based state files in~/.claude/to deduplicate warnings. - Rules support both
path_checklambdas (directory filtering) andsubstringsarrays (content scanning), with matches printing to stderr and exiting code 2 to block operations. - Enable or disable the hook via the
ENABLE_SECURITY_REMINDERenvironment variable, and extend detection by appending dictionaries toSECURITY_PATTERNSin the source file. - Debug output writes to
/tmp/security-warnings-log.txt, while automatic cleanup removes state files older than 30 days.
Frequently Asked Questions
How do security guidance hooks integrate with Claude Code's plugin system?
According to the anthropics/claude-code source, the plugin system discovers hooks by reading plugins/security-guidance/hooks/hooks.json at startup. This file declares the PreToolUse hook type and specifies the Python executable path. When Claude Code prepares to invoke a tool like Write or Edit, it serializes the tool input to JSON and pipes it through the registered hook command. If the hook exits with a non-zero status (specifically code 2), Claude Code surfaces the stderr output in the chat interface and aborts the tool invocation, effectively embedding security checks directly into the edit pipeline without requiring external CI/CD steps.
What types of dangerous patterns can the hook detect?
The default SECURITY_PATTERNS configuration in security_reminder_hook.py targets high-risk code constructs including GitHub Actions workflow injections (e.g., ${{ github.event.issue.title }} in run: commands), unsafe Node.js patterns (child_process.exec, eval, new Function), and React's dangerouslySetInnerHTML. Each pattern defines either a path_check constraint (such as .github/workflows/) or content substrings (such as exec( or rm -rf), allowing granular control over when warnings trigger. You can extend this list to cover organization-specific risks—like hardcoded API keys or deprecated cryptographic functions—by appending new rule dictionaries to the SECURITY_PATTERNS array.
Why does the hook exit with code 2 specifically?
Exit code 2 serves as a semantic signal to Claude Code that the operation should be blocked due to a security concern rather than a system error. The security_reminder_hook.py implementation explicitly calls sys.exit(2) after printing the warning to stderr, distinguishing security failures from other plugin crashes or environment issues. This convention allows Claude Code to differentiate between a failed hook (which might retry) and a security intervention (which requires user acknowledgment). The session state tracking in ~/.claude/security_warnings_state_<session_id>.json ensures that even if the user reissues the same edit command, the hook recognizes the previously shown warning and permits the operation, preventing permanent lockouts while maintaining initial friction against risky changes.
Can I run security guidance hooks in CI/CD pipelines or pre-commit checks?
While designed for interactive Claude Code sessions, the hook's architecture supports headless execution because it reads JSON from stdin and returns standard exit codes. You can invoke python3 plugins/security-guidance/hooks/security_reminder_hook.py programmatically by constructing the expected JSON payload containing session_id, tool_name, and tool_input. However, the built-in state management assumes a single-session context tied to ~/.claude/, so CI implementations should either clear state files between runs or modify the load_state function to use ephemeral storage. For pre-commit workflows, consider adapting the SECURITY_PATTERNS logic into a standalone linter that reads staged files rather than JSON payloads, though this replicates functionality already available in dedicated security scanners.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →