Skip to content

fix: stop env-var hosts from doubling $HOME in binary-resolver fallback#2056

Open
simjak wants to merge 1 commit into
garrytan:mainfrom
simjak:fix/env-var-host-binary-path-doubling
Open

fix: stop env-var hosts from doubling $HOME in binary-resolver fallback#2056
simjak wants to merge 1 commit into
garrytan:mainfrom
simjak:fix/env-var-host-binary-path-doubling

Conversation

@simjak

@simjak simjak commented Jun 19, 2026

Copy link
Copy Markdown

Fixes #2055.

Problem

Every non-Claude host that resolves binaries through $GSTACK_* env vars (codex, factory, cursor, opencode, slate, kiro, hermes, gbrain) ships a binary-resolver fallback that doubles $HOME, so the global-install fallback path never resolves. The skill reports NEEDS_SETUP / BROWSE_NOT_AVAILABLE / DESIGN_NOT_AVAILABLE even when the binary is built and present.

Reproduction

$ cd ~/.claude/skills/gstack && ./setup --host codex
$ grep -nE '\] && B=' ~/.codex/skills/gstack-qa/SKILL.md
861:[ -z "$B" ] && B="$HOME$GSTACK_BROWSE/browse"

$GSTACK_BROWSE is defined earlier in the same preamble as an absolute path:

GSTACK_ROOT="$HOME/.codex/skills/gstack"
GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"

so $HOME$GSTACK_BROWSE/browse expands to /Users/me/Users/me/.codex/skills/gstack/browse/dist/browse — a doubled $HOME that does not exist:

$ [ -e "$HOME$GSTACK_BROWSE/browse" ] && echo EXISTS || echo MISSING
MISSING
$ [ -e "$GSTACK_BROWSE/browse" ] && echo EXISTS || echo MISSING
EXISTS

The repo-local branch on the line above ($_ROOT/.agents/skills/.../browse) resolves first, so this only bites plain global installs, not repo-vendored ones — which is exactly the common Codex/Factory case.

Scope

18 generated skills, three patterns across the browse/design/make-pdf setup blocks:

B="$HOME$GSTACK_BROWSE/browse"
D="$HOME$GSTACK_DESIGN/design"
P="$HOME$GSTACK_MAKE_PDF/pdf"

Root cause

scripts/resolvers/{browse,design,make-pdf}.ts build the fallback as:

B="$HOME${ctx.paths.browseDir.replace(/^~/, '')}/browse"

This assumes browseDir is ~-rooted — true for Claude (~/.claude/skills/gstack/browse/dist), where stripping ~ and prepending $HOME is correct. But for env-var hosts buildHostPaths() (scripts/resolvers/types.ts) sets browseDir = "$GSTACK_BROWSE", so .replace(/^~/, '') is a no-op and $HOME is prepended to an already-absolute value.

Fix

One shared helper in scripts/resolvers/types.ts, applied at all six call sites:

export function toShellPath(dir: string): string {
  return dir.startsWith('~') ? `$HOME${dir.slice(1)}` : dir;
}
  • Claude (~/.claude/.../browse/dist) → $HOME/.claude/.../browse/dist — byte-identical to today.
  • Env-var hosts ($GSTACK_BROWSE) → passes through untouched → $GSTACK_BROWSE/browse.

Before / after (regenerated gstack-qa)

-[ -z "$B" ] && B="$HOME$GSTACK_BROWSE/browse"
+[ -z "$B" ] && B="$GSTACK_BROWSE/browse"

Verification

  • bun run scripts/gen-skill-docs.ts --host all0 remaining $HOME$GSTACK* occurrences across .agents/.factory/.opencode/.cursor.
  • Regenerated Codex $GSTACK_BROWSE/browse resolves to an existing binary.
  • Claude generated output unchanged (verified the qa/design/make-pdf fallback lines byte-for-byte).
  • tsc --noEmit clean.
  • bun test test/resolver-entry.test.ts test/host-config.test.ts make-pdf/test/270 pass, 0 fail (golden ship fixtures unaffected — ship has no binary-resolver block).

Call sites changed

  • scripts/resolvers/types.ts — add toShellPath()
  • scripts/resolvers/browse.ts:109
  • scripts/resolvers/design.ts:795,803,840
  • scripts/resolvers/make-pdf.ts:22

No generated/host output is committed (.agents/ et al. are gitignored); CI regeneration picks up the source change.

Non-Claude hosts (codex, factory, cursor, opencode, slate, kiro, hermes,
gbrain) resolve the browse/design/make-pdf binaries through absolute
$GSTACK_* env vars. The setup blocks built the global-install fallback as
`$HOME${dir.replace(/^~/, '')}/bin`, which assumes a ~-rooted dir. For
env-var hosts `dir` is already `$GSTACK_BROWSE` (absolute, contains $HOME),
so $HOME got prepended twice — e.g. `$HOME$GSTACK_BROWSE/browse` expands to
/home/me/home/me/.codex/.../browse and never resolves, leaving the skill
stuck on BROWSE_NOT_AVAILABLE / NEEDS_SETUP even when the binary is built.

Add toShellPath() in resolvers/types.ts: ~-rooted dirs expand via $HOME,
absolute env-var dirs pass through untouched. Applied at all six call
sites in browse.ts, design.ts, make-pdf.ts.

Claude output is byte-identical; env-var hosts now emit $GSTACK_BROWSE/browse.
tsc clean; resolver + make-pdf + golden host-config suites pass (270).
@trunk-io

trunk-io Bot commented Jun 19, 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.

Non-Claude hosts: binary-resolver fallback doubles $HOME ($HOME$GSTACK_BROWSE), browse/design/make-pdf never resolve

1 participant