Find and fix CI steps that only ever passed by timing luck — an asynchronous detach of a same-name mounted volume colliding with the next iteration's mount, and a downloader that quietly saves an error page as the artifact; reach for it when a step that ran green for months starts failing after a runner image update, or when a job succeeds and something minutes later fails on a corrupt or empty file it was handed.
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill ci-flaky-timing-luck
Two failures with the same shape, both from a release pipeline:
name**. Releasing a volume is asynchronous, so the next mount either failed as busy or — worse —
quietly mounted under a suffixed name while the script kept writing to the hardcoded path.
answered with a redirect to a mirror; on a cross-protocol redirect the stream API saved the
error page body as the target file and returned normally.
Neither is a race you introduced. Both are steps that were always wrong and were being covered
by how fast the machine happened to be. A runner image update changes the timing, and they start
failing — which is why the commit that "breaks" them is usually unrelated.
**The expensive shape is not "fails intermittently", it is "succeeds while producing the wrong
artifact."** A hard failure costs one re-run. A step that reports success and hands the next job a
zero-byte file, an error page, or an empty mount point costs an afternoon, and the traceback points
somewhere it did not come from.
Never hardcode a mount point — capture the one the tool reports. The tool tells you where it
actually mounted; a name collision changes that answer and nothing else warns you:
# adapted — condensed from the wrapping script, names genericised
MOUNT_POINT="$(hdiutil attach -nobrowse -noautoopen "$RW_DMG" 2>/dev/null \
| grep -Eo '/Volumes/.*$' | tail -1 || true)"
[[ -d "$MOUNT_POINT" ]] || { echo "Could not mount $RW_DMG" >&2; exit 1; }
VOL_NAME="$(basename "$MOUNT_POINT")"
Everything downstream then derives from $MOUNT_POINT and $VOL_NAME. A script that writes to
/Volumes/<Name> after a silent remount to /Volumes/<Name> 1 writes into the *previous*
iteration's volume and produces an output missing whatever it thought it added.
Clean up before you start, not only after you finish. The previous iteration's cleanup may not
have completed, and a cleanup you run only at the end never runs at all if the body failed:
# adapted — condensed from the wrapping script, names genericised
detach_stale_volumes() {
for v in /Volumes/<Name> /Volumes/<Name>\ *; do
[[ -e "$v" ]] && hdiutil detach -force "$v" >/dev/null 2>&1 || true
done
}
detach_stale_volumes
for attempt in 1 2 3 4 5; do
MOUNT_POINT="$(hdiutil attach … | grep -Eo '/Volumes/.*$' | tail -1 || true)"
[[ -d "$MOUNT_POINT" ]] && break
detach_stale_volumes; sleep 3
done
Note the glob covers the suffixed names too — those are exactly the leftovers a previous collision
created.
Force and retry the release as well. The next iteration starts from whatever this one left
behind, so a best-effort detach at the end of the body is worth a short retry loop.
Prefer a downloader that fails loudly on HTTP errors. The stdlib stream API does not follow a
redirect that switches protocol — it hands back the redirect response itself, and copying that
stream saves the redirect's own HTML page to disk under the artifact's filename. The download looks
like it worked; the checksum is the only thing left to catch it. An external downloader with
explicit flags fails instead:
// adapted
ProcessBuilder(
"curl", "-fsSL", // --fail: non-zero on HTTP errors instead of saving the error body
// -L: follows redirects across protocols and mirrors
"--retry", "5", "--retry-delay", "5",
"--retry-all-errors", // plain --retry does NOT cover a mid-transfer receive failure;
// it retries only HTTP 5xx/408/429 and connection errors
"-o", target.absolutePath, url,
).inheritIO().start().waitFor()
Check the result even after a zero exit, and delete the partial file. Otherwise an empty or
truncated download is cached and every retry reproduces it:
check(exit == 0 && target.exists() && target.length() > 0) {
if (target.exists()) target.delete()
"download failed (exit $exit) for $url"
}
Mirrors are a lottery, so verify content rather than trusting the source. A release URL can
redirect to a different mirror on every request. Pair the download with a digest check against a
value pinned in your source — that is the only assertion that survives a mirror serving something
else.
A cached artifact is a cached failure. Any caching layer in front of a download will happily
preserve a bad file forever. Make the failure path delete, and make the cache key include whatever
identifies the *publication*, not just the version.
Timing-luck steps have tells you can grep for. In shell steps and build scripts, look for:
grep -rn 'Volumes/\|/mnt/\|/tmp/[a-z-]*/' --include='*.sh' --include='*.yml' scripts .github
grep -rn 'openStream\|URL(\|readBytes()' --include='*.kts' --include='*.gradle' .
grep -rn 'sleep ' --include='*.sh' --include='*.yml' scripts .github
sleep standing in for a condition — it is a timing assumption with a comment missing.Then ask of each: if this step did the wrong thing, would the job still be green? Every yes is
a place to add an assertion on the artifact — its existence, its size, its type, its digest — right
where it is produced, rather than letting a later job discover it.
Take maxrave-dev/ci-flaky-timing-luck from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.