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.
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 deniedfatal: not in a git directoryerror: 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 mainelse 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.
A three-tier fallback that always lands somewhere writable
With the decision function in place, what remains is the landing strategy. The three tiers I use are these.
Tier
Action
Precondition
1
Reuse the persistent folder as-is
It exists and an actual write succeeds
2
Delete the broken folder and rebuild it
The parent is writable and removal succeeds
3
Escape to a per-user location
Removal fails (owned by another). $HOME is always writable by me
Tier three is the crux. A folder that has drifted in ownership belongs to someone else (nobody), so even rm -rf may be rejected. Persisting here never wins. Give up cleanly and switch to a location you can certainly write, such as $HOME/repos. For an unattended run, "run to completion in a writable place" is more correct than "cling to the ideal location and stall" — a judgment the mornings taught me more than once. I prefer this bias toward escaping over deleting.
# List candidates in priority order and settle on the first writable one.# Returns the chosen working directory path on stdout.resolve_workdir() { local site="$1" # e.g. antigravitylab.net local primary="/tmp/repos/${site}" # the persistent folder we want to reuse local fallback="${HOME}/repos/${site}" # --- Tier 1: can we use the existing persistent folder as-is? --- if [ -d "$primary/.git" ] && is_writable_dir "$primary"; then if owner_is_me "$primary"; then echo "$primary"; return 0 fi # Writable but not owned by me (rare, e.g. ACLs). Allow it, but log it. echo "WARN: $primary is writable but owner UID differs (ACL etc.)" >&2 echo "$primary"; return 0 fi # --- Tier 2: if broken, delete and rebuild --- if [ -e "$primary" ]; then echo "INFO: $primary not reusable. Attempting removal." >&2 if rm -rf "$primary" 2>/dev/null && mkdir -p "$primary" 2>/dev/null && is_writable_dir "$primary"; then echo "$primary"; return 0 fi echo "WARN: cannot delete/recreate $primary (likely owned by another user)" >&2 else # Only absent so far: test whether we can create it. if mkdir -p "$primary" 2>/dev/null && is_writable_dir "$primary"; then echo "$primary"; return 0 fi fi # --- Tier 3: escape to a per-user location --- mkdir -p "$fallback" 2>/dev/null if is_writable_dir "$fallback"; then echo "INFO: using fallback $fallback" >&2 echo "$fallback"; return 0 fi echo "FATAL: could not secure a writable working directory" >&2 return 1}
The caller does not hardcode the path; it uses the function's return value.
SITE="antigravitylab.net"WORK="$(resolve_workdir "$SITE")" || { echo "Failed to secure a working folder. Aborting run."; exit 1; }echo "Working directory: $WORK"# From here, $WORK is guaranteed writable, so clone / pull can branch safely.if [ -d "$WORK/.git" ]; then git config --global --add safe.directory "$WORK" cd "$WORK" git remote set-url origin "https://${TOKEN}@github.com/${SITE%.*}/${SITE}.git" git reset --hard HEAD; git clean -fd git pull --rebase origin mainelse git clone --depth 1 --branch main "https://${TOKEN}@github.com/${SITE%.*}/${SITE}.git" "$WORK" cd "$WORK"figit config user.email "you@example.com"git config user.name "Your Name"
The second wall: Git refuses even when you can write
There is a reason I slip in git config --global --add safe.directory "$WORK". When you reuse a folder whose owner just changed, Git can refuse every operation with detected dubious ownership. The write itself goes through, yet Git's ownership check stops you — a wall on a different layer. If you are dealing with ownership drift, this one line is almost a required companion. I learned that the hard way when a folder became writable again and Git's check tripped me a second time, because I had forgotten to add this line.
Order matters too. Register safe.directorybeforecd or git reset. In a dubious-ownership state almost every git subcommand is rejected immediately, so registering it late means the git config itself may pass while the earlier reset has already failed. Treat the writability check and the ownership registration as one continuous, two-layered wall.
What six weeks of unattended runs taught me
Here is the record from running the same four-site schedule before and after adding this design. The sample is six weeks and 47 runs after the preflight went in.
Item
Before (impression)
After (measured)
Ownership-drift events
A few per month, patched by hand in the morning
5 of 47 runs
Recovered automatically
0 (always manual)
5/5 (all completed via fallback)
Breakdown of recovery
—
Tier 2 once, Tier 3 escape four times (about 80%)
Preflight overhead
—
about 40–120 ms per run
The numbers look modest, but the one thing that mattered to me was that morning patching went to zero. In about 80% of the events — four of five — the run finished via the Tier 3 escape; even when the /tmp persistent folder became someone else's property, that day's article generation did not stop. Tier 2 (delete and rebuild) was enough only once. In other words, most ownership drift is discovered in an "I can no longer delete this" state, and betting on the escape holds up better in practice than betting on removal — a feel that matched the data.
The preflight cost at most about 120 ms per run: creating and removing a probe file, one stat, and a mkdir -p attempt. As a preamble to a run that takes tens of minutes, it is within the noise. Skimping here and running on the existence check instead — then tripping midway through a pull and leaving a half-written state — costs far more to clean up in total.
Idempotency, so the same morning never returns
One last caution when you fold this function into an unattended run: it matters that resolve_workdir settles on the same conclusion no matter how many times it is called. So that a run retried midway does not scatter work across yet another folder, I keep to three rules:
Always remove the probe file; leave no litter behind on each check
Never make a Tier 2 removal failure fatal; fall quietly through to the next tier
Pin the fallback to the same path every time; do not let retries multiply it
When working folders multiply under unattended operation, the next morning waiting for you is a different one: disk exhaustion. A design that does not multiply folders is, in the same breath, a design for stable operation.
Start by replacing a single [ -d "$WORK/.git" ] in your pipeline with is_writable_dir "$WORK". I am still finding my footing with unattended operation myself, but when the mornings stay quietly green for a stretch, I let out a small breath of relief. Thank you for reading.
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.