Problem
.claude/skills/create-skill/SKILL.md mandates this temp-file pattern (Pattern 3, Pattern 4, Pattern 13, and the Phase 4 checklist):
mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX.ext"
Pattern 13 explicitly justifies it as portable: "Use mktemp with template syntax (mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX.ext") — GNU flags like --suffix and -p are not available on macOS BSD mktemp".
On macOS this does not work. BSD mktemp only substitutes the X placeholders when they are trailing. With a suffix after them it returns the template literally and creates a file with that literal name:
$ mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX.txt"
/var/folders/.../T//tmp.XXXXXXXXXX.txt # <-- not randomized
$ mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX.json" # second run, file now exists
mktemp: mkstemp failed on /var/folders/.../T//tmp.XXXXXXXXXX.json: File exists
$ mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX" # trailing X's
/var/folders/.../T//tmp.a4gpeJrPkl # correctly randomized
Impact
Every skill generated by /create-skill that follows the mandated pattern gets a fixed, non-unique temp path on macOS. This produces exactly the failure Pattern 4 exists to prevent:
- Concurrent sessions collide on the same literal path — a real risk given this repo's parallel-worktree workflow.
- Re-runs fail outright with
mkstemp failed ... File exists once the literal file is left behind by an earlier run.
- Failures are silent until the second run, so they look like flakes.
The pattern is currently reproduced in the guidance for Patterns 2, 3, 4, 13, 14, and Phase 5, so generated skills inherit it consistently.
Suggested fix
Use trailing Xs, and when an extension is genuinely required (e.g. codegraph's extension-based language detection, which is the stated reason for the suffix), create a temp directory and place the named file inside it:
# No extension needed:
TMP=$(mktemp "${TMPDIR:-/tmp}/myskill.XXXXXXXXXX")
# Extension required (codegraph language detection):
TMPDIR_WORK=$(mktemp -d "${TMPDIR:-/tmp}/myskill.XXXXXXXXXX")
trap 'rm -rf "$TMPDIR_WORK"' EXIT
PREV_FILE="$TMPDIR_WORK/prev.js"
Also worth adding a lint-skill.sh check that flags mktemp templates whose X run is not trailing — it would have caught this.
Notes
lint-skill.sh itself uses the broken form (mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX.blocks"), so the linter can fail on a re-run if the literal file survives.
- Found while authoring the
/fixer skill; that skill uses the trailing-X form instead.
- Platform: macOS (Darwin 25.2.0), BSD mktemp. GNU coreutils
mktemp does substitute non-trailing X's, which is likely why this went unnoticed on CI.
Problem
.claude/skills/create-skill/SKILL.mdmandates this temp-file pattern (Pattern 3, Pattern 4, Pattern 13, and the Phase 4 checklist):Pattern 13 explicitly justifies it as portable: "Use
mktempwith template syntax (mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX.ext") — GNU flags like--suffixand-pare not available on macOS BSDmktemp".On macOS this does not work. BSD
mktemponly substitutes theXplaceholders when they are trailing. With a suffix after them it returns the template literally and creates a file with that literal name:Impact
Every skill generated by
/create-skillthat follows the mandated pattern gets a fixed, non-unique temp path on macOS. This produces exactly the failure Pattern 4 exists to prevent:mkstemp failed ... File existsonce the literal file is left behind by an earlier run.The pattern is currently reproduced in the guidance for Patterns 2, 3, 4, 13, 14, and Phase 5, so generated skills inherit it consistently.
Suggested fix
Use trailing
Xs, and when an extension is genuinely required (e.g. codegraph's extension-based language detection, which is the stated reason for the suffix), create a temp directory and place the named file inside it:Also worth adding a
lint-skill.shcheck that flagsmktemptemplates whoseXrun is not trailing — it would have caught this.Notes
lint-skill.shitself uses the broken form (mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX.blocks"), so the linter can fail on a re-run if the literal file survives./fixerskill; that skill uses the trailing-X form instead.mktempdoes substitute non-trailing X's, which is likely why this went unnoticed on CI.