●RELEASE — Claude Code v2.1.248 shipped on August 27, followed a day later by v2.1.250, a one-line stabilizing release. It is the largest batch of changes this week●RESTRICTED — The new --restricted flag drops the built-in tools that run commands or code along with WebFetch, keeps file work inside the working directory, and refuses bypassPermissions●SECURITY — /ultrareview was uploading credential files such as prod.env, *.tfvars, and swap or backup copies like key.pem.tmp. Those now stay on your machine●TOKENS — The Workflow tool's description shrank from roughly 5.7k tokens to about 1k, with the script-writing reference moved into a bundled workflow-authoring skill●HARDWARE — Anthropic released the Model Hardware Standard as a research preview, a way to connect Claude to scientific, robotics, and manufacturing hardware●LIMITS — The 50% weekly limit increase runs through August 31 for Pro, Max, Team, and seat-billed Enterprise accounts, which leaves two days●RELEASE — Claude Code v2.1.248 shipped on August 27, followed a day later by v2.1.250, a one-line stabilizing release. It is the largest batch of changes this week●RESTRICTED — The new --restricted flag drops the built-in tools that run commands or code along with WebFetch, keeps file work inside the working directory, and refuses bypassPermissions●SECURITY — /ultrareview was uploading credential files such as prod.env, *.tfvars, and swap or backup copies like key.pem.tmp. Those now stay on your machine●TOKENS — The Workflow tool's description shrank from roughly 5.7k tokens to about 1k, with the script-writing reference moved into a bundled workflow-authoring skill●HARDWARE — Anthropic released the Model Hardware Standard as a research preview, a way to connect Claude to scientific, robotics, and manufacturing hardware●LIMITS — The 50% weekly limit increase runs through August 31 for Pro, Max, Team, and seat-billed Enterprise accounts, which leaves two days
The lock I left in a shared folder shut out every run after the first
A cloud-synced connected folder allows create, append, and rename, but refuses delete. Putting a single-instance lock there quietly disabled every unattended run after the first. Measurements for three lock styles, plus an implementation that picks a safe location.
A job I had queued the night before finished with a single line in the log. Not an error. Just the branch I had written myself: "another process is running, skipping this round."
No other process was running.
As an indie developer I hand more and more of the routine work to unattended scheduled runs, and several of those runs touch the same connected folder. Double execution was the one thing I wanted to rule out, so I added a lock. The mistake was where I put it — in the most reliably shared place I could think of, which was the connected folder itself.
The mutual exclusion worked. It worked so well that it never let go.
The locking part was fine
My first suspicion was the acquisition logic. If nothing was competing but the code said otherwise, the condition must be wrong. So I checked whether exclusion was happening at all.
# hold the lock for 3 seconds in one shell, then try a second one( flock -n 9 && sleep 3 ) 9> "$SHARED/slot.lock" &sleep 0.5( flock -n 9 && echo "second acquired (unexpected)" \ || echo "second blocked (as expected)" ) 9> "$SHARED/slot.lock"wait
The answer came back second blocked (as expected). flock behaves correctly on this folder. This was not the classic story of advisory locks failing over a network filesystem.
The problem was on the other side of the lock's life.
Only deletion was refused
I broke a file's life cycle into individual operations and ran each one in the same place: create, append, read, rename, make a directory, and remove.
A writability check passes here. Mine did. You can create, you can append, you can even rename, so as far as write permission goes nothing is wrong.
Deletion is the only refusal. And a single-instance lock is a mechanism that only works if you can delete it.
✦
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 tell, before the second unattended run, whether your lock style can actually be released where you put it
✦You will be able to swap mkdir locks, noclobber locks, and flock for a form that stays correct even when deletes are refused
✦You will be able to turn lock-caused skips into recorded outcomes instead of silent successes, so a job cannot sit dead for days unnoticed
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.
There are three common ways to implement a lock. I ran each of them twice in a row in the same connected folder. For unattended work, the second run is the one that matters.
# 1. mkdir lock (existence-based)for run in 1 2; do if mkdir "$SHARED/mkdir.lock" 2>/dev/null; then echo "run$run: acquired" rmdir "$SHARED/mkdir.lock" 2>/dev/null \ && echo "run$run: released" || echo "run$run: could not release" else echo "run$run: could not acquire" fidone
Style
First acquire
Exclusion
Release
Second run
mkdir lock
OK
works
refused
never acquires again
set -o noclobber lock
OK
works
refused
never acquires again
flock (file descriptor)
OK
works
not needed — closing the fd releases it
acquires normally (only the file lingers)
For the first two, the lock is the existence of a file or directory. Where deletion is refused, a lock taken once is never given back. The very first run seals the slot against every future run.
flock behaves differently because the exclusion lives on the open file descriptor, not on the file's existence. When the process exits, the descriptor closes and the lock is gone. Even though I could not delete the file, the next run acquired it without trouble. All that remains is one empty file.
That settled which style belongs in unattended work. If you have no choice but to store the lock somewhere undeletable, existence-based locks are off the table.
When migrating a job that already runs on an existence-based lock, this order kept production running for me.
Check whether deletion works where the lock currently lives. If it does, there is no urgency
Switch to flock and move only the location to local, run-private storage. Leave the acquisition logic alone
Confirm from the ledger's skip count that no old artifact is still locking the slot out
Reversing that order means carrying the leftovers into the new scheme, which makes diagnosis harder. I tried to slip a workaround in at this point and only added more state to verify. If you do adopt an existence-based lock, I recommend confirming first that deletion goes through where you plan to put it.
"Could not acquire" and "someone is running" are different facts
The nastier half of this was how the failure presented itself.
An existence-based lock cannot distinguish a live holder from a dead one. When mkdir fails, the only information you get is "it already exists." Whether that came from another process three seconds ago or from a leftover three weeks old is not recoverable.
And most implementations read that state as "the previous run must still be going" and exit cleanly. Exit code 0, one line in the log, no alert. That was exactly my morning: the work had stopped, but nothing in the record looked broken.
I considered writing a PID into the lock file to tell the two apart, but it does not help in this setup. Each unattended run happens in a separate sandbox, so comparing a recorded PID against the current process table is meaningless — you get a number that does not exist here.
For a moment I wanted to file this under environment bug. The more I looked, the more sense it made.
A connected folder is the real working folder you handed over. It holds drafts, settings, records. Letting an automated process create and append while routing deletion through a human decision is a clean line: the irreversible accidents mostly live on the delete side. Rename passing the same check fits the same logic. A renamed file can be found again; a deleted one cannot.
So the boundary was drawn from the direction of the accident it prevents. Not something to resent — something to design around.
That same premise shapes what belongs in a connected folder at all. When I counted what was actually sitting in mine before handing it over, the results are in I counted the keys in a folder before giving it to an AI: filenames found 5 of 18. What is safe to place there and what becomes unrecoverable the moment you place it are two separate questions.
Sort storage by lifetime, not by sharedness
Once the cause was clear, I changed the criterion for where things go from "is it shared?" to "how long should it live?"
What
Where
Why
Locks, temp files, working copies
Local, run-private storage
Created per run, and must disappear when the run ends
Run ledger, logs, deliverables
Connected folder (append only)
Kept to be read later; never needs deleting
Configuration, reference data
Connected folder (read only)
Humans update it, the job only reads it
Ledgers and logs are arguably safer in a place that refuses deletion. They are append-only anyway, so the restriction costs nothing. Appends to the connected folder went through without issue in my environment.
Locks are the opposite. Disappearing is part of their specification, so they cannot live somewhere that forbids it.
An implementation you can run
I made the script decide where the lock goes at runtime — not by checking existence, and not by checking writability, but by checking whether cleanup completes.
#!/usr/bin/env bash# Single-instance lock for unattended runs.# Never place the lock anywhere that refuses deletion.set -uo pipefailSLOT="${1:?slot name required}" # e.g. nightly-reportSHARED_LEDGER="${SHARED_LEDGER:-}" # append-only; a connected folder is fine# Candidate lock roots, private and local firstlock_root_candidates() { printf '%s\n' \ "${XDG_RUNTIME_DIR:-}" \ "/run/user/$(id -u 2>/dev/null || echo 0)" \ "${TMPDIR:-/tmp}" \ "$HOME/.cache"}# Verify that both creation and removal go through.# The probe uses a fixed name so it cannot pile up per run.supports_full_lifecycle() { local dir="$1" probe if [ -z "$dir" ] || [ ! -d "$dir" ] || [ ! -w "$dir" ]; then return 1; fi probe="$dir/.lifecycle_probe" # A leftover probe means this location already failed the test if [ -e "$probe" ]; then return 1; fi if ! mkdir "$probe" 2>/dev/null; then return 1; fi if ! rmdir "$probe" 2>/dev/null; then return 1; fi return 0}pick_lock_root() { local d while IFS= read -r d; do if supports_full_lifecycle "$d"; then printf '%s\n' "$d"; return 0; fi done < <(lock_root_candidates) return 1}LOCK_ROOT="$(pick_lock_root)" || { echo "FAILED: no location accepts a lock (nothing allows deletion)" >&2 exit 3}LOCK_FILE="$LOCK_ROOT/unattended-$SLOT.lock"exec 9> "$LOCK_FILE" || exit 3if ! flock -n 9; then echo "SKIPPED: $SLOT is held by another process" >&2 [ -n "$SHARED_LEDGER" ] && printf '%s\t%s\t%s\t%s\n' \ "$(date -Iseconds)" "$SLOT" "$$" "skipped" >> "$SHARED_LEDGER" exit 2 # do not exit 0fitrap 'flock -u 9 2>/dev/null; rm -f "$LOCK_FILE" 2>/dev/null' EXIT[ -n "$SHARED_LEDGER" ] && printf '%s\t%s\t%s\t%s\n' \ "$(date -Iseconds)" "$SLOT" "$$" "acquired" >> "$SHARED_LEDGER"# ---- your actual work goes here ----[ -n "$SHARED_LEDGER" ] && printf '%s\t%s\t%s\t%s\n' \ "$(date -Iseconds)" "$SLOT" "$$" "released" >> "$SHARED_LEDGER"exit 0
Throwing a second run at it while the first holds the lock ends like this:
SKIPPED: nightly-report is held by another process
second run exit=2
One thing only became obvious after I had written it. In a location that refuses cleanup, this probe cannot clean up its own probe. The routine for detecting "cleanup does not work here" needs cleanup — a slightly comic arrangement.
That is why the probe name is fixed. A rejected location keeps exactly one probe artifact, and the next run sees it, creates nothing, and returns the same verdict. Leaving one artifact for the lifetime of the folder is a very different thing from leaving one per run.
Splitting the exit codes was about eliminating windows where nothing is heard.
Exit code
Meaning
How to treat it
0
The work completed
Check both the deliverable and the log
2
Skipped because a live lock was held
Record it. A run of these is an anomaly
3
No usable location for a lock
Fail immediately. This is an environment problem
Counting ledger lines surfaces trouble quickly. An acquired with no matching released means a run grabbed the lock and died holding it. A wall of skipped with nothing holding anything means the job has been standing aside for no reason.
awk -F'\t' ' $4=="acquired" { open[$3]++ } $4=="released" { open[$3]-- } $4=="skipped" { skip++ } END { for (p in open) if (open[p] > 0) print "unreleased:", p print "skips:", skip+0 }' "$SHARED_LEDGER"
I fold this output into my morning check. If the number reaches double digits, I treat that slot as not running.
What I deliberately did not automate
A few things I chose to leave out, and why.
There is no stale-lock breaker. With flock you do not need one, because the lock dies with the process. Adding a breaker means adding something that can misfire and permit double execution. Capabilities you do not need are safer left out.
I do not auto-clean the connected folder either. Routing deletion through human judgement is the design; working around it for my own convenience defeats the point. One empty probe artifact remains for the lifetime of the folder, and deciding whether to remove it is a decision I can make myself.
And I leave other sessions' files alone. Another unattended run may be active on the same machine, and its temp files are not mine to tidy.
One thing worth checking today
If you have anything running unattended, there is a single check worth doing now. In the directory where your lock file lives, try one delete.
: > "$(dirname "$YOUR_LOCK_FILE")/.rm_check" && \ rm -f "$(dirname "$YOUR_LOCK_FILE")/.rm_check" && \ echo "deletion works here" || echo "do not put a lock in this location"
If it passes, your current setup is fine. If it does not, there is a real chance the second run never started. It cost me a night and a bit to work that out, and I would be glad if it saved someone else the same evening.
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.