How to Create Security Hooks in Claude Code to Prevent Dangerous Operations
Claude Code provides a PreToolUse hook framework that intercepts file modification tools, inspects incoming paths and content via JSON stdin, and aborts operations by exiting with code 2 when dangerous patterns are detected.
Claude Code, Anthropic's AI coding assistant, ships with a robust plugin architecture in the anthropics/claude-code repository that enables developers to implement security hooks blocking risky operations before execution. The built-in security-guidance plugin demonstrates this pattern by intercepting Edit, Write, and MultiEdit commands to scan for unsafe code patterns. By creating custom hooks that leverage the PreToolUse event, you can enforce organizational security policies and prevent potentially destructive file changes.
Architecture of the Claude Code Hook System
The hook framework relies on three coordinated components that process tool execution requests before they reach the filesystem.
Core Components
Hook Definition (hooks.json) — Located at plugins/security-guidance/hooks/hooks.json, this file declares which Claude Code events trigger your validation logic and specifies the executable command to run.
Plugin Manifest (plugin.json) — Found at plugins/security-guidance/.claude-plugin/plugin.json, this JSON file makes the hook discoverable by Claude Code, registering it as a valid plugin within the ecosystem.
Hook Script (security_reminder_hook.py) — Implemented in plugins/security-guidance/hooks/security_reminder_hook.py, this Python script contains the actual validation logic. It loads configurable SECURITY_PATTERNS, inspects incoming file paths and content, maintains per-session state to avoid repetitive warnings, and exits with code 2 to block operations or 0 to permit them.
Execution Flow
When Claude Code receives a tool request, the hook framework executes the following sequence:
- Claude Code receives a tool request (e.g.,
Editon a TypeScript file). - The
PreToolUseentry inhooks.jsonmatches the tool name against the patternEdit|Write|MultiEdit. - Claude Code launches the command defined in the hook configuration:
python3 ${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py. - The hook script reads the JSON payload from stdin, containing
session_id,tool_name, andtool_input. - The script extracts the file path and content fields (which vary by tool type).
- The
check_patterns()function iterates overSECURITY_PATTERNS, executing path-check lambdas or searching for forbidden substrings in the content. - If a pattern matches, the hook checks a per-session state file at
~/.claude/security_warnings_state_<session_id>.jsonto determine if this warning was already shown. - On first encounter, the script prints the
remindertext to stderr and exits with code2, signaling Claude Code to abort the tool. - If no patterns match, the script exits with code
0and the original tool proceeds.
The framework also performs automatic cleanup of state files older than 30 days to prevent unbounded storage growth.
Implementing a Custom Security Hook
Creating a security hook requires three files: the executable script, the hook definition, and the plugin manifest.
The Hook Script
Your hook script must read JSON from stdin and exit with specific codes. Here is a minimal Python implementation that blocks usage of eval():
#!/usr/bin/env python3
import json, sys
def main():
payload = json.load(sys.stdin)
tool = payload.get("tool_name")
if tool not in ["Edit", "Write", "MultiEdit"]:
sys.exit(0)
# Extract content based on tool type
content = ""
if tool == "Edit":
content = payload["tool_input"].get("new_string", "")
elif tool == "Write":
content = payload["tool_input"].get("content", "")
elif tool == "MultiEdit":
content = " ".join(e.get("new_string", "") for e in payload["tool_input"].get("edits", []))
# Security check
if "eval(" in content:
print("⚠️ Avoid using eval() – it can lead to code injection.", file=sys.stderr)
sys.exit(2) # Abort the tool
sys.exit(0) # Allow the tool
if __name__ == "__main__":
main()
Save this as my_security_hook.py in your plugin's hooks/ directory.
Configuring hooks.json
Register your hook by creating hooks.json with a PreToolUse matcher:
{
"description": "Blocks unsafe eval() usage",
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/my_security_hook.py"
}
],
"matcher": "Edit|Write|MultiEdit"
}
]
}
}
The ${CLAUDE_PLUGIN_ROOT} variable resolves to your plugin directory at runtime.
Registering the Plugin Manifest
Create .claude-plugin/plugin.json to make Claude Code aware of your hook:
{
"name": "my-security-plugin",
"version": "0.1.0",
"description": "Example security plugin that blocks eval() usage",
"author": { "name": "Your Name", "email": "[email protected]" }
}
Place this file at the root of your plugin directory. Claude Code automatically discovers plugins containing this manifest file.
Extending Built-in Security Patterns
The reference implementation in security_reminder_hook.py uses a SECURITY_PATTERNS list defining detection rules. Each pattern entry contains:
- ruleName: Unique identifier for the rule
- pathCheck (optional): Lambda function evaluating the file path
- substrings: List of forbidden strings to search for in content
- reminder: Warning message displayed to the user
To add a new pattern blocking process.env access in shell scripts, modify the SECURITY_PATTERNS list:
{
"ruleName": "process_env_leak",
"substrings": ["process.env"],
"reminder": "⚠️ Direct use of `process.env` can expose secrets. Prefer passing needed values explicitly."
}
The hook evaluates all patterns on every file modification, ensuring comprehensive coverage without manual intervention.
Summary
- Claude Code hooks intercept tool execution via the
PreToolUseevent before files are modified. - Hooks receive JSON input on stdin containing
session_id,tool_name, andtool_input, and must exit with code0(allow) or2(abort). - The three required files are: the hook script (logic),
hooks.json(registration), andplugin.json(manifest). - The reference implementation at
plugins/security-guidance/hooks/security_reminder_hook.pydemonstrates pattern matching, session state management, and warning deduplication. - The framework is language-agnostic—any executable respecting the stdin/exit code contract works.
- State files track shown warnings per session to avoid repetitive alerts, with automatic cleanup after 30 days.
Frequently Asked Questions
What exit codes should a Claude Code security hook use?
Exit code 0 allows the tool to proceed normally, while exit code 2 aborts the operation and displays the stderr output to the user as a warning. Any non-zero exit code blocks execution, but code 2 is the conventional signal for policy violations in the Claude Code hook framework.
Which tools support PreToolUse hooks in Claude Code?
The PreToolUse event supports file modification tools including Edit, Write, and MultiEdit. You can target specific tools using regex matchers in hooks.json (e.g., Edit|Write|MultiEdit or individual tool names) to apply security checks only where relevant.
How does Claude Code prevent repetitive security warnings?
The security_reminder_hook.py implementation creates session-specific state files at ~/.claude/security_warnings_state_<session_id>.json to track which warnings have already been displayed. Before emitting a warning, the hook checks this state file and only blocks the operation if it's the first occurrence in that session, preventing alert fatigue while maintaining security.
Can I write security hooks in languages other than Python?
Yes, the Claude Code hook framework is language-agnostic. Any executable binary or script that can read JSON from stdin, perform logic, and exit with the appropriate status codes (0 or 2) will function correctly. The reference implementation uses Python, but Bash, Node.js, Go, or compiled binaries work equally well provided they follow the input/output contract.
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 →