●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.301 — Unattended retries now stop on their own when a usage limit is hit instead of waiting (10/15). Latest is 2.1.304 (10/19)●SONNET 4.5 — Retires on the Claude API on 11/30, 40 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.301 — Unattended retries now stop on their own when a usage limit is hit instead of waiting (10/15). Latest is 2.1.304 (10/19)●SONNET 4.5 — Retires on the Claude API on 11/30, 40 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Why I Stopped Borrowing a Stale Repo and Started Cloning Fresh After a Scheduled Task Couldn't Write to Shared /tmp
A scheduled task stopped because it could not create its clone directory under /tmp. Here is how I traced it, the borrowable clone that was about 9.6 hours behind, the cost of cloning fresh every run, and a script that picks a working directory.
On a Wednesday evening I opened the log of a scheduled task and found it had stopped at its first Git operation.
fatal: could not create work tree dir '/tmp/repos/claudelab.net': Permission denied
My runbook says to keep a persistent repository under /tmp/repos and git pull it next time. That one line told me there are days when the assumption does not hold.
What follows is how I traced the cause, and why I stopped borrowing a clone that another run had left behind and decided to clone fresh every time. If you run Git against a repository from a Cowork scheduled task, I hope it saves you from stepping on the same spot.
Four commands to separate the cases: /tmp is writable, /tmp/repos is not
I suspected /tmp itself first: a full disk, or a read-only mount. I checked in this order.
id # who am Ils -ld /tmp /tmp/repos # owner and mode of parent and childmkdir /tmp/repos/x # can I create under the childtouch /tmp/_w_test && echo "tmp root writable" # can I write directly in /tmp
Three things to read from this. /tmp is drwxrwxrwt (sticky bit, writable by everyone), so a touch directly under it succeeds. /tmp/repos is owned by nobody with mode 755, so my user (uid 1079) can enter it but cannot create anything inside. And its creation time, 01:16 the same day, shows that a different run made it first.
Being able to write to /tmp says nothing about whether you can write to a directory someone else created inside it. I briefly reasoned that since /tmp was writable the problem must lie elsewhere, and started with the disk. There was no shortage: 3,787 MB free on / and 7,229 MB on /sessions.
A borrowable clone existed, but it was about 9.6 hours behind
Directly under /tmp sat a claudelab.net clone, apparently left by another run (also owned by nobody). It was readable, and I was ready to copy it and skip the clone.
9296076 2026-10-20 23:06:43 +0000 <- the borrowable one
3dd2738 2026-10-21 08:41:15 +0000 <- latest origin/main
The borrowable clone trailed origin by about 9.6 hours. Because .git was not writable either (touch .git/probe gave Permission denied), I could not pull in place. Using it would mean copying first and pulling afterwards, which is a two-step detour.
For work that edits content, that state is quietly dangerous. If you check for duplicate slugs or confirm that internal link targets exist while blind to nine hours of changes, a page you judge missing may exist, or the reverse. The real cost of borrowing a stale clone is not the clone time. It is the probability of a wrong decision.
✦
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 can tell whether a shared /tmp is 'writable but not for you' and find the cause with four commands
✦You can stop a run from appending to a stale clone that another run left behind, by comparing SHAs before you touch anything
✦You can put the choice of working directory into one script, so a scheduled task spends no time hunting for a place to work
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.
So what does giving up reuse actually cost? I measured it.
Operation
Time
Disk
git clone --depth 1 (new)
2.8 s
50 MB working tree (13 MB of it .git)
git pull --rebase (my own existing clone)
0.7 s
delta only
Whole script below, first run (pick location + clone + SHA check)
3.7 s
50 MB
Whole script below, second run (pull + SHA check)
1.85 s
delta only
Reuse saves about 2 seconds per run. For a task that runs once a day, that is roughly 12 minutes a year. Taking on "depends on another run's state" and "cannot notice staleness" for that price was not a good trade for how I work.
One caveat. $HOME is a per-run location (/sessions/rcw-… this time) and was not carried into the next run. So "keep it in $HOME/repos and pull next time" cannot be counted on, and every run ends up cloning anyway. At 2.8 seconds, that is fine.
A script that picks the location, then verifies after cloning
I reduced the policy to two rules. Choose a location only after actually creating a child in it. And compare the local HEAD with origin/main's SHA before starting any work.
The first script chooses the location.
#!/usr/bin/env bash# pick_base.sh — return the first writable parent directory from the listset -uCANDIDATES=("/tmp/repos" "${TMPDIR:-/nonexistent}/repos" "$HOME/repos")for base in "${CANDIDATES[@]}"; do # mkdir -p succeeds even when the directory exists but is not writable. # So create and remove a real child to prove it can be written to mkdir -p "$base" 2>/dev/null || { echo "skip(mkdir): $base" >&2; continue; } probe="$base/.probe.$$" if mkdir "$probe" 2>/dev/null; then rmdir "$probe" echo "$base" exit 0 fi echo "skip(owner=$(stat -c %U "$base") mode=$(stat -c %a "$base")): $base" >&2doneecho "FATAL: no writable working location" >&2exit 1
Run in this environment, it skipped /tmp/repos with a reason and chose the one under TMPDIR.
Why not stop at mkdir -p? mkdir -p returns success for an existing directory, so it would have judged the unwritable /tmp/repos as usable. The skip(owner=…) line appearing instead of skip(mkdir) confirmed that behaviour here.
The second script clones and verifies.
#!/usr/bin/env bash# fresh_repo.sh <remote-url> <name> — prints the working directory pathset -euo pipefailREMOTE="$1"; NAME="$2"BASE=$("$(dirname "$0")/pick_base.sh")DIR="$BASE/$NAME"if [ -d "$DIR/.git" ]; then # Reuse an existing clone only if it is writable by me AND current if [ -w "$DIR/.git" ] && git -C "$DIR" pull -q --rebase origin main 2>/dev/null; then : else echo "stale or read-only: $DIR -> recreating" >&2 rm -rf "$DIR" 2>/dev/null || DIR="$BASE/$NAME.$$" fifi[ -d "$DIR/.git" ] || git clone -q --depth 1 --branch main "$REMOTE" "$DIR"# Always verify last: does local HEAD equal remote main?LOCAL=$(git -C "$DIR" rev-parse HEAD)REMOTE_SHA=$(git -C "$DIR" ls-remote origin main | cut -f1)[ "$LOCAL" = "$REMOTE_SHA" ] || { echo "FATAL: HEAD=$LOCAL origin/main=$REMOTE_SHA" >&2; exit 2; }echo "$DIR"
I put the check at the end because even after deciding not to borrow, silent pull failures remain (expired auth, a dropped network). With 2>/dev/null swallowing the error, the exit code alone would let a stale tree through. If the script refuses to return a directory until the SHAs match, work never starts from an old state.
Three steps at the top of a scheduled task
I placed the two scripts at the very start of the runbook, in this order.
Decide a writable location with pick_base.sh. If it fails, stop there and do not proceed.
Call fresh_repo.sh and store the returned path in a variable. The path is not returned until the SHAs match.
In every later step, use only that variable. I recommend leaving no fixed path such as /tmp/claudelab.net anywhere in the runbook text.
In my case the third step is the one I expect to matter most. If even one fixed path remains, an old step goes on reading it right after the way the location is chosen has been tidied up.
Pitfalls
Cleaning with rm -rf /tmp/repos/* does not work on what other runs created. Deletion is governed by write permission on the parent directory, not the target, so you cannot remove entries under a nobody-owned 755 directory. If your out-of-space procedure says "empty /tmp", rewrite it to remove only places you created.
Reading someone else's clone with git can be stopped by an ownership check. This time I added -c safe.directory='*' for that single call. I avoided writing it into the global config.
The token-in-URL approach is left to the REMOTE argument here. How to carry credentials is a separate matter; I wrote up the case where it stopped working after a redirect in another article. I mask the output with sed before reading logs so the token never appears.
Mixing up the working location can also come from looking at a different shell. The morning WS came back empty is written up here. The cause differs, but the idea of "one line to check first" is the same.
When reuse is fine, and when it is not
My decision assumes a repository around 50 MB and a handful of runs a day. Change those, and the answer changes.
If cloning takes tens of seconds or more (deep history, LFS), reuse earns its place. Even then, keep the two rules: put it where you can write, and compare SHAs.
If you clone every run, keep --depth 1. Pulling full history makes the 2.8 seconds a different story.
If runs overlap, giving each run its own name such as $BASE/$NAME.$$ avoids fights over a directory (the script above falls back to that form only when deletion fails).
One thing to do next: put pick_base.sh and the SHA check at the top of your scheduled task. From that day on, you no longer have to ask where today's working directory is.
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.