●AUTO — From August 14, auto mode becomes the default in Claude Code for Pro, Max, and Team. It only stops for actions judged irreversible, destructive, or aimed outside your environment●SAFETY — In a study with 1,053 paid testers, auto mode caught 89% of harmful actions while human review caught 13.6%●HABIT — Manual review becomes habitual, Anthropic notes: users approve 97% of the permission prompts Claude Code shows them●GUARD — Prompt injection screening and customizable hard deny rules have been added to keep things like data exfiltration off the table●VOICE — The head of Claude Code says he and his team have used auto mode exclusively for months and cannot imagine going back to permission prompts●VERSION — The latest release is v2.1.226 from August 8, bug fixes and reliability only. Workbench retires August 17, and Sonnet 5 promo pricing runs through August 31●AUTO — From August 14, auto mode becomes the default in Claude Code for Pro, Max, and Team. It only stops for actions judged irreversible, destructive, or aimed outside your environment●SAFETY — In a study with 1,053 paid testers, auto mode caught 89% of harmful actions while human review caught 13.6%●HABIT — Manual review becomes habitual, Anthropic notes: users approve 97% of the permission prompts Claude Code shows them●GUARD — Prompt injection screening and customizable hard deny rules have been added to keep things like data exfiltration off the table●VOICE — The head of Claude Code says he and his team have used auto mode exclusively for months and cannot imagine going back to permission prompts●VERSION — The latest release is v2.1.226 from August 8, bug fixes and reliability only. Workbench retires August 17, and Sonnet 5 promo pricing runs through August 31
The Same rm -rf Was Recoverable in Ten Places and Unrecoverable in Five — Measuring Reversibility Before Auto Mode Becomes the Default
Auto mode becomes the default on Pro, Max and Team from August 14. It stops on operations judged irreversible, destructive, or outward-facing — but reversibility turned out to be a property of state, not of commands. Here is the probe and the measurements.
I put August 14 in my calendar and then spent half a day finding out what actually changes on my machine.
Claude Code's auto mode becomes the default for Pro, Max and Team. Instead of asking for approval at each step, it stops only on operations judged irreversible, destructive, or directed outside your own environment.
As a sentence, that leaves no room for confusion.
As soon as I tried to map it onto my own setup, though, I stalled. I run overnight jobs as an indie developer — cleaning up build artifacts, regenerating screenshots, reorganizing output directories — and deletions and overwrites are mixed in throughout. I could not say which of those were the irreversible ones.
So I measured, using a whole repository as the test bed. What came out of it was that my starting assumption — that I needed a list of dangerous commands — was the thing that was wrong.
Trying to define "irreversible" by command name
I started the way most people do: scrolling back through shell history and writing down anything that looked risky.
rm -rf, git push --force, git clean -fdx, npm publish, POST to an external API.
The list stopped being useful almost immediately. The same rm -rf behaves completely differently depending on whether the directory is tracked by git or listed in .gitignore. The first comes back in a few hundred milliseconds. The second never comes back at all.
Reversibility, in other words, is not an attribute of the command. It is an attribute of the state of the files that command happens to be pointed at, at that moment.
I was trying to express something state-shaped using a list of command names. That was the first hour of the half day.
If it is a property of state, then the answer is to measure the state before executing.
What matters is the number of bytes under the target path that git cannot bring back. Tracked and clean files come back. Untracked and ignored files do not.
Here is the probe I actually used. Standard library only.
#!/usr/bin/env python3"""Classify files under a path by whether git can restore them, and total the unrecoverable bytes."""import os, subprocess, sys, collectionsdef git(*args): r = subprocess.run(["git", *args], capture_output=True, text=True) return [p for p in r.stdout.split("\0") if p]def classify(root="."): tracked = set(git("ls-files", "-z")) ignored = set(git("ls-files", "-z", "--others", "--ignored", "--exclude-standard")) untracked = set(git("ls-files", "-z", "--others", "--exclude-standard")) dirty = set(git("diff", "-z", "--name-only")) staged = set(git("diff", "-z", "--name-only", "--cached")) buckets = collections.defaultdict(lambda: [0, 0]) # [files, bytes] for dirpath, dirnames, filenames in os.walk(root): if ".git" in dirpath.split(os.sep): continue for name in filenames: path = os.path.relpath(os.path.join(dirpath, name), root) if path in ignored: kind = "ignored" # unrecoverable elif path in untracked: kind = "untracked" # unrecoverable elif path in staged: kind = "tracked-staged" # index only elif path in dirty: kind = "tracked-modified" # edits are lost elif path in tracked: kind = "tracked-clean" # fully restorable else: kind = "unknown" try: size = os.path.getsize(os.path.join(root, path)) except OSError: continue buckets[kind][0] += 1 buckets[kind][1] += size return bucketsRECOVERABLE = {"tracked-clean"}if __name__ == "__main__": b = classify(sys.argv[1] if len(sys.argv) > 1 else ".") lost_f = lost_b = tot_f = tot_b = 0 for kind, (n, size) in sorted(b.items()): tot_f += n; tot_b += size if kind not in RECOVERABLE: lost_f += n; lost_b += size print(f"{kind:18} files={n:6d} bytes={size:12,d}") print("-" * 46) print(f"unrecoverable files={lost_f}/{tot_f} ({lost_f/tot_f*100:.1f}%) " f"bytes={lost_b:,}/{tot_b:,} ({lost_b/tot_b*100:.1f}%)")
The six buckets exist for a reason. Only tracked-clean comes back unconditionally. tracked-modified is the in-between case — the file returns, but today's uncommitted edits do not — and counting it as recoverable quietly corrupts the judgment. I deliberately left RECOVERABLE with a single member.
All numbers below were produced by running this in a sandbox: Linux 6.8.0, Python 3.10.12, git 2.34.1, 4 vCPUs, 3.9 GB RAM.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦If you have been unsure what to put in your deny rules before auto mode flips on, you can now count the unrecoverable bytes in your own repository first and draw the line from data
✦You will see the same rm -rf come out reversible in 10 targets and irreversible in 5, and move from a list of scary command names to a state-based pre-flight check
✦You will understand why recovering a force push depends on who still holds the SHA, and set up a recovery path that does not rely on the remote
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Measurement 1 — the same rm -rf, reversible in 10 targets and irreversible in 5
The test bed was a real Next.js repository holding 1,678 articles (1,790 tracked files, 28.9 MB). To it I added the kinds of things any working checkout accumulates: 400 files under node_modules/, 120 under .next/cache/, 40 under build/artifacts/, 25 scratch notes, and a .env.
Probe output:
Bucket
Files
Bytes
Comes back?
tracked-clean
1,789
28,980,257
Yes
tracked-modified
1
620
File only
ignored
561
9,830,453
No
untracked
25
166
No
Out of 2,376 files and 38,811,496 bytes, 587 files (24.7%) and 9,831,239 bytes (25.3%) were unrecoverable.
Run the same probe on a fresh clone with nothing added and the figure is 0.0%. The file contents are identical. The only difference is whether anyone has been working in the directory for a while.
Then I pointed the same judgment at 15 different directories.
Target
Verdict
Unrecoverable files
Unrecoverable bytes
content/
Reversible
0
0
content/articles/
Reversible
0
0
src/
Reversible
0
0
src/components/
Reversible
0
0
public/
Reversible
0
0
scripts/
Reversible
0
0
.next/
Irreversible
120
3,932,160
node_modules/
Irreversible
400
3,276,800
scratch/
Irreversible
25
166
The split across the 15 targets was 10 reversible, 5 irreversible — with exactly one command shape, rm -rf <target>.
scratch/ is the interesting one. Its unrecoverable payload is 166 bytes, less than one twenty-thousandth of node_modules/. And yet losing it would hurt me far more. Node modules reinstall; half-written notes do not.
Byte counts measure what will not come back, not what it costs you. The probe can only produce the first number, so the weighting has to come from you. I ended up marking .env and my notes directory as unconditional stops regardless of size.
Even where the verdict is "reversible", getting things back is not free.
A single full-scale run:
Operation
Time
File count
rm -rf (tracked and ignored together)
45ms
2,376 → 111
git restore (from HEAD)
183ms
111 → 1,790
All 1,678 MDX files under content/ came back. .env, node_modules/ and scratch/ did not — which is exactly why the count lands on 1,790 rather than 2,376.
Five repetitions against content/ alone:
Run
Delete
Restore
1
28ms
174ms
2
27ms
172ms
3
27ms
174ms
4
31ms
235ms
5
27ms
187ms
Median 27ms to delete, 174ms to restore — roughly a 6.4x gap. The single full-scale run showed 45ms against 183ms, or 4.07x.
That asymmetry is worth carrying around as an intuition. When an agent run goes sideways, destruction throughput always exceeds recovery throughput. In the few seconds it takes a human to notice and reach for the keyboard, the gap widens by exactly that ratio.
Measurement 3 — force push is not decided by what survives on the remote
The canonical "outward-facing" operation is git push --force, and here the reversibility verdict was far less intuitive than deletion.
I reproduced it with a bare repository standing in for the remote:
Push a hotfix commit representing a teammate's work
Take a second clone, standing in for another machine
In the first clone, git reset --hard HEAD~1, rewrite, and --force push
Immediately after the force push:
What I checked
Result
core.logAllRefUpdates on the remote
Unset (disabled by default for bare)
Reflog entries on the remote
0
The overwritten commit object
Still present
Reflog entries locally
4
The clone taken beforehand
Still holds the object
The overwritten commit had not disappeared from the remote. But nothing named it. A bare repository does not record a reflog by default, so from the remote alone there is no way to learn which SHA main was pointing at a minute ago.
The object is there and unreachable. Calling that "recoverable" is a stretch.
I then ran git reflog expire --expire=now --all and git gc --prune=now on the remote. It took 32ms, and the object was genuinely gone.
The clone taken beforehand, however, could put it back:
# From the other machine, push the lost commit back under a new namegit push origin <lost-sha>:refs/heads/rescue# → completes in 20ms; the object reappears on the remote
So the reversibility of a force push is decided by who is holding that SHA at that moment — your own reflog, or someone who fetched before the rewrite. If neither exists, it is effectively unrecoverable well before garbage collection runs.
Structurally identical to the deletion case. Reversibility is a function of distributed state at execution time, not of the command's name.
Turning the ledger into deny rules
With the materials in hand, the remaining work is turning them into a pre-execution decision.
Here is the lightweight probe: one git status call with a pathspec, exiting 1 if anything unrecoverable is in scope.
#!/usr/bin/env python3"""Scoped variant. Exits 1 if anything unrecoverable sits under the target."""import os, subprocess, sysdef scoped_unrecoverable(path): r = subprocess.run( ["git", "status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignored=matching", "--", path], capture_output=True, text=True) lost_files = lost_bytes = 0 for entry in r.stdout.split("\0"): if len(entry) < 4: continue code, name = entry[:2], entry[3:] # ?? = untracked, !! = ignored — neither comes back from git if code not in ("??", "!!"): continue if os.path.isdir(name): for dp, _, fs in os.walk(name): for f in fs: lost_files += 1 try: lost_bytes += os.path.getsize(os.path.join(dp, f)) except OSError: pass else: lost_files += 1 try: lost_bytes += os.path.getsize(name) except OSError: pass return lost_files, lost_bytesif __name__ == "__main__": target = sys.argv[1] f, b = scoped_unrecoverable(target) print(f"{target}: unrecoverable {f} files / {b:,} bytes") sys.exit(1 if f else 0)
Results:
Target
Unrecoverable
Average time
content/
0 files / 0 bytes
61ms
node_modules/
400 files / 3,276,800 bytes
29ms
scratch/
25 files / 166 bytes
27ms
Whole repository
546 files / 7,209,147 bytes
32ms
When mapping this onto deny rules I did not simply enumerate every path with a non-zero count. I split the treatment by bucket:
State
Treatment
Why
Ignored and regenerable (node_modules, .next)
Allow
Does not come back, but can be rebuilt
Ignored and not regenerable (.env, keys)
Hard deny
Losing it stops the work
Untracked (scratch, notes)
Ask
Small in bytes, with no substitute
Tracked-modified
Ask
File returns, today's edits do not
Tracked-clean only
Allow
Fully back in 174ms
"Regenerable or not" is the one axis the probe cannot measure. That judgment has to be made once, by hand. Across four repositories it took me about 30 minutes to write that table. After that, the probe applies it at execution time.
What the pre-flight check costs inside a hook
Putting the judgment in a PreToolUse hook adds tens of milliseconds per call. Measuring the breakdown put the cost somewhere I did not expect.
Measured
Median
git status (whole repository)
6.2ms
git status (limited to content/)
5.3ms
git status (limited to node_modules/)
3.2ms
python3 -c pass (startup only)
17.3ms
python3 -S -c pass (skipping site)
8.1ms
scoped_probe.py src (end to end)
26.6ms
bash plus git status only
5.5ms
The logic itself accounts for 3–6ms. The dominant cost is Python interpreter startup at 17.3ms, and passing -S to skip site already brings it down to 8.1ms.
Making the algorithm smarter therefore changes nothing you can feel. Hooks are not long-lived processes, so the thing to cut is startup. I eventually rewrote the check as a shell script wrapping a single git status: 5.5ms.
Scaling with repository size:
Tracked files
Scoped
Full walk
1,000
34ms
67ms
5,000
54ms
182ms
20,000
88ms
582ms
At 20,000 files the full walk costs 582ms, which is too heavy for interactive work; the scoped version stays at 88ms. Narrowing with a pathspec pays off more the larger the repository gets.
Four things I actually tripped over during the half day.
Forgetting --ignored hides the unrecoverable set
git status --porcelain omits ignored files by default. Without --ignored=matching, neither .env nor node_modules/ appears at all, and the probe cheerfully reports zero unrecoverable files. A probe that reports "safe" incorrectly is worse than no probe.
At the default normal setting, an untracked directory folds into a single scratch/ entry — no file count, no byte count. Only with all did the 25 files and 166 bytes appear.
Do not count tracked-modified as recoverable
The path is restored, so it is tempting to put it on the reversible side. What returns is the HEAD content; the day's edits are gone. I ran a whole pass with that judgment too loose and had to redo it when the numbers refused to add up.
Do not rely on the remote's reflog
core.logAllRefUpdates is disabled by default in bare repositories. A recovery runbook written on the assumption that "it is still on the remote" will find the object present and unnameable. The paths you can actually rely on are your own reflog, or a clone taken before the rewrite.
The highest-value move is to run the probe once against the repository you are working in right now and look at the unrecoverable byte count as an actual number. Mine was 25.3%. I had been assuming zero.
Seeing that number changes the order in which you write deny rules. Instead of listing commands that sound dangerous, you start by covering the places where the things you cannot replace happen to live.
August 14 is close, but the raw material for the decision costs tens of milliseconds of execution plus the thirty minutes it takes to decide, once, what is regenerable. Having your own reversibility in numbers makes the choice between turning auto mode off and leaning into it a much calmer one.
Two of the four app repositories I maintain still had ambiguous handling of .env until I ran this. If it saves someone else the same stumble, that is a good outcome.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.