Securing Claude Code Hooks and Preventing Dangerous Commands: A Complete Guide
Claude Code hooks provide an event-driven framework to intercept and block destructive shell commands, malicious user prompts, and leaked secrets by returning exit code 2 from validation scripts.
The luongnv89/claude-howto repository demonstrates how to implement a defense-in-depth security layer for Claude Code using its 25 built-in hook events. By configuring validation scripts in ~/.claude/settings.json or project-level .claude/settings.json, you can enforce security policies that prevent dangerous operations before they execute. This guide covers the architectural patterns and concrete implementations found in the 06-hooks/ directory.
How Claude Code Hooks Work
Claude Code's hook system operates as a middleware layer that inspects tool invocations and user interactions through JSON payloads passed via stdin.
Event-Driven Security Architecture
The system fires hooks at deterministic execution points:
- PreToolUse: Executes before any tool runs, allowing you to block dangerous Bash commands or file operations
- UserPromptSubmit: Intercepts user prompts before processing, enabling content filtering
- PostToolUse: Runs after tool completion to scan outputs or generated files for secrets
- Stop: Captures session termination for audit logging
According to the 06-hooks/README.md, hooks receive a JSON payload on stdin and must exit with a numeric code where 0 allows the operation and 2 blocks it. Optional JSON on stdout can modify the session context or provide additional warnings.
Configuration Hierarchy and Matcher Syntax
Settings merge from multiple sources in priority order:
- Global:
~/.claude/settings.json - Project:
.claude/settings.json - Local:
.claude/settings.local.json - Managed policies and plugin-scoped
hooks/hooks.json
Each hook entry uses a matcher to determine applicability: exact strings, regex patterns, wildcards (*), or MCP-specific patterns. As documented in the repository, all matching hooks for a given event execute in parallel, with identical commands automatically deduplicated.
Blocking Dangerous Commands with PreToolUse Hooks
The PreToolUse event is your primary defense against destructive shell operations. The reference implementation in 06-hooks/validate-bash.py demonstrates pattern-based blocking:
#!/usr/bin/env python3
# File: 06-hooks/validate-bash.py
import json, sys, re
BLOCKED = [
(r"\brm\s+-rf\s+/", "Blocking dangerous rm -rf /"),
(r"\bsudo\s+rm", "Blocking sudo rm command"),
]
data = json.load(sys.stdin)
if data.get("tool_name") != "Bash":
sys.exit(0)
cmd = data.get("tool_input", {}).get("command", "")
for pat, msg in BLOCKED:
if re.search(pat, cmd):
print(msg, file=sys.stderr)
sys.exit(2) # 2 = blocking error
sys.exit(0)
Configuration requires adding the hook to your settings file:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\""
}
]
}
]
}
}
This pattern ensures that commands matching rm -rf / or sudo rm never reach the shell, as the exit code 2 signals Claude to halt execution and surface the error message.
Validating User Prompts Before Submission
Malicious or accidental instructions often enter through the prompt itself. The UserPromptSubmit hook acts as a content filter before Claude processes the request.
The 06-hooks/validate-prompt.py implementation inspects the user_prompt field for dangerous patterns:
#!/usr/bin/env python3
# File: 06-hooks/validate-prompt.py
import json, sys, re
BLOCKED = [
(r"delete\s+(all\s+)?database", "Dangerous: database deletion"),
(r"rm\s+-rf\s+/", "Dangerous: root deletion"),
]
data = json.load(sys.stdin)
prompt = data.get("user_prompt") or data.get("prompt", "")
for pat, msg in BLOCKED:
if re.search(pat, prompt, re.IGNORECASE):
out = {"decision": "block", "reason": f"Blocked: {msg}"}
print(json.dumps(out))
sys.exit(0)
sys.exit(0)
Register this validator in your configuration:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.py\""
}
]
}
]
}
}
Unlike PreToolUse, prompt validation returns a JSON object indicating the block decision, providing explicit reasoning that appears in the Claude interface.
Scanning Generated Code for Secrets
Post-execution validation prevents credential leaks in generated files. The PostToolUse hook with matchers for Write and Edit tools scans content before it reaches the filesystem.
The 06-hooks/security-scan.py example targets hardcoded credentials:
#!/usr/bin/env python3
# File: 06-hooks/security-scan.py
import json, sys, re
SECRETS = [
(r"password\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded password"),
(r"api[_-]?key\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded API key"),
]
data = json.load(sys.stdin)
if data.get("tool_name") not in ["Write", "Edit"]:
sys.exit(0)
content = data["tool_input"].get("content") or data["tool_input"].get("new_string", "")
path = data["tool_input"].get("file_path", "")
warnings = [msg for pat, msg in SECRETS if re.search(pat, content, re.I)]
if warnings:
out = {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": f"Security warnings for {path}: " + "; ".join(warnings)
}
}
print(json.dumps(out))
sys.exit(0)
Configure this scanner to run after file modifications:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.py\""
}
]
}
]
}
}
This approach catches secrets immediately after generation without interfering with the write operation itself, surfacing warnings through the additionalContext field.
Auto-Adapting Permission Rules Safely
The 06-hooks/auto-adapt-mode.py demonstrates machine learning-style permission adaptation that generalizes safe patterns while maintaining strict deny-lists for dangerous operations.
Key security features of this implementation:
- Baseline safe rules: Seeds permissions with read-only operations like
Read(*),Bash(git status:*), andBash(ls:*) - Dangerous pattern exclusion: Explicitly blocks
rm -rf,git push --force,sudo, SQLDROPstatements, and pipe-to-shell curl commands - Command generalization: Transforms specific approved commands into wildcard patterns (e.g.,
Bash(git status)becomesBash(git status:*))
# Excerpt from 06-hooks/auto-adapt-mode.py
DANGEROUS = [
r"rm\s+(-[a-zA-Z]*r[a-zA-Z]*|--recursive)",
r"git\s+push\s+(-[a-zA-Z]*f|--force)",
r"sudo\b",
r"DROP\s+(TABLE|DATABASE)",
r"curl\s+.*\|\s*(bash|sh)",
]
The script modifies ~/.claude/settings.json to append safe rules automatically, but only after checking against the DANGEROUS regex list. This prevents the system from learning destructive behaviors even when they appear in successful tool executions.
Implementation and Best Practices
Directory Structure and Setup
Organize hooks in a version-controlled directory structure:
mkdir -p ~/.claude/hooks
cp 06-hooks/*.py ~/.claude/hooks/
chmod +x ~/.claude/hooks/*.py
Reference implementations in the repository include format-code.sh for automatic linting and context-tracker.py for token usage monitoring, illustrating how to compose multiple validators for the same event.
Security-First Configuration Patterns
Follow these guidelines from the 06-hooks/README.md to prevent privilege escalation:
- Quote shell variables: Always use
"$CLAUDE_PROJECT_DIR"instead of unquoted$CLAUDE_PROJECT_DIRto prevent word-splitting attacks - Whitelist environment variables: For HTTP hooks, explicitly define
allowedEnvVarsto prevent credential leakage via environment interpolation - Validate before trusting: Apply regex validation to all
tool_inputfields before allowing execution - Test in isolation: Pipe sample JSON to your hook script directly (
echo '{}' | python3 hook.py) before deploying
The repository includes a .pre-commit-config.yaml that runs security-scan.py and format-code.sh before Git commits, ensuring hooks themselves pass security checks.
Summary
- Hook architecture: Claude Code provides 25 events including
PreToolUse,UserPromptSubmit, andPostToolUsethat receive JSON via stdin and use exit code 2 to block operations - Command validation: The
validate-bash.pyexample in06-hooks/validate-bash.pyblocks destructive patterns likerm -rf /before execution - Prompt filtering:
validate-prompt.pyintercepts dangerous user instructions using regex matching against theuser_promptfield - Secret detection:
security-scan.pyscans written content for hardcoded passwords and API keys usingPostToolUsehooks - Safe automation:
auto-adapt-mode.pylearns permission rules from approved executions while maintaining explicit deny-lists for dangerous commands - Configuration: Store hooks in
.claude/hooks/and reference them in~/.claude/settings.jsonor project-level.claude/settings.jsonusing the$CLAUDE_PROJECT_DIRenvironment variable
Frequently Asked Questions
How do Claude Code hooks prevent arbitrary code execution?
Hooks act as a mandatory access control layer that inspects tool invocations before the Claude process executes them. By validating the command field in PreToolUse events and exiting with code 2 when dangerous patterns like sudo or rm -rf are detected, the system blocks the operation entirely and surfaces an error to the user without reaching the shell.
What is the difference between PreToolUse and PostToolUse hooks?
PreToolUse fires before a tool executes and can prevent the operation by returning exit code 2, making it ideal for blocking dangerous commands. PostToolUse fires after successful completion and cannot block the operation (it always uses exit code 0), but can add warnings or audit entries through the additionalContext field in the JSON stdout response, making it suitable for secret scanning and logging.
Can hooks access environment variables and network resources?
Command hooks run inside Claude's sandboxed environment with access to safe variables like $CLAUDE_PROJECT_DIR. HTTP hooks can make network calls, but require explicit allowedEnvVars configuration to access environment variables, preventing accidental credential leakage. The repository's examples use Python scripts that read from stdin and write to stdout without requiring network access.
Where should I store hook scripts for team-wide use?
Store hook scripts in a version-controlled .claude/hooks/ directory within your project repository, then reference them using "$CLAUDE_PROJECT_DIR/.claude/hooks/script.py" in your .claude/settings.json. For personal protection across all projects, place scripts in ~/.claude/hooks/ and reference them in ~/.claude/settings.json. The luongnv89/claude-howto repository provides reference implementations that can be copied directly into these locations.
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 →