Skip to content

fix(paths): shell-quote gstack-paths output so eval round-trips values - #2376

Open
fangearhq-boop wants to merge 1 commit into
garrytan:mainfrom
fangearhq-boop:fix/gstack-paths-eval-quoting
Open

fix(paths): shell-quote gstack-paths output so eval round-trips values#2376
fangearhq-boop wants to merge 1 commit into
garrytan:mainfrom
fangearhq-boop:fix/gstack-paths-eval-quoting

Conversation

@fangearhq-boop

Copy link
Copy Markdown

Fixes #2374.

Re-filed. The original PR (#2375) was closed automatically when its head fork was
deleted by mistake — GitHub closes any PR whose head repository goes away, and such a PR
can't be reopened. Same commit (0134f29e), same tests, no content changes. Apologies for
the duplicate notification.

The bug

bin/gstack-paths is consumed as eval "$(gstack-paths)" (the documented contract, used
at Step 0.6 of every skill), but emits bare KEY=VALUE lines with an unquoted right-hand
side. eval re-parses those: backslashes become escape sequences, spaces become word
separators.

On Windows $TMP is always a backslash path, so this fires every time:

TMP='C:\Users\me\AppData\Local\Temp'
eval "$(gstack-paths)"
echo "$TMP_ROOT"
# C:UsersmeAppDataLocalTemp      <- every separator gone

The mktemp "$TMP_ROOT/codex-err-XXXXXX.txt" that follows then fails, TMPERR is empty,
and 2>"$TMPERR" is a redirect to an empty filename, so the bash block exits 1 before
Codex ever runs
:

mktemp: failed to create file via template 'C:Users...Temp/codex-err-XXXXXX.txt': No such file or directory
bash: line 1: : No such file or directory
exit=1

A value containing a space is worse — eval word-splits it, prints
FilesTemp: command not found, and leaves the variable empty, so mktemp targets the
filesystem root and fails with Permission denied.

This is the same user-visible cascade as #2091 and #2370, both filed as macOS/BSD-mktemp
bugs. The root cause here is different, so those fixes don't help: a Windows user searching
the symptom finds them, applies them, and is still broken.

Not Windows-only — any POSIX TMPDIR containing a space or a backslash corrupts the same way.

The fix

Emit with printf %q so eval reproduces values byte-for-byte:

-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"

The script is already #!/usr/bin/env bash, so %q is available. Plain POSIX paths are
unchanged by %q, so existing callers and the existing assertions see identical output.

I also reworded the header comment. The old note said callers should quote expansions,
which doesn't cover this failure — the corruption happens during eval, before the
caller has a variable to quote. Callers here already quote correctly and were still broken.

Tests

Three eval-round-trip cases added to test/gstack-paths.test.ts: backslash, space, and
embedded single quote. They assert the full documented calling convention rather than the
raw stdout, which is what the existing tests check.

Verified they are real regression tests — against the old echo they fail, with %q they pass:

old echo printf %q
test/gstack-paths.test.ts 2 fail, 10 pass 12 pass, 0 fail
test/gstack-paths.test.ts + test/spec-template-invariants.test.ts 60 pass, 0 fail

(2 rather than 3 failures on the old code because the backslash case is skipped on this
Windows runner — see below. On Linux CI all three fail without the fix.)

Test-environment note worth flagging for review: the backslash case is skipped on win32.
MSYS/Git Bash rewrites both C:\.../c/... and backslashes → forward slashes in env
values, so a literal backslash cannot be injected through the environment on a Git Bash
runner. What it guards is pure eval semantics, so exercising it on Linux/macOS CI is
sufficient. This follows the same reasoning as the existing HOME-unset skips in that
file. The space and quote cases do run on Windows, and both catch the bug there.

The three test files that reference gstack-paths or TMP_ROOT are
test/gstack-paths.test.ts, test/spec-template-invariants.test.ts, and
test/skill-e2e-plan-tune-cathedral.test.ts (evals tier). The first two pass in full.
No templates are touched, so generated SKILL.md output is unaffected.

Alternative considered

Keeping echo and changing the contract to set -a; . <(gstack-paths) also fixes it and
preserves the plain human-readable output documented on line 4. I didn't take it because
it changes Step 0.6 in every skill template plus each host variant, where this is three
lines in one file. Happy to switch if you'd rather have the unescaped display form.

Found while dogfooding /codex review on Windows: with a literal TMP_ROOT=/tmp
substituted before the mktemp, /codex review works normally on 1.60.1.0, which isolates
this as the only blocker on that path.

`bin/gstack-paths` is consumed as `eval "$(gstack-paths)"`, but emitted bare
`KEY=VALUE` lines with an unquoted RHS. eval re-parses those, so backslashes
become escape sequences and spaces become word separators.

On Windows this breaks every skill. $TMP is a backslash path, so:

  TMP=C:\Users\me\AppData\Local\Temp
  eval "$(gstack-paths)"
  echo "$TMP_ROOT"   # C:UsersmeAppDataLocalTemp

The following `mktemp "$TMP_ROOT/codex-err-XXXXXX.txt"` fails, TMPERR is empty,
and `2>"$TMPERR"` is a redirect to an empty filename — so the bash block exits 1
before Codex ever runs and the user sees no review and no obvious cause. A value
containing a space (C:\Program Files\Temp) is worse: eval word-splits it,
prints `FilesTemp: command not found`, and leaves the variable empty.

This is the same user-visible cascade as garrytan#2091 / garrytan#2370, which are filed as
macOS/BSD-mktemp bugs. The root cause here is different, so those fixes don't
help — a Windows user finds them, applies them, and is still broken.

Not Windows-only: any POSIX TMPDIR containing a space or backslash corrupts the
same way.

Fix: emit with `printf %q` so eval reproduces values byte-for-byte. The script
is already `#!/usr/bin/env bash`, so %q is available. Plain POSIX paths are
unchanged by %q, so existing callers and tests see identical output.

The header's existing "callers should quote expansions" note did not cover this:
the corruption happens during eval, before the caller has a variable to quote.
Reworded to say what the emitter now guarantees.

Tests: three eval-round-trip cases (backslash, space, embedded quote) added to
test/gstack-paths.test.ts. They fail against the old `echo` and pass with %q.
The backslash case is skipped on win32 — MSYS rewrites backslashes to forward
slashes in env values, so it cannot be injected through the environment on a Git
Bash runner; the escape-eating it guards is pure eval semantics and is exercised
on Linux/macOS CI. Same reasoning as the existing HOME-unset skips in that file.

Fixes garrytan#2374

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Jul 28, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant