◉ANTIGRAVITY LABJP
Articles/AI Tools
⚙ AI Tools/2026-10-06Intermediate

A report of a cache cleanup that wiped a drive: why I now route every delete through a checkpoint

After reading about a cache cleanup request that ended with a developer's drive contents gone, I reopened my own cleanup script. Here is a small Python checkpoint that narrows how far a delete can reach, plus where I draw the line on what to hand to an agent without approval.

Antigravity377SafetyDelete CommandsPython18Approvals

The report said a developer asked an agent to clear a cache and ended up with the contents of a drive gone. That night I opened my own build-cleanup shell script instead of closing the tab.

As an indie developer, I clear build leftovers several times a week. One line in that script, if its variable ever came through empty, could have pointed at a root directory. The report was about an agent, but my stomach dropped over an old script of mine.

What I want to say first: narrowing how far a delete can reach matters more than deciding whether to switch approvals off. Below is a small Python checkpoint, and the line I now draw between what I hand over and what I don't.

What was reported, and what I won't claim

I only read the headline and summary of the coverage. A cache cleanup was requested, and more than intended was deleted. Whether the cause was a vague instruction, path resolution, or a permission setting, I can't tell from what is public.

So please read what follows not as a reproduction of that case, but as a precaution against the same kind of accident in your own setup. The kind is simple: the delete reached further than the person who asked imagined.

Narrow the reach first, polish the request second

My first instinct was to fix the prompt. I added "only the build folder of this project" and then more reminders. The results were poor, because a reminder only works if the reader honors it.

I do it the other way round now. I don't leave correctness to attention or polite wording. A machine checks the scope at the one point just before the delete runs. The checks are few:

  • The target is not an empty string (this catches an unexpanded variable)
  • Once resolved to a real path, the target sits inside the project
  • The target is not the project root itself
  • The target is not shallow, like a root or /Users/name
  • Symbolic links are not followed
  • Directories that contain .git are never removed

Every one of these looks obvious in hindsight. That is exactly why they slip when left to human eyes.

The checkpoint (dry-run by default)

Save this as safe_rm.py outside the project, for example in a bin folder under your home directory. Keeping it outside the project is a small detail that matters. If it lives where the agent can edit, the checkpoint itself can be rewritten.

#!/usr/bin/env python3
"""A checkpoint in front of delete commands. Dry-run by default; deletes only with --apply."""
import argparse
import os
import shutil
from pathlib import Path
 
MIN_DEPTH = 3  # anything shallower than /Users/name/project is refused outright
 
def check(target: str, root: Path) -> Path:
    if not target.strip():
        raise SystemExit("refused: empty target (a variable may not have expanded)")
    p = Path(target).expanduser()
    if p.is_symlink():
        raise SystemExit(f"refused: symlinks are not followed: {p}")
    real = p.resolve(strict=True)
    if len(real.parts) <= MIN_DEPTH:
        raise SystemExit(f"refused: path is too shallow: {real}")
    if real == root or root not in real.parents:
        raise SystemExit(f"refused: outside the project, or the root itself: {real}")
    if (real / ".git").exists():
        raise SystemExit(f"refused: directory contains .git: {real}")
    return real
 
def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("targets", nargs="*")
    ap.add_argument("--root", default=os.getcwd())
    ap.add_argument("--apply", action="store_true")
    a = ap.parse_args()
    root = Path(a.root).resolve(strict=True)
    if not a.targets:
        raise SystemExit("refused: no targets")
    for t in a.targets:
        real = check(t, root)
        if real.is_dir():
            size = sum(f.stat().st_size for f in real.rglob("*") if f.is_file())
        else:
            size = real.stat().st_size
        print(f"{'delete' if a.apply else 'would delete'}: {real} ({size:,} bytes)")
        if a.apply:
            shutil.rmtree(real) if real.is_dir() else real.unlink()
 
if __name__ == "__main__":
    main()

Three reasons for the shape. Without --apply nothing is removed, so the first command an agent runs is always a preview. resolve(strict=True) refuses to wave through paths that don't exist. And the check uses root not in real.parents rather than a string prefix, because a prefix match confuses /proj with /proj-old.

What happened when I ran it

I built a small tree: a proj folder with build/cache and a symlink pointing outside, and one file outside proj that must survive.

Target passed inResult
proj/build/cache (dry-run)Preview only; nothing removed
Empty stringRefused (exit code 1)
A directory outside projRefused
A symlink pointing outsideRefused; the outside file is untouched
The project root itselfRefused
/tmp (shallow)Refused
A subdirectory containing .gitRefused
proj/build/cache (--apply)Deleted; the outside file remains

Because refusals exit with code 1, they show up as failures when an agent runs the script. Nothing slips through quietly.

Which work gets approval-free, and which doesn't

A checkpoint doesn't replace deciding what to hand over. Here is where I split it.

TaskHand over without approvalWhy
Regenerating build artifactsYesIf it vanishes, the same steps bring it back
Deletes through safe_rm.py (dry-run)YesIt only prints a plan; nothing changes
safe_rm.py --applyAfter I lookI read the printed plan once myself
Deletes that include paths outside the projectNoI confirm recoverability first
Deletes under a synced folder (Dropbox and similar)NoThe deletion propagates to other machines

I added the synced-folder row from experience. Something deleted inside Dropbox leaves you relying on version history unless you have a local backup. It is recoverable, but hunting for it in the middle of the night is something I would rather skip.

A second safety net, for work that skips the checkpoint

Even with deletes narrowed, I keep a way back. Before any large task I do just two things:

  1. Commit the working directory once in Git so the state, untracked files included, is pinned
  2. Keep anything that lives outside the project out of the agent's working directory in the first place

Both are short sentences, and that is why I can still do them on a tired day.

Approval-screen names and locations change between versions, so I won't describe them here. The idea of allow lists and deny lists is laid out in the official documentation; when you touch those settings, I'd keep it open beside you.

One line to start with

Open your cleanup script today and check what it would delete if a variable came through empty. That alone gave me one cold sweat.

People polish the request; a machine narrows the reach. I try to keep that split even on days when I'm worn out.

Share

Thank You for Reading

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

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⚙ AI Tools2026-08-14
When a Fast Model Feels Slow, Look at Reasoning Effort Before Switching Models
Antigravity lets you pick a reasoning effort level per model. Here is how I decide between Low, Medium, and High based on the shape of the task, what to check when changing it makes no difference, and a small script for correcting your own judgment with records instead of memory.
⚙ AI Tools2026-07-30
Half My Tasks Went to Pro — and So Did Only 61% of the Tokens
A singular model setting became a models collection, which means routing across models is now something you define yourself. Here is how I re-measured a task-type routing rule against the actual context-size distribution of 67 tasks in my own repository.
⚙ AI Tools2026-07-05
Is the $100 AI Ultra Tier Worth It Solo? Measure the Break-Even from Limits and Parallelism
Whether the $100/month AI Ultra tier (5x the Pro limit) is worth it for an indie developer, framed as a break-even from how often you hit the cap and the effective throughput of parallel agents, with a calculator script.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links