Claude Code can run any command on your machine. That includes rm -rf /, DROP TABLE users, and chmod 777 ~/.ssh. If you've used it for more than a week, you've probably had a close call.
The fix is hooks. Claude Code supports lifecycle hooks that run before and after every tool execution. You can use them to block dangerous commands before they touch your filesystem.
Here's how to build a security layer in under 30 minutes.
How Claude Code Hooks Work
Hooks are scripts that Claude Code runs at specific lifecycle events:
- PreToolUse: Runs before a tool executes. Can block the operation (exit code 2).
- PostToolUse: Runs after a tool completes. Good for logging.
- SessionStart: Runs when Claude starts a session.
- SessionEnd: Runs when a session ends.
The key one for security is PreToolUse. When your hook exits with code 2, Claude Code blocks the command and shows your error message instead.
Layer 1: Pattern Matching
The simplest protection is regex matching against known dangerous commands. Create a YAML file with patterns:
# patterns.yaml
critical:
- pattern: "rm\\s+(-[rfRF]+\\s+)?/"
description: "Recursive delete from root"
- pattern: "DROP\\s+(TABLE|DATABASE)"
description: "SQL destructive operation"
- pattern: "chmod\\s+777"
description: "World-writable permissions"
- pattern: "curl.*\\|\\s*(bash|sh|zsh)"
description: "Pipe to shell execution"
- pattern: "git\\s+push.*--force\\s+.*(main|master)"
description: "Force push to main branch"
warning:
- pattern: "npm\\s+publish"
description: "Publishing to npm registry"
- pattern: "docker\\s+system\\s+prune"
description: "Docker full cleanup"
Then in your PreToolUse hook (TypeScript with Bun):
import { readFileSync } from "fs";
import { parse } from "yaml";
const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));
// Only check Bash tool calls
if (input.tool_name !== "Bash") process.exit(0);
const command = input.tool_input?.command || "";
const patterns = parse(readFileSync("patterns.yaml", "utf-8"));
for (const p of patterns.critical) {
if (new RegExp(p.pattern, "i").test(command)) {
// Exit code 2 = block the command
console.error(`[BLOCKED] ${p.description}`);
process.exit(2);
}
}
for (const p of patterns.warning) {
if (new RegExp(p.pattern, "i").test(command)) {
console.error(`[WARNING] ${p.description}`);
// Exit 0 = allow but warn
}
}
process.exit(0);
Layer 2: Path Guards
Pattern matching catches known commands, but what about cat ~/.ssh/id_rsa > /tmp/exfil? You need path-based protection.
Define protection tiers:
# paths.yaml
zero_access:
- "~/.ssh"
- "~/.aws/credentials"
- "~/.env"
- "~/.gnupg"
read_only:
- "/etc/passwd"
- "~/.bashrc"
- "~/.zshrc"
no_delete:
- "~/.git"
- "package.json"
- "tsconfig.json"
Then check tool inputs against these paths. Any Write or Edit targeting a zero-access path gets blocked. Any destructive operation on a no-delete path gets blocked.
Layer 3: Heuristic Detection
Patterns and paths catch known threats. Heuristics catch novel ones:
function heuristicCheck(command: string): string | null {
// Pipe chains with network tools
if (/\|.*\|.*\|/.test(command) &&
/(curl|wget|nc|netcat)/.test(command)) {
return "Complex pipe chain with network access";
}
// Loops with delete operations
if (/(for|while).*do.*rm/.test(command)) {
return "Loop containing delete operations";
}
// Eval with variable injection
if (/eval.*\$/.test(command)) {
return "Eval with variable expansion";
}
return null;
}
Wiring It Up
Add the hook to your Claude Code settings (~/.claude/settings.json):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bun ~/.claude/hooks/damage-control.ts"
}
]
}
]
}
}
Now every Bash command runs through your security layer before execution.
Testing It
# This should be BLOCKED:
claude -p "rm -rf /" --allowedTools Bash
# You should see:
# [BLOCKED] Recursive delete from root
What This Doesn't Cover
This approach has limitations:
- Multi-step attacks: Claude could write a script, then execute it. The hook only sees the execution command, not the script contents.
- Indirect access: If Claude writes to a file that another process reads, the hook won't catch it.
- New tools: Claude Code may add new tools beyond Bash. Your hook needs to cover them.
For production use, you want a more comprehensive solution that handles all three layers with proper YAML configuration, covers all tool types, and includes heuristic detection for novel patterns.
Get the Full Implementation
I built a complete security bundle that handles all of this:
Damage Control Bundle ($99): Production-ready security hooks with 30+ patterns, path guards for SSH/credentials/.env, and heuristic detection. Drop into
~/.claude/and you're protected.Claude Code Hooks Starter Kit ($49): Templates for all 4 hook types (PreToolUse, PostToolUse, SessionStart, SessionEnd). Good starting point if you want to build your own.
Both include TypeScript source, YAML configs, settings.json, and documentation.
The Bigger Picture
AI coding assistants are getting more autonomous. Claude Code can already run commands, edit files, and manage git. The capability gap between "helpful tool" and "dangerous tool" is one bad prompt away.
Security hooks are the minimum viable safety layer. Build them before you need them.
Top comments (0)