Every week someone posts a screenshot of an agent deleting something it shouldn’t have, and the replies underneath say the same thing: the model is reckless and shouldn’t need babysitting.
I understand the feeling and I think it’s a fair one. These things have improved so fast that expecting them not to make silly mistakes has started to feel reasonable.
They still make them. But the distance between what a model might do and what it can actually reach on your machine is a config file most people have never opened. You need permissions to keep things running smooth, not faith.
Agentic CLIs like Claude Code, Codex and opencode all ship with well thought-out permission systems, and hardly anyone turns them on. That is usually the whole problem.
Here I will talk about Claude Code. Let’s get right into it.
1. Use auto mode
Press Shift+Tab and cycle through the permission modes until you land on auto mode. It runs a classifier over each command, rejecting the destructive ones, approving the safe ones, and surfacing the rest for you to decide.
This is the change that does the most for the least effort. It also fixes permission fatigue, which is the real reason people reach for --dangerously-skip-permissions and end up with no seatbelt at all. Prefer auto mode to that flag.
2. Turn on the sandbox
Auto mode decides whether a command runs. The sandbox decides what it can touch once it is running, and the operating system enforces that rather than a pattern match on the command string. Child processes are covered too, so a script that shells out stays inside the same walls.
On macOS it uses the built-in Seatbelt framework and there is nothing to install. Run /sandbox for the current project, or switch it on everywhere:
| |
Sandboxed commands can then write to your working directory and the session temp directory, nowhere else. filesystem.denyRead goes further and closes off your home directory, but pair it with allowRead or you’ll block your own projects too, since they also live under home. Put that pair in the project’s .claude/settings.json rather than the global file, because a . path resolves relative to the settings file it sits in, and from ~/.claude/settings.json it points at ~/.claude.
One catch worth knowing: if the sandbox can’t start, Claude Code warns you and runs the command anyway. Set failIfUnavailable to true if you would rather it stop. Linux and WSL2 need bubblewrap and socat installed first.
3. Deny the fatal commands outright
Auto mode and the sandbox both make judgement calls. For the short list of commands you never want judged at all, write them down. Deny rules are always respected, including inside the sandbox, and an rm aimed at a critical path still goes through the permission flow even when sandboxed commands are auto-approved. This goes in your global ~/.claude/settings.json:
| |
The diskutil entries erase disk storage and volumes. The rm -rf entries delete everything under a path, and the sudo variants do it as admin. Note the :* on the diskutil rules. It matches the command plus whatever arguments follow, which is what you want for anything that always takes a target.
4. Move your secrets out of your shell config
If your tokens live in .zshrc or .bash_profile, then every agent that reads your shell config reads your keys. Move them into their own file and source it at startup:
| |
Your shell behaves exactly as before. The difference is that there is now a single path to deny:
| |
A bare filename matches at any depth, so those last two cover every .env and .env.local in every project you open, without writing a rule per repo.
If you turned on the sandbox in step 2, there is a stronger version of this. The credentials block denies reads on the files you name and strips environment variables before each sandboxed command runs, so a token you exported hours ago can’t be read back out of the environment. It covers sandboxed Bash commands only, and needs Claude Code v2.1.187 or later:
| |
5. Block cat
A Read deny rule covers the Read tool but not a shell command that happens to print the same file. Models write multi-step scripts now, and a script that pipes a file through cat reaches content the deny rule was written to protect.
| |
Worth saying plainly: cat is the common case, not the only one. head, sed, less and a one-line Python script all read files too. Blocking cat raises the floor rather than sealing the room, and pairing it with step 3 is what makes it worth doing.
These rules all live in the same deny array, so merge them into one list instead of repeating the block three times.
6. Keep agents inside project folders
Run agents from inside the project they’re working on. An agent scoped to a repo has a much smaller blast radius than one started from your home directory, and most of the scary stories start with the second one.
The part permissions can’t do
Everything above is prevention, and prevention has a ceiling. Very few of the incidents people screenshot are catchable with rules. They are an agent deleting the wrong folder, running git checkout . over an afternoon of uncommitted work, or force-pushing a branch someone else was on. Every one of those is a normal command pointed at the wrong target, and no deny rule catches it because there is nothing wrong with the string.
What covers the gap isn’t a permission at all. Commit often and work on a branch, so a clobbered working tree costs you minutes. Learn git reflog before the day you need it. Keep a real backup running, because permission rules cover the repo and backups cover the rest of the disk.
Wrapping up
There is a lot more to configure if you want to go further. These six take about twenty minutes, and together with a commit habit they cover the failure modes that actually show up in those screenshots.
Models keep getting better and still have not reached anything like human judgement. That’s their biggest drawback, and it’s the reason the person at the keyboard decides what the agent is allowed to touch.
What’s in your deny list that isn’t in mine? I’d like to see it.
