ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-08-24Advanced

The Cleanup Step Removed the Working Directory, Not Its Contents — Making Unattended Destruction Fail Closed

An unattended cleanup step deleted the working directory itself instead of what was inside it. Here is why the mkdir -p that followed was not a safety net, and how a defensive-looking default value ended up selecting the destructive branch, with the actual verification output.

antigravity443agents134automation91shell3reliability12

Premium Article

I opened the logs one morning and a directory that should have been there was gone.

To be precise, what disappeared was not the contents of the cleanup target. It was the cleanup target itself. A maintenance step that runs once overnight — one line I had written — had taken the parent directory along with everything under it.

The cause was obvious within a minute. I had forgotten -mindepth 1 on a find. What took much longer was everything after that: why I had not noticed, and why the mkdir -p sitting right below it had not helped. Following those two questions turned up the same shape of hole in several other pieces of automation I run as an indie developer shipping my own apps.

The real question turned out to be a single one. When an unattended step lands in an "I don't know" state, which way does it fall? Mine all fell toward destruction.

What disappeared was the directory, not the files inside it

Here is the incident reproduced. The output below comes from an actual run on GNU bash 5.1.16 with GNU findutils 4.8.0.

B=/tmp/probe1; rm -rf $B; mkdir -p $B
mkdir -p $B/a/work/sub1 $B/b/work/sub1
touch $B/a/work/f1 $B/a/work/sub1/f2
touch $B/b/work/f1 $B/b/work/sub1/f2
 
# The broken form: no -mindepth 1
find $B/a/work -delete
echo "a/work exists? $([ -d $B/a/work ] && echo yes || echo NO)"
 
# The correct form
find $B/b/work -mindepth 1 -delete
echo "b/work exists? $([ -d $B/b/work ] && echo yes || echo NO) / entries=$(ls -A $B/b/work | wc -l)"

Result:

a/work exists? NO
b/work exists? yes / entries=0

The starting path you hand to find is itself the first match. -delete acts on that first match too. So -mindepth 1 is not an optional refinement. It is the flag that decides whether the starting point survives.

That part is preventable once you know it. The part that actually cost me time came next.

mkdir -p was not a safety net

My script had mkdir -p "$WORK" immediately after the cleanup. The intent was "even if something goes wrong, we recreate it." Here is what that actually buys you:

mkdir -p $B/a/work
echo "a/work exists? $([ -d $B/a/work ] && echo yes) / entries=$(ls -A $B/a/work | wc -l)"
a/work exists? yes / entries=0

The directory comes back. The contents do not. And the awkward part is that everything downstream then proceeds normally, because everything downstream only checks that the directory exists. The existence test passes, writes succeed, the exit code is zero.

An incident happened, and nothing observed it as one. I found out the next morning by reading logs with my own eyes. The mkdir -p I had written as recovery was working as concealment.

The same shape shows up in other places. "Create it if missing." "Reinitialize it if corrupt." "Fall back to defaults if unreadable." All three wear the face of recovery, and all three erase the evidence before continuing.

Antigravity CLI 1.1.16 shipped a fix for exactly this pattern: it no longer overwrites an unparseable settings.json with defaults. Previously, once the file could not be parsed, the next save silently reset every setting. Now the save is refused, the file is preserved byte for byte, and the status line names the offending file.

Overwriting an unreadable config with defaults is the same construction as my mkdir -p. A branch written with recovery in mind became the path that destroys the most information. It was a well-timed reminder to go read my own scripts.

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
You will be able to find which of your unattended cleanup and initialization steps fall toward deletion when something is unknown, before an incident forces you to find out
You will be able to recognize, inside your own scripts, why filling a failed lookup with a default value quietly selects the destructive branch
You will be able to port a guard that keeps deletion inside an allowed root, including the symlink path that a naive prefix check lets through
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.

or
Unlock all articles with Membership →
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 →

Related Articles

App Dev2026-07-02
Stop Treating Dependency Updates as a Monthly Chore — Weekly Agent Runs with Semver Risk Triage and Verification Gates
Move from batch-updating 47 stale packages at once to a weekly agent-driven routine: semver-based risk tiers, a playbook YAML, hallucination-proof changelog reports, and a lockfile diff gate.
App Dev2026-08-15
How Far to Narrow an Agent's Choices in a 30-Category Wallpaper Classification Pipeline
Asking an agent to pick one of 30 categories per image means re-running every image the moment a definition changes. Here is the reasoning and the implementation behind switching to closed-vocabulary tags plus a deterministic rule mapping.
App Dev2026-07-18
After Compose-First: Choosing Which View Screens to Migrate, Ranked by Churn Instead of Count
Google has declared Android development Compose-first. Here is how I rank View-based screens for migration using git history rather than screen counts, with the scoring script I actually ran and the three places partial migration quietly duplicates state.
📚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
See all →