ANTIGRAVITY LABJP
Articles/Integrations
Integrations/2026-07-15Advanced

When an Unattended Run Finds Its Working Folder Owned by Someone Else

A schedule that had been green for weeks failed one morning because it could not write .git/config. The cause was ownership drift on a reused working folder. Here is how to tell apart the read-but-not-write trap, probe writability for real, and fall back to a folder you can always write.

antigravity433antigravity-cli9unattended2automation82troubleshooting106

Premium Article

One morning I was reading down a column of unattended run logs and my finger stopped. A run that had been green for weeks had gone red on its very first step. The log said:

error: could not lock config file .git/config: Permission denied
fatal: not in a git directory
error: cannot open .git/FETCH_HEAD: Permission denied

The strange part was a line a little further down, where find content/articles/ja -name '*.mdx' | wc -l had returned a count without complaint. The files were readable. The listing worked. And yet git insisted it could not write. Nothing had been deleted, and no credential had expired.

The culprit was the owner of the persistent working folder I had been reusing on every run. When you run several sites on unattended schedules as an indie developer, re-cloning from scratch each time feels wasteful, so you keep the checkout somewhere like /tmp/repos/site.net and pull only the diff with git pull --rebase. But when the execution sandbox is recycled between generations, that persistent folder can survive as the property of a different UID, often nobody. Anyone can read it, so ls and find pass. What fails is the lock on .git/config — the write. That asymmetry is exactly what made the morning triage harder than it should have been.

This is a record of designing a preflight that spots the "readable but not writable" folder before an unattended run starts, and quietly moves the work to a place I can write. It leans toward checking the footing before the run begins, rather than chasing error messages afterward.

Why the existence check lies

Most pipelines, I suspect, decide whether a working folder can be reused like this:

if [ -d "$WORK/.git" ]; then
  cd "$WORK" && git pull --rebase origin main
else
  git clone --depth 1 "$REPO_URL" "$WORK"
fi

-d "$WORK/.git" only asks whether the directory exists. Regardless of owner, regardless of permissions, if it is there, it returns true. A folder that has drifted in ownership is misjudged as "reusable" right here. Then git pull --rebase goes to write .git/config or .git/FETCH_HEAD for the first time, and only then does everything collapse.

In other words, the failure is late. The preflight does not catch it; the real work advances partway and then trips, so the log reads like "a mysterious permission error in the middle of a pull." I spent the better part of an hour suspecting GitHub authentication. Authentication had nothing to do with it — I simply could not touch the local .git. That mistaken read is exactly the kind of gotcha worth closing off in advance, precisely so an unattended run in production never stalls.

One note from firsthand digging: reads succeed because the folder or its parents usually carry 755-style permissions that grant others read plus execute. Even when the owner is nobody, the rest of us can traverse the directory and read inside it. Only the write bit is missing for me. That single line of asymmetry explains the entire symptom.

Decide writability by actually writing

If the existence check lies, replace the decision with "did a write actually succeed." Comparing the owner UID (via stat) helps as a signal, but the final truth is whether I can create a temporary file under that directory right now. Depending on the mount type or ACLs, a matching UID may still be unwritable, and a mismatched UID may be writable. I prefer, and recommend, a two-layer stance: treat the owner UID as a warning signal and let an actual write decide.

The following function is the core of this article. It takes a candidate directory, creates and removes a temporary file, and returns true if the write succeeded.

# Confirm "can I write here right now" by an actual write.
# Do not rely on existence or the owner UID alone.
is_writable_dir() {
  local dir="$1"
  [ -d "$dir" ] || return 1
  # Create a uniquely named probe directly inside; judge by success.
  local probe="${dir}/.wtest.$$.$RANDOM"
  if ( set -C; : > "$probe" ) 2>/dev/null; then
    rm -f "$probe" 2>/dev/null
    return 0
  fi
  return 1
}
 
# Does the owner UID match mine? (A warning signal, not the verdict.)
owner_is_me() {
  local dir="$1"
  [ -d "$dir" ] || return 1
  local owner_uid
  owner_uid="$(stat -c '%u' "$dir" 2>/dev/null || stat -f '%u' "$dir" 2>/dev/null)"
  [ "$owner_uid" = "$(id -u)" ]
}

I redirect with set -C (noclobber) enabled so that even a probe-name collision will not clobber an existing file. Mixing $$ (the PID) and $RANDOM makes collisions all but impossible, but I stay on the safe side anyway. stat uses different syntax on Linux (-c) and macOS/BSD (-f), so trying both is the practical workaround. An unattended run does not know which base it will land on, and absorbing these small differences up front prevents the "fails only on one environment" surprise later.

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
A complete preflight that decides reuse by an actual write probe, not an existence check that quietly lies
A three-tier fallback that tries to delete a drifted folder and, when it cannot, escapes to a per-user location you always own
Six weeks and 47 runs of field data: five ownership-drift events, all recovered automatically, plus the idempotency details that keep the folder from multiplying
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 $10 for lifetime access
View Membership →

Related Articles

Integrations2026-06-18
On Cutover Day to the Antigravity CLI, Verify Production Automation by Side-Effect Equivalence, Not Output
On the day you switch from the Gemini CLI to the Antigravity CLI, verify production automation by the equivalence of side effects — files written, commits, network calls — instead of matching stdout. A sandbox parallel run and a go/no-go cutover gate, with implementation steps.
Integrations2026-07-13
Pour Your Unattended Run Logs into SQLite and Count Failures Across Every Run
Running Antigravity CLI on an unattended schedule leaves a growing pile of JSON Lines logs. Instead of grepping files, I pour them into a tiny SQLite database and answer 'which step fails, and when' with a single query. Here is the ingest script, six diagnostic queries, and six weeks of real numbers.
Integrations2026-07-08
When Successful Automation Quietly Stops Earning Its Keep: Designing for Value Decay and Retirement
A dashboard full of green success logs often hides the most dangerous kind of failure. Here is a design — with working code — for surfacing automations that keep succeeding while producing no value, and deciding whether to retire, repair, or keep them.
📚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 →