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:

  1. Claude Code receives a tool request (e.g., Edit on a TypeScript file).
  2. The PreToolUse entry in hooks.json matches the tool name against the pattern Edit|Write|MultiEdit.
  3. Claude Code launches the command defined in the hook configuration: python3 ${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py.
  4. The hook script reads the JSON payload from stdin, containing session_id, tool_name, and tool_input.
  5. The script extracts the file path and content fields (which vary by tool type).
  6. The check_patterns() function iterates over SECURITY_PATTERNS, executing path-check lambdas or searching for forbidden substrings in the content.
  7. If a pattern matches, the hook checks a per-session state file at ~/.claude/security_warnings_state_<session_id>.json to determine if this warning was already shown.
  8. On first encounter, the script prints the reminder text to stderr and exits with code 2, signaling Claude Code to abort the tool.
  9. If no patterns match, the script exits with code 0 and 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 PreToolUse event before files are modified.
  • Hooks receive JSON input on stdin containing session_id, tool_name, and tool_input, and must exit with code 0 (allow) or 2 (abort).
  • The three required files are: the hook script (logic), hooks.json (registration), and plugin.json (manifest).
  • The reference implementation at plugins/security-guidance/hooks/security_reminder_hook.py demonstrates 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:

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 →