Give Claude Code real autonomy without letting it wreck your system Developer Harry Philippe Mbouyap has released an open-source project that provides a three-layer safety system for giving Claude Code real autonomy without risking system damage. The approach combines permission settings, a pre-tool hook that blocks dangerous commands like recursive force deletes and force pushes, and a fail-closed mode for unattended runs. The code is available on GitHub under an MIT license. Author: Harry Philippe Mbouyap. All the code below is MIT and lives in a small repo you can clone and run: https://github.com/hbouyap/claude-code-safe-automation Getting an AI agent to write code is a solved problem. The part that keeps teams up at night is different: do you trust it to run commands on your machine, unattended? One rm -rf in the wrong directory, one git push --force to the wrong branch, and "the agent saved me an afternoon" becomes "the agent cost me a week." So most people keep the agent on a tight leash — approving every step — which throws away most of the value. There's a better middle ground. You can give an agent real autonomy on a workflow and still make it structurally unable to do the few things that would hurt. Here's the three-layer approach I use with Claude Code. Start by telling the agent what it's allowed to touch, in .claude/settings.json : { "permissions": { "allow": "Bash ls: ", "Bash git status: ", "Bash git diff: ", "Read ", "Grep " , "deny": "Bash sudo: ", "Read .env ", "Read /secrets/ " } } This is necessary but not sufficient. Permission globs are coarse — Bash git: allows both git status and git push --force . For the commands that actually matter, you want logic, not a glob. That's the next layer. Claude Code can run a hook before every tool call. If the hook exits with code 2 , the call is denied and the reason is handed back to the model. That's the perfect place to put a deny-list of things that should never run unattended: python import json, re, sys DANGEROUS = rm -rf in any flag order, plus the PowerShell alias rm -Recurse -Force r"\brm\b ?=. ?:- a-z r|--recursive ?=. ?:- a-z f|--force ", "recursive force delete" , r"\bremove-item\b ?=. -rec ?=. -for ", "recursive force delete Windows " , r"\bgit\s+push\b ?=. ?:--force\b|\s-f\b ", "force push can overwrite history" , r"\bgit\s+reset\s+--hard\b", "hard reset discards work" , r"\b ?:curl|wget \b. \|\s ?:sudo\s+ ? ?:sh|bash \b", "pipe-to-shell from the network" , ... fork bombs, raw disk writes, sudo, format, shred, del /s, rmdir /s def main : event = json.load sys.stdin if event.get "tool name" = "Bash": return 0 command = event.get "tool input", {} .get "command", "" for pattern, reason in DANGEROUS: if re.search pattern, command, re.IGNORECASE : print f"guard: blocked -- {reason}", file=sys.stderr return 2 Claude Code treats exit 2 as "deny" return 0 sys.exit main Two details that matter more than they look: rm -rf . If your team runs Claude Code on Windows, you also need Remove-Item -Recurse -Force , del /s , rmdir /s , format , and friends — otherwise the guardrail is theater on half your machines. rm -rf , rm -R -f , rm --recursive --force and rm -Rf are the same danger. The two-lookahead regex above catches "has a recursive flag AND has a force flag" regardless of arrangement, while still letting a plain rm file.txt through.Wire it up in settings.json : { "hooks": { "PreToolUse": { "matcher": "Bash", "hooks": { "type": "command", "command": "python .claude/hooks/guard.py" } } } } Now test it — this is the part people skip: echo '{"tool name":"Bash","tool input":{"command":"rm -rf /"}}' | python .claude/hooks/guard.py - exit 2, blocked echo '{"tool name":"Bash","tool input":{"command":"git status"}}' | python .claude/hooks/guard.py - exit 0, allowed A deny-list is friendly for interactive work on your own machine: block the known-dangerous, allow everything else. For unattended runs or client work , flip it to fail-closed : only commands you explicitly allow run, everything else is refused. Log every decision either way, so you have an audit trail of what the agent tried. Shell isn't the only way an agent acts on your world. The moment you give it an MCP server that talks to your API or database, the same discipline applies. The rule that keeps you safe: Reads are free. Writes are gated. Give the agent all the read access it needs, but make every state change require a second deliberate signal — or refuse it outright. A read-only SQLite server, for example: php @mcp.tool def query sql: str - str: """Run a SELECT query. Non-SELECT statements are refused.""" if not sql.lstrip .lower .startswith "select" : return "refused: only SELECT statements are allowed read-only guardrail ." ... open the DB with a read-only connection and run it And for anything that mutates state, require an explicit confirm=true argument so the agent can't change things by accident: php @mcp.tool def update resource path: str, body: str, confirm: bool = False - str: if not confirm: return "refused: pass confirm=true to perform this write guardrail ." ... perform the write Keep credentials in environment variables, never in code or tool arguments — that way they can't leak into a transcript or a log. Three layers, each doing one job: With those in place, you can point Claude Code at a real workflow — write, test, deploy, verify — and let it run, because the handful of actions that could actually hurt are structurally blocked. Everything above is in a small MIT repo you can clone and run in a minute: https://github.com/hbouyap/claude-code-safe-automation — a working guardrail, scoped If you'd rather skip the assembly, I also package a fuller MCP & Guardrails Kit fail-closed allow-list mode, three MCP server templates, a subagent library, and a one-command installer — and I build these setups for teams directly. Links are on my profile. What's your approach to giving agents autonomy safely? I'd like to hear it.