Skip to content

Windows: eval "$(gstack-paths)" strips backslashes from TMP_ROOT — mktemp fails and skills exit before Codex runs #2374

Description

@fangearhq-boop

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions