DEV Community

Bill Wilson
Bill Wilson

Posted on

Build Your Own Claude Code Kill Switch in 50 Lines

Yesterday I published a safety guide for Claude Code Auto Mode. Several people asked for the actual implementation - not just the concepts, but working code they could drop into a project today.

Here it is. A kill switch that monitors Claude Code sessions for destructive operations and stops them before they execute.

The Problem

Claude Code Auto Mode executes commands without asking. Most of those commands are fine - creating files, running tests, editing code. But some commands can destroy production infrastructure in seconds: terraform destroy, rm -rf, DROP TABLE, aws rds delete-db-instance.

You need a layer between Claude Code and your shell that catches these before they run.

The Implementation

This is a bash wrapper that intercepts commands and checks them against a blocklist before execution:

#!/bin/bash
# claude-guard.sh - Kill switch for Claude Code Auto Mode
# Drop this in your project root and use it as your shell wrapper

set -euo pipefail

KILL_FILE="${CLAUDE_GUARD_KILL_FILE:-.claude-stop}"
LOG_FILE="${CLAUDE_GUARD_LOG:-/tmp/claude-guard.log}"
BLOCKED_LOG="${CLAUDE_GUARD_BLOCKED:-/tmp/claude-blocked.log}"

# Destructive patterns - add your own
DESTRUCTIVE_PATTERNS=(
    'terraform destroy'
    'terraform apply -destroy'
    'rm -rf /'
    'rm -rf /*'
    'DROP TABLE'
    'DROP DATABASE'
    'DELETE FROM .* WHERE 1'
    'aws rds delete-db'
    'aws s3 rb'
    'aws ec2 terminate'
    'kubectl delete namespace'
    'kubectl delete -f .* --all'
    'docker system prune -a'
    'git push.*--force.*main'
    'git push.*--force.*master'
    'truncate table'
)

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}

block() {
    local cmd="$1"
    local pattern="$2"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] BLOCKED: '$cmd' matched pattern: '$pattern'" >> "$BLOCKED_LOG"
    echo "BLOCKED: Destructive operation detected."
    echo "Pattern matched: $pattern"
    echo "Command: $cmd"
    echo "To override, remove the matching pattern from claude-guard.sh"
    exit 1
}

# Check kill switch
if [ -f "$KILL_FILE" ]; then
    echo "KILL SWITCH ACTIVE - All Claude Code operations halted"
    echo "Remove $KILL_FILE to resume"
    exit 1
fi

# Get the command to execute
CMD="$*"

if [ -z "$CMD" ]; then
    echo "Usage: claude-guard.sh <command>"
    exit 1
fi

# Check against destructive patterns
for pattern in "${DESTRUCTIVE_PATTERNS[@]}"; do
    if echo "$CMD" | grep -iqE "$pattern"; then
        block "$CMD" "$pattern"
    fi
fi

# Command passed - log and execute
log "ALLOWED: $CMD"
exec $CMD
Enter fullscreen mode Exit fullscreen mode

How to Use It

1. Save the script:

curl -o claude-guard.sh https://gist.githubusercontent.com/.../claude-guard.sh
chmod +x claude-guard.sh
Enter fullscreen mode Exit fullscreen mode

2. Use it as a command prefix:

./claude-guard.sh terraform apply
# Works fine - 'apply' isn't blocked

./claude-guard.sh terraform destroy
# BLOCKED: Destructive operation detected.
# Pattern matched: terraform destroy
Enter fullscreen mode Exit fullscreen mode

3. Emergency stop - create the kill file:

touch .claude-stop
# Now ALL commands through claude-guard.sh are blocked

rm .claude-stop
# Resume normal operations
Enter fullscreen mode Exit fullscreen mode

Python Version With Slack Alerts

If you want notifications when something gets blocked:

#!/usr/bin/env python3
"""claude_guard.py - Kill switch with Slack alerting."""

import sys
import os
import re
import json
import subprocess
from datetime import datetime
from pathlib import Path

try:
    from urllib.request import Request, urlopen
except ImportError:
    urlopen = None

KILL_FILE = os.environ.get('CLAUDE_GUARD_KILL_FILE', '.claude-stop')
SLACK_WEBHOOK = os.environ.get('CLAUDE_GUARD_SLACK_WEBHOOK', '')

DESTRUCTIVE_PATTERNS = [
    r'terraform\s+destroy',
    r'terraform\s+apply\s+-destroy',
    r'rm\s+-rf\s+/',
    r'DROP\s+(TABLE|DATABASE)',
    r'DELETE\s+FROM\s+.*WHERE\s+1',
    r'aws\s+rds\s+delete-db',
    r'aws\s+s3\s+rb',
    r'aws\s+ec2\s+terminate',
    r'kubectl\s+delete\s+(namespace|ns)',
    r'git\s+push.*--force.*(main|master)',
]

def alert_slack(cmd: str, pattern: str):
    if not SLACK_WEBHOOK or not urlopen:
        return
    payload = json.dumps({
        'text': f':rotating_light: Claude Guard BLOCKED a destructive command:\n'
                f'```
{% endraw %}
{cmd}
{% raw %}
```\nPattern: `{pattern}`\n'
                f'Time: {datetime.now().isoformat()}\n'
                f'Host: {os.uname().nodename}'
    }).encode()
    req = Request(SLACK_WEBHOOK, data=payload,
                  headers={'Content-Type': 'application/json'})
    try:
        urlopen(req, timeout=5)
    except Exception:
        pass  # Don't let alerting failure block the guard

def main():
    if Path(KILL_FILE).exists():
        print('KILL SWITCH ACTIVE - All operations halted')
        sys.exit(1)

    if len(sys.argv) < 2:
        print('Usage: claude_guard.py <command>')
        sys.exit(1)

    cmd = ' '.join(sys.argv[1:])

    for pattern in DESTRUCTIVE_PATTERNS:
        if re.search(pattern, cmd, re.IGNORECASE):
            print(f'BLOCKED: Destructive operation detected.')
            print(f'Pattern: {pattern}')
            print(f'Command: {cmd}')
            alert_slack(cmd, pattern)
            sys.exit(1)

    # Safe to execute
    result = subprocess.run(sys.argv[1:], capture_output=False)
    sys.exit(result.returncode)

if __name__ == '__main__':
    main()
Enter fullscreen mode Exit fullscreen mode

Set CLAUDE_GUARD_SLACK_WEBHOOK to your Slack incoming webhook URL and you'll get an alert any time a destructive command is caught.

Customizing the Patterns

The default patterns cover the obvious cases. You'll want to add patterns specific to your stack:

  • Using Prisma? Add prisma migrate reset
  • Using Docker Compose in production? Add docker-compose down -v
  • Running Kubernetes? Add helm uninstall for critical releases
  • Using Fly.io? Add fly apps destroy

The patterns are regular expressions. Be as specific as you need - rm -rf /var/data is more targeted than rm -rf (which would also block rm -rf node_modules, which is usually fine).

Limitations

This catches direct command invocations. It won't catch:

  • Claude Code writing a Python script that calls os.system('terraform destroy') and then running that script
  • Destructive operations embedded in Makefiles or shell scripts that Claude Code invokes
  • API calls made directly through curl or SDK code

For those cases, the AWS permission boundary approach from the safety guide is your backstop. The guard catches the easy cases; IAM boundaries catch everything else.

No single layer is enough. Use both.

This article was written with AI assistance. All technical claims, code, and architectural decisions were validated by the author.

Top comments (0)