# Give Claude Code real autonomy without letting it wreck your system

> Source: <https://dev.to/hbouyap/give-claude-code-real-autonomy-without-letting-it-wreck-your-system-1f3e>
> Published: 2026-09-03 03:31:29+00:00

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.*
