Summary
On Windows (git-bash/MSYS), eval "$(gstack-paths)" corrupts TMP_ROOT before any skill
gets to use it. $TMP on Windows is a backslash path like C:\Users\me\AppData\Local\Temp,
and bin/gstack-paths emits it as a bare, unquoted assignment. eval then consumes the
backslashes as escape sequences, so the caller ends up with C:UsersmeAppDataLocalTemp.
Every skill that does mktemp "$TMP_ROOT/..." after Step 0.6 then fails, and the failure
cascades into exactly the same "empty TMPERR → : No such file or directory" symptom
already documented for macOS in #2091 — but from a different root cause, so the fix there
does not help here.
Environment
- gstack: 1.60.1.0 (
a3259400)
- OS: Windows 11 Pro 26200,
MINGW64_NT-10.0-26200
- bash: GNU bash 5.2.37(1)-release (x86_64-pc-msys)
TMPDIR: unset (normal on Windows)
TMP: C:\Users\me\AppData\Local\Temp
Reproduction
P=~/.claude/skills/gstack/bin/gstack-paths
# The script's own output is correct:
env -u TMPDIR TMP='C:\Users\test\AppData\Local\Temp' bash "$P" | grep TMP_ROOT
# TMP_ROOT=C:\Users\test\AppData\Local\Temp
# ...but the documented usage, eval, corrupts it:
env -u TMPDIR TMP='C:\Users\test\AppData\Local\Temp' bash -c 'eval "$('"$P"')"; echo "[$TMP_ROOT]"'
# [C:UserstestAppDataLocalTemp] <-- every separator gone
# A path with a space is worse — eval word-splits it and TMP_ROOT ends up empty:
env -u TMPDIR TMP='C:\Program Files\Temp' bash -c 'eval "$('"$P"')"; echo "[$TMP_ROOT]"'
# bash: line 3: FilesTemp: command not found
# []
Downstream impact
skills/codex/SKILL.md Step 2A (and every other skill using $TMP_ROOT) does:
TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")
... codex review "..." 2>"$TMPERR"
With the corrupted value:
mktemp: failed to create file via template 'C:UserstestAppDataLocalTemp/codex-err-XXXXXX.txt': No such file or directory
bash: line 1: : No such file or directory
exit=1
mktemp fails, TMPERR is empty, and 2>"$TMPERR" is a redirect to an empty filename, so
Codex never runs at all. The user sees an immediate exit 1 with no review and no
obvious cause. In the C:\Program Files case TMP_ROOT is empty instead, so mktemp
targets the filesystem root and fails with Permission denied.
This is the same user-visible failure as #2091 / #2370, which is worth noting because
those are both filed as macOS/BSD-mktemp problems — a Windows user hitting this will find
them, try the BSD fix, and still be broken.
Root cause
bin/gstack-paths ends with:
echo "GSTACK_STATE_ROOT=$_state_root"
echo "PLAN_ROOT=$_plan_root"
echo "TMP_ROOT=$_tmp_root"
The documented contract is eval "$(gstack-paths)", so these lines are re-parsed by the
shell. An unquoted RHS means backslashes are escapes and spaces are word separators.
The header comment does anticipate hostile values:
Security: output values are not sanitized — callers may receive paths with shell-special
characters if env vars contain them. Skills should always quote expansions
("$GSTACK_STATE_ROOT", not $GSTACK_STATE_ROOT).
That mitigation does not apply to this failure. The corruption happens during eval,
before the caller has any variable to quote. Callers already quote correctly and are still
broken. Note this is not only a Windows concern: any POSIX TMPDIR containing a space or
backslash corrupts the same way.
Proposed fix
Emit shell-quoted assignments so eval round-trips the exact value. printf %q is a bash
builtin and the script is already #!/usr/bin/env bash:
-echo "GSTACK_STATE_ROOT=$_state_root"
-echo "PLAN_ROOT=$_plan_root"
-echo "TMP_ROOT=$_tmp_root"
+printf 'GSTACK_STATE_ROOT=%q\n' "$_state_root"
+printf 'PLAN_ROOT=%q\n' "$_plan_root"
+printf 'TMP_ROOT=%q\n' "$_tmp_root"
Verified round-trip through eval on all four cases:
| Input |
Result |
C:\Users\test\AppData\Local\Temp |
PASS |
C:\Program Files\Temp |
PASS |
/tmp/o'brien |
PASS |
/plain/posix/path |
PASS |
One tradeoff worth a maintainer decision: the header also documents a plain
non-eval usage (gstack-paths → prints GSTACK_STATE_ROOT=...). With %q, POSIX paths
print unchanged, but a Windows path prints escaped (C:\\Users\\...). If that
human-readable form matters, an alternative is keeping echo and changing the contract to
set -a; . <(gstack-paths), though that is a wider change across every skill's Step 0.6.
Notes
Found while dogfooding /codex review on Windows. Working around it with a literal
TMP_ROOT=/tmp before the mktemp makes /codex review work normally on 1.60.1.0,
which isolates this as the only blocker on that path.
Related but distinct: #2091, #2370 (same cascade, BSD mktemp root cause, macOS),
#1782 (gstack-config truncating paths at the first space — same unquoted-path family).
Summary
On Windows (git-bash/MSYS),
eval "$(gstack-paths)"corruptsTMP_ROOTbefore any skillgets to use it.
$TMPon Windows is a backslash path likeC:\Users\me\AppData\Local\Temp,and
bin/gstack-pathsemits it as a bare, unquoted assignment.evalthen consumes thebackslashes as escape sequences, so the caller ends up with
C:UsersmeAppDataLocalTemp.Every skill that does
mktemp "$TMP_ROOT/..."after Step 0.6 then fails, and the failurecascades into exactly the same "empty
TMPERR→: No such file or directory" symptomalready documented for macOS in #2091 — but from a different root cause, so the fix there
does not help here.
Environment
a3259400)MINGW64_NT-10.0-26200TMPDIR: unset (normal on Windows)TMP:C:\Users\me\AppData\Local\TempReproduction
Downstream impact
skills/codex/SKILL.mdStep 2A (and every other skill using$TMP_ROOT) does:With the corrupted value:
mktempfails,TMPERRis empty, and2>"$TMPERR"is a redirect to an empty filename, soCodex never runs at all. The user sees an immediate exit 1 with no review and no
obvious cause. In the
C:\Program FilescaseTMP_ROOTis empty instead, somktemptargets the filesystem root and fails with
Permission denied.This is the same user-visible failure as #2091 / #2370, which is worth noting because
those are both filed as macOS/BSD-mktemp problems — a Windows user hitting this will find
them, try the BSD fix, and still be broken.
Root cause
bin/gstack-pathsends with:The documented contract is
eval "$(gstack-paths)", so these lines are re-parsed by theshell. An unquoted RHS means backslashes are escapes and spaces are word separators.
The header comment does anticipate hostile values:
That mitigation does not apply to this failure. The corruption happens during
eval,before the caller has any variable to quote. Callers already quote correctly and are still
broken. Note this is not only a Windows concern: any POSIX
TMPDIRcontaining a space orbackslash corrupts the same way.
Proposed fix
Emit shell-quoted assignments so
evalround-trips the exact value.printf %qis a bashbuiltin and the script is already
#!/usr/bin/env bash:Verified round-trip through
evalon all four cases:C:\Users\test\AppData\Local\TempC:\Program Files\Temp/tmp/o'brien/plain/posix/pathOne tradeoff worth a maintainer decision: the header also documents a plain
non-eval usage (
gstack-paths→ printsGSTACK_STATE_ROOT=...). With%q, POSIX pathsprint unchanged, but a Windows path prints escaped (
C:\\Users\\...). If thathuman-readable form matters, an alternative is keeping
echoand changing the contract toset -a; . <(gstack-paths), though that is a wider change across every skill's Step 0.6.Notes
Found while dogfooding
/codex reviewon Windows. Working around it with a literalTMP_ROOT=/tmpbefore themktempmakes/codex reviewwork normally on 1.60.1.0,which isolates this as the only blocker on that path.
Related but distinct: #2091, #2370 (same cascade, BSD
mktemproot cause, macOS),#1782 (
gstack-configtruncating paths at the first space — same unquoted-path family).