●MODEL — Claude Opus 4.8 and Haiku 4.5 arrive in the Messages API for coding and agentic work●CODE — Claude Code adds /rewind to resume before /clear, with steadier MCP reliability and OAuth retries●CODE — CPU use during streaming drops about 37%, improving stability on long-running sessions●CLOUD — Claude is generally available in Microsoft Foundry on Azure with Azure-native access●SECURITY — Static API keys can now be replaced with WIF short-lived, scoped credentials●POLICY — The US government clears Anthropic to release Mythos 5 to about 100 firms and agencies●MODEL — Claude Opus 4.8 and Haiku 4.5 arrive in the Messages API for coding and agentic work●CODE — Claude Code adds /rewind to resume before /clear, with steadier MCP reliability and OAuth retries●CODE — CPU use during streaming drops about 37%, improving stability on long-running sessions●CLOUD — Claude is generally available in Microsoft Foundry on Azure with Azure-native access●SECURITY — Static API keys can now be replaced with WIF short-lived, scoped credentials●POLICY — The US government clears Anthropic to release Mythos 5 to about 100 firms and agencies
Trusting a Three-Day-Old Mirror: Stopping Unattended Tasks from Acting on a Stale Working Copy
A persistent clone reused for speed quietly drifts from the remote. Read 'has this article been fixed yet?' from an out-of-sync tree and your unattended task duplicates work or 'succeeds' against an old world. Here is a HEAD-match plus writability preflight, with a self-healing re-clone, in bash.
An unattended scheduled task can log "success" while the thing it produced is still from a previous timeline. As an indie developer, I run four AI blogs (Dolice Labs) as Cowork scheduled tasks, and one morning's log gave me a jolt. Before editing, I checked "has this article already been revised?" against my local clone — but that clone was three days old, and in reality another run had finished updating the very same article upstream. I was one step away from rewriting the same piece twice.
Not a single error fired. The git clone had succeeded, the files were readable, the quality gates passed. And yet the ground my decision stood on had shifted. This class of incident shows up not as a "failure" but as "correct processing on top of an outdated truth," so no exception handler will catch it. Here is how to treat a persistent clone as a cache and verify its freshness as a contract before you make any decision.
Why a persistent clone drifts in silence
For speed, most automations don't re-clone the working repo every run. They keep it somewhere like /tmp/repos/{site} and update with git pull --rebase. That's a sound optimization. The danger is that downstream steps implicitly assume the persistent clone is "the latest." Drift enters through three paths.
First, another run has already moved ahead. If several scheduled slots (morning generation, midday revision, nightly recovery) touch the same repo, another run has pushed since your last pull. Your tree doesn't know about that push.
Second, ownership flipped and the pull itself failed silently. On Cowork's VM, /tmp/repos/ can persist across sessions owned by nobody:nogroup, and then git pull and writes are both rejected. If your recovery appends || true, the step keeps going after failing — and you decide on top of an old tree.
Third, a rebase finished halfway. If a prior run hit a conflict or interruption and couldn't complete the rebase, it may leave a detached state or a partial commit. git log looks plausible but isn't current.
The common thread: readable but old. That is exactly why "could I read it?" is the wrong question, and "does it match the latest?" is the right one.
Why deciding on a stale tree breaks quietly
An unattended task reads its local tree mostly for decisions that come before processing. Questions like these:
The decision
What a stale tree does
Does this slug already exist?
Mistakes an upstream-added article for "missing" and creates a duplicate slug
Has this article been revised already?
Sees "not done" for something already fixed, and rewrites it twice
Do JA and EN counts match?
Calls it balanced on an old count, missing a real mismatch
Did we just publish this topic?
Reads an old article list, judges "new," and duplicates the subject
When the decision is wrong, no amount of correct processing afterward saves the result. Worse, the failure mode is "clean exit," so the log reads Status: SUCCESS. The principle I hold onto: a decision made on an unverified tree is invalid, even if its conclusion looks right. Guarantee freshness before the decision. Design it as a matter of ordering.
✦
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
✦Three ways a persistent clone goes stale (another run advanced it after push, ownership flipped to nobody, a half-finished rebase) and how each one poisons decision reads
✦A sub-second preflight that matches local HEAD against remote via git ls-remote and probes .git writability before any decision is made
✦A self-heal flow that catches up with pull --rebase, falls back to a fresh clone under $HOME when it can't, and treats any decision made on an unverified tree as invalid
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.
You don't need a heavy git fetch. git ls-remote pulls only the refs, so you can read the branch tip SHA and match it against your local SHA. While you're at it, confirm you can even write (ownership and permissions).
# preflight_clone_fresh: is the working tree writable AND equal to the remote tip?# return: 0=fresh / 10=stale(needs pull) / 20=unwritable / 30=missingpreflight_clone_fresh() { local work="$1" remote_url="$2" branch="${3:-main}" # (a) does the clone exist? [ -d "$work/.git" ] || return 30 # (b) is .git writable? (detect nobody-owned dirs / missing permissions) if ! ( touch "$work/.git/.wtest" 2>/dev/null && rm -f "$work/.git/.wtest" ); then return 20 fi # (c) match local HEAD against the remote tip SHA local local_sha remote_sha local_sha="$(git -C "$work" rev-parse HEAD 2>/dev/null)" remote_sha="$(git ls-remote "$remote_url" "refs/heads/$branch" 2>/dev/null | awk '{print $1}')" # if the remote can't be read, treat "unknown" as stale and fail safe [ -z "$remote_sha" ] && return 10 [ -z "$local_sha" ] && return 10 [ "$local_sha" = "$remote_sha" ] && return 0 return 10}
The key move is deliberately treating "unknown" — couldn't read the remote SHA, couldn't read the local SHA — as the stale side (needs sync). Optimistically calling something "latest" when you can't confirm its freshness is precisely how this incident started. When you lack proof, don't advance. That's what failing safe means here.
A self-heal flow once you detect it
Branch on the preflight's return code: catch up, or rebuild. If pull --rebase can catch you up, that's fast. If you can't write, the clone is missing, or you can't catch up, make a fresh clone under $HOME (sidestepping /tmp ownership trouble).
ensure_fresh_clone() { local work="$1" remote_url="$2" branch="${3:-main}" preflight_clone_fresh "$work" "$remote_url" "$branch" local code=$? case "$code" in 0) echo "fresh (local==remote): $work" ;; 10) echo "stale detected — catching up with pull --rebase" if git -C "$work" pull --rebase origin "$branch" >/dev/null 2>&1; then # re-match after catching up to get proof preflight_clone_fresh "$work" "$remote_url" "$branch" \ && echo "synced" \ || { echo "still mismatched after pull — re-cloning"; reclone_fresh "$work" "$remote_url" "$branch"; } else echo "pull failed — re-cloning" reclone_fresh "$work" "$remote_url" "$branch" fi ;; 20|30) echo "unwritable/missing — fresh clone under \$HOME" reclone_fresh "$work" "$remote_url" "$branch" ;; esac}reclone_fresh() { local stale="$1" remote_url="$2" branch="${3:-main}" local fresh="$HOME/repos/$(basename "$stale")" rm -rf "$fresh" 2>/dev/null git clone --depth 1 --branch "$branch" "$remote_url" "$fresh" >/dev/null 2>&1 git -C "$fresh" config user.email "you@example.com" git -C "$fresh" config user.name "you" # everything downstream uses this fresh tree as the working copy export WORK="$fresh" echo "fresh clone ready: $fresh"}
reclone_fresh overwrites and exports WORK on purpose: from this point on, both decision reads and writes must run against the verified tree. Keep referencing the old WORK by accident and your preflight is wasted. Swap the ground, swap the variable. It's unglamorous, but skip it and the incident comes back.
Before / After: one line before the decision
My task preamble used to read like this:
# Before: implicitly trusting the persistent clone as "latest"WORK="/tmp/repos/claudelab.net"cd "$WORK" && git pull --rebase origin main || true # keeps going even on failure# then decides "does this slug exist?" <- possibly on an old tree
|| true is the trap: if pull fails on an ownership problem, the step still advances, and the later slug check and count run on a stale tree. Insert the preflight and it becomes:
# After: guarantee freshness as a contract before the decisionWORK="/tmp/repos/claudelab.net"REMOTE="https://github.com/you/claudelab.net.git"ensure_fresh_clone "$WORK" "$REMOTE" main # may swap WORK to a fresh treecd "$WORK"# decision reads from here run only against a verified treeJA=$(find content/articles/ja -name '*.mdx' | wc -l)EN=$(find content/articles/en -name '*.mdx' | wc -l)
The diff is barely a line, but the meaning shifts entirely. You replace the implicit assumption "I could read it, so it's latest" with the explicit contract "I confirmed it's latest, so I read it."
How far to take it — balancing cost and safety
A fresh clone every run is certain, but it's 2-3x slower for large repos and eats disk. So the realistic combination is "fast persistent clone plus cheap freshness check." git ls-remote reads refs once and finishes in a few hundred milliseconds; the write probe is instant. The heavy re-clone (tens of seconds) runs only when it truly must — unwritable, or can't catch up. I recommend that asymmetry: light all the time, heavy only rarely.
Drawing the line by the weight of the decision helps too. The priority order I actually bake into tasks:
Before decisions that corrupt what you publish (duplicate slugs, JA/EN count parity), always run the preflight
Before decisions that lead straight to double work (already revised? already published this topic?), run it too
For harmless steps like formatting a log, skip it
I run the same design in a separate task that aggregates AdMob revenue every morning, because getting the freshness of those numbers wrong skews the whole day's decision. Rather than guarding everything at maximum strength, concentrate verification where breakage hurts; that's what keeps a long-running operation from burning out.
One thing you can do tomorrow: add a single local HEAD == remote HEAD match to the top of an unattended task you already run. Since I added that one line, the particular dread of "it says success but the contents are old" has eased a great deal. I'd be glad if we could keep closing these quiet failures one at a time, together.
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.