mcpbeat Sign in

CI Flaky Timing Luck Agent Skill

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.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill ci-flaky-timing-luck

The instruction itself

3 sections, as written by the author

Steps that passed by timing luck

Two failures with the same shape, both from a release pipeline:

  • A loop wrapped one disk image per architecture. Each iteration mounted a volume under the **same

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.

  • A build step downloaded a native archive through the standard library's URL stream. The download

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.

Traps

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.

Verifying it

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
  • a fixed path where a tool reports its own,
  • a stdlib fetch where an HTTP error is possible,
  • a bare 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.

How to use it

Copy the folder

Take maxrave-dev/ci-flaky-timing-luck from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

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.