Skip to content

fix(host-paths): use $HOME instead of ~ in non-env-var host paths#1451

Open
jisulee42 wants to merge 1 commit into
garrytan:mainfrom
jisulee42:fix/claude-host-bindir-tilde-quoting
Open

fix(host-paths): use $HOME instead of ~ in non-env-var host paths#1451
jisulee42 wants to merge 1 commit into
garrytan:mainfrom
jisulee42:fix/claude-host-bindir-tilde-quoting

Conversation

@jisulee42
Copy link
Copy Markdown

@jisulee42 jisulee42 commented May 12, 2026

Summary

HOST_PATHS for Claude (and any future non-env-var host) emits paths
starting with a literal ~. Several preamble blocks then interpolate
those paths inside double quotes (e.g. generate-brain-sync-block.ts:45
emits _BRAIN_SYNC_BIN="${ctx.paths.binDir}/gstack-brain-sync").
POSIX shells do not expand ~ inside double quotes, so the emitted
line is a literal "~/.claude/.../gstack-brain-sync" and every call to
that binary fails with exit 127, No such file or directory. Both
stderr and the exit code are swallowed by 2>/dev/null || true, so
the failure is silent.

Practical impact: on every Claude Code install with artifacts_sync_mode
on, the skill preambles call gstack-brain-sync --once on every skill
boundary, but the binary is never reached. The artifacts queue
(~/.gstack/.brain-queue.jsonl) grows indefinitely while
~/.gstack/.brain-last-push stays at never. I hit this exact symptom
locally after running /setup-gbrain — ~24 hours of skill usage, queue
at 45 entries, last-push never, status file never created. Manual
gstack-brain-sync --once drained the queue and pushed cleanly, which
confirmed the binary itself works and the problem was upstream of the
call.

codex and factory hosts are unaffected: they use $GSTACK_BIN,
which DOES expand inside double quotes.

Root cause

scripts/resolvers/types.ts:37 (before):

} else {
  const root = `~/${config.globalRoot}`;   // literal ~
  paths[config.name] = {
    skillRoot: root,
    binDir: `${root}/bin`,
    ...
  };
}

The emitter at scripts/resolvers/preamble/generate-brain-sync-block.ts:45
then wraps that value in double quotes, defeating tilde expansion.

Fix

One-line: emit $HOME/${config.globalRoot} instead of ~/${config.globalRoot}.
Same semantics outside quotes (both expand), correct semantics inside
quotes (only $HOME expands). Added a WHY comment pointing at bash(1)
"Tilde Expansion" + "Parameter Expansion" so the next person who looks
at this knows why it matters.

Why CI didn't catch it

Two reasons, both worth fixing:

  1. test/host-config.test.ts:246 asserted the broken form as the
    expected value:

    expect(HOST_PATHS.claude.binDir).toBe('~/.claude/skills/gstack/bin');

    The test was passing because the code produced the same broken value
    the test was asserting. Updated to expect $HOME/....

  2. test/fixtures/golden/claude-ship-SKILL.md was generated from
    the broken code path, so golden-file regression was comparing the
    bug against itself. Regenerated via bun run gen:skill-docs --host all.

Also added a narrow regression guard at
test/host-config.test.ts (HOST_PATHS describe block):

test('non-env-var binDirs are safe to interpolate inside double quotes (no leading ~)', () => {
  for (const config of ALL_HOST_CONFIGS) {
    if (!config.usesEnvVars) {
      expect(HOST_PATHS[config.name].binDir.startsWith('~')).toBe(false);
      // ... + skillRoot/browseDir/designDir/makePdfDir
    }
  }
});

If anyone reintroduces the literal-tilde form later, this fires
immediately.

