Skip to content

Fix: resolveSlug silently returns "unknown" on Windows - #2367

Open
benjaminberes-bp wants to merge 2 commits into
garrytan:mainfrom
benjaminberes-bp:fix/windows-resolve-slug
Open

Fix: resolveSlug silently returns "unknown" on Windows#2367
benjaminberes-bp wants to merge 2 commits into
garrytan:mainfrom
benjaminberes-bp:fix/windows-resolve-slug

Conversation

@benjaminberes-bp

Copy link
Copy Markdown

On Windows, resolveSlug() never resolves a slug. It returns the literal
"unknown", with exit code 0 and no output.

bin/gstack-slug is an extension-less bash script. spawnSync without a shell
goes through CreateProcess, which does not honour shebangs: the call fails
with ENOENT, stdout is null, the SLUG= regex does not match, and the
: "unknown" branch is taken.

// Windows, git-bash, bun 1.3.14
const { spawnSync } = require("child_process");
const r = spawnSync("C:/Users/<u>/.claude/skills/gstack/bin/gstack-slug",
                    { encoding: "utf-8" });
console.log(r.error && r.error.code, JSON.stringify(r.stdout));
// ENOENT null

Why this is worse than a wrong path

Both consumers, bin/gstack-decision-log and bin/gstack-decision-search,
then work against ~/.gstack/projects/unknown/.

  • Write side: one anonymous bucket shared by every repo on the machine.
  • Read side: the directory does not exist, so the search returns an empty
    list and exits 0. The session reads "no prior decisions" and re-litigates
    settled calls in good faith.

Observed on a real project: gstack-decision-search --recent 5 printed nothing
while ~/.gstack/projects/<slug>/decisions.active.json held 3930 bytes of
decisions at the correct path. The reader was simply looking elsewhere.

The : "unknown" fallback turned an exec failure into a plausible value, which
is why this went unnoticed rather than surfacing as an error.

The fix

shell: true does not work — cmd.exe has no association for an
extension-less file and yields "unknown" too. Naming the interpreter does.

  • Retry via bash <script> when the direct call yields no slug. The direct call
    is still attempted first, so POSIX behaviour is unchanged. The retry is
    keyed on "no slug parsed" rather than on ENOENT specifically, so a shim that
    exits non-zero without output is covered as well. bash and not sh:
    gstack-slug uses [[ ]] and set -o pipefail.
  • Warn on stderr before falling back. The fallback value is kept so no caller
    breaks, but a tooling failure is not an anonymous project.

Tests

test/bin-context-resolve-slug.test.ts pins both halves — that an
extension-less shebang script resolves on every platform, and that a failure is
audible. Verified failing against the pre-fix function and passing after it.

Notes

gitBranch() in the same file is unaffected: it spawns git, which Windows
resolves via PATHEXT. Only extension-less scripts break, so an audit could
look for other spawnSync calls targeting bin/gstack-*.

Verified on Windows 11 / git-bash / bun 1.3.14. Happy to adjust the approach if
you prefer a process.platform check over the retry-on-empty.

benjaminberes-bp and others added 2 commits July 27, 2026 14:58
`gstack-slug` is an extension-less bash script. On Windows, spawnSync
without a shell goes through CreateProcess, which does not honour shebangs:
the call fails with ENOENT, stdout is null, the SLUG regex does not match,
and resolveSlug returns the literal "unknown".

Repro (Windows, git-bash, bun):

    bun -e 'const {spawnSync}=require("child_process");
      const r=spawnSync("C:/Users/<u>/.claude/skills/gstack/bin/gstack-slug",
                        {encoding:"utf-8"});
      console.log(r.error&&r.error.code, JSON.stringify(r.stdout))'
    # ENOENT null

The impact is worse than a wrong path. Both consumers — gstack-decision-log
and gstack-decision-search — then read and write ~/.gstack/projects/unknown/.
On the write side that is a single anonymous bucket shared by every repo on
the machine. On the read side the directory does not exist, so the search
returns an empty list and exits 0: the session is told there are no prior
decisions and re-litigates settled calls in good faith. The `?? "unknown"`
fallback turned an exec failure into a plausible value, which is why this
went unnoticed for so long.

`shell: true` does not fix it — cmd.exe has no association for an
extension-less file and yields "unknown" too. Naming the interpreter does.

Two changes:

- Retry via `bash <script>` when the direct call yields no slug. The direct
  call is still attempted first, so POSIX behaviour is unchanged. The retry
  is keyed on "no slug parsed" rather than on ENOENT specifically, so a shim
  that exits non-zero without output is covered as well. bash and not sh:
  gstack-slug uses `[[ ]]` and `set -o pipefail`.

- Warn on stderr before falling back. The fallback value is kept for
  compatibility, but a tooling failure is not an anonymous project, and a
  mute fallback is what allowed this to live.

Verified on Windows 11 / git-bash / bun 1.3.14:

- nominal: resolveSlug returns benjaminberes-bp-alfred-agent
- failure: warns `direct: ENOENT; via bash: exited 127` and still returns
  "unknown", so no caller breaks
- end to end: `gstack-decision-search --recent 5` goes from empty to 4
  records against an existing 3930-byte decisions.active.json

gitBranch() in the same file is unaffected: it spawns `git`, which Windows
resolves via PATHEXT. Only extension-less scripts break, so an audit should
look for spawnSync calls targeting bin/gstack-*.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pins both halves of the bug: an extension-less shebang script must resolve on
every platform, and a resolution failure must be audible.

Verified to fail against the pre-fix function (both cases) and pass after it.
The first case is the one that reproduces only on Windows; the second holds
everywhere, which is what keeps the silent-fallback regression from returning
on any platform.

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

trunk-io Bot commented Jul 27, 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.

1 participant