DEV Community

Dan Jeong
Dan Jeong

Posted on

How to Stop Claude Code from Destroying Your System

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"
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)