Diff shape

  • scripts/resolvers/types.ts — +6 / -1 (one-line fix + WHY comment).
  • test/host-config.test.ts — +20 / -8 (assertion updates + regression
    guard).
  • test/fixtures/golden/claude-ship-SKILL.md — +57 / -57 (golden
    regen, ~$HOME).
  • 42 Claude-host */SKILL.md files — auto-regenerated via
    bun run gen:skill-docs --host all. Codex/Factory artifacts
    unchanged.

The 42 SKILL.md files dominate the diff line count but are mechanical
regen output. Reviewer focus: scripts/resolvers/types.ts (6 lines)
and test/host-config.test.ts (~30 lines).

Test plan

  • bun test test/host-config.test.ts — 74 pass, 0 fail (includes
    new regression guard).
  • bun test test/gen-skill-docs.test.ts test/brain-sync.test.ts
    412 pass, 0 fail.
  • bun test test/preamble-compose.test.ts test/gstack-gbrain-sync.test.ts test/gbrain-sources.test.ts
    34 pass, 0 fail.
  • bun run test (main suite, e2e excluded) — green locally.
  • Manual verification of the original symptom: gstack-brain-sync --once
    now reachable, queue drains, push succeeds, ~/.gstack/.brain-last-push
    updates.

Notes for maintainer

I found this while debugging "/sync-gbrain isn't actually pushing
anything" on a fresh /setup-gbrain install. Happy to split into
two PRs (fix vs. regenerated artifacts) if you'd prefer the diff
to land more narrowly, but the regen is verbatim output of
bun run gen:skill-docs --host all against the one-line fix and
should be deterministic. Searched open issues/PRs for prior reports
of this — none found.

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

`HOST_PATHS` for Claude (and any future host with `usesEnvVars=false`) was
built from `~/${config.globalRoot}`. The resulting binDir is then
interpolated into double-quoted bash strings by several preamble emitters,
e.g. `generate-brain-sync-block.ts:45`:

  _BRAIN_SYNC_BIN="${ctx.paths.binDir}/gstack-brain-sync"

POSIX shells do NOT expand `~` inside double quotes. The emitted line
becomes a literal `"~/.claude/.../gstack-brain-sync"`, which fails with
`exit 127, No such file or directory` when invoked. Both stderr and the
exit code are suppressed by `2>/dev/null || true`, so the failure is
silent: skill preambles call `gstack-brain-sync --once` on every skill
boundary, but it never runs. The artifacts queue (~/.gstack/.brain-queue.jsonl)
grows indefinitely while last-push stays at "never". Codex/Factory hosts
are unaffected because they use `$GSTACK_BIN`, which DOES expand inside
double quotes.

Fix: emit `$HOME/${config.globalRoot}` instead. Identical semantics outside
quotes, correct semantics inside quotes.

Why CI didn't catch it:
- `test/host-config.test.ts:246` asserted the broken form as the
  expected value (`expect(...).toBe('~/.claude/skills/gstack/bin')`).
- The Claude golden fixture (`test/fixtures/golden/claude-ship-SKILL.md`)
  was generated from the broken code path, so the regression suite was
  comparing the bug against itself.

Changes:
- `scripts/resolvers/types.ts`: emit `$HOME/...` for non-env-var hosts
  (one-line change + WHY comment).
- `test/host-config.test.ts`: update existing assertions to expect
  `$HOME/...`. Add a narrow regression guard that asserts no non-env-var
  host's binDir/skillRoot/browseDir/designDir/makePdfDir begins with `~`
  (i.e., is safe to interpolate inside double quotes).
- Regenerated artifacts via `bun run gen:skill-docs --host all`:
  42 Claude-host SKILL.md files + the claude-ship golden fixture flip
  from `~/...` to `$HOME/...`. Codex/Factory artifacts unchanged.

Test plan:
- `bun test test/host-config.test.ts` — 74 pass, 0 fail (includes new guard).
- `bun test test/gen-skill-docs.test.ts test/brain-sync.test.ts` —
  412 pass, 0 fail.
- `bun run test` (main suite, e2e excluded) — green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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