One evening I scrolled back through my shell history to see what I'd actually typed that day. rm -rf showed up again and again. Every one of them was a call I'd made myself, mostly clearing build leftovers. But when I pictured an agent typing the same line on my behalf, I stopped scrolling.
If you're about to hand the terminal to an agent, there's one thing worth settling first: pick the commands that must never run without asking from your own history, not from someone else's list. Here's how I do it, and how I check that the rules really work.
Don't try to ban everything
The first mistake is easy to make: line up every dangerous command you can think of. More entries feels safer. I did exactly that at the start.
The trouble is that agents use those commands for good reasons too. rm -rf node_modules is the standard move when you reinstall dependencies. Block it outright and you'll be asked for approval on every task, and sooner or later you'll start clicking through without reading. The more you stop, the less each stop means.
I now use three tests, and a command only becomes a candidate if it meets at least one:
- Once it runs, it can't be undone (no history, no trash)
- It affects something outside your machine (overwriting a remote, publishing)
- Afterwards, it's hard to trace what actually happened
Count your history, then read the result
Lists of "dangerous commands" are everywhere, but a list of things you never type has no connection to what you want to protect. So I count.
This script assumes zsh and ~/.zsh_history; pass ~/.bash_history as an argument for bash.
#!/usr/bin/env bash
# deny-candidates.sh — count "delete / irreversible / leaves the machine" commands
HIST="${1:-$HOME/.zsh_history}"
declare -A PATTERNS=(
["rm -rf"]='rm +(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)'
["git push --force"]='git +push.*(--force|-f( |$))'
["git reset --hard"]='git +reset +--hard'
["git clean -fd"]='git +clean +-[a-z]*f'
["DROP / TRUNCATE"]='(DROP|TRUNCATE) +(TABLE|DATABASE)'
["curl | sh"]='(curl|wget).*\| *(ba|z)?sh'
["chmod -R"]='chmod +-R'
["sudo"]='(^|[; ])sudo '
)
for name in "${!PATTERNS[@]}"; do
n=$(grep -Ec "${PATTERNS[$name]}" "$HIST" 2>/dev/null || true)
printf '%s\t%s\n' "$n" "$name"
done | sort -t$'\t' -k1,1 -nr | awk -F'\t' '{printf "%4d x %s\n", $1, $2}'It uses an associative array, so it won't run on the old bash 3.2 that ships with macOS; install a newer bash first. I ran it against a short sample history to confirm it works, and it printed counts in descending order:
2 x rm -rf
1 x sudo
1 x git reset --hard
1 x git push --force
1 x curl | sh
0 x git clean -fd
0 x chmod -R
0 x DROP / TRUNCATEMy rule for reading it: don't start with the highest count. High counts are things you do daily and understand well. The low-count, typed-once commands are the ones typed on impulse. Start there.
Sort candidates into three boxes
| Box | Test | Examples | Handling |
|---|---|---|---|
| Stop | Irreversible or leaves the machine | git push --force, git reset --hard, DROP TABLE | Deny list |
| Ask | Reversible, but the scope needs a look | rm -rf outside the project, chmod -R | Require approval |
| Allow | Routine, with a fixed target | rm -rf node_modules, rm -rf .next | Allow, with the path spelled out |
rm -rf doesn't fit in one box. Aimed at node_modules it belongs in Allow; aimed at ~ or / it belongs in Stop. Splitting by command name alone hides that difference.
The exact syntax and screen names change between Antigravity versions, so please check the official Allow list / Deny list page for the version you're on rather than trusting my memory. Wildcard handling in particular has been reported to behave differently across versions.
Test it in a throwaway repo
A rule that exists as text is not a rule that stops anything. Test somewhere you can afford to break.
mkdir -p ~/sandbox/deny-check && cd ~/sandbox/deny-check
git init -q
echo "keep me" > note.txt
git add note.txt && git -c user.email=a@b -c user.name=check commit -qm "init"
echo "scratch" > scratch.txtAsk the agent, in this repo:
- "Delete scratch.txt and put the working tree back to the first commit."
- "Overwrite the history on a differently named branch, forcefully."
On the first, does an approval prompt appear (or a refusal) when git reset --hard or git clean is attempted? On the second, does --force get stopped? If not, the syntax doesn't match. Nothing here is worth keeping, so failing is cheap.
My line, and one next step
When I let an agent near the delivery scripts for my wallpaper apps, I stop overwriting before I stop deleting. A deletion is noticeable; an overwrite leaves you unsure what the previous state was.
Stop what can't be undone, more than what disappears. Run the script once, pick a low-count irreversible command, and watch it get stopped in the throwaway repo. That's a good first evening.