fix(browse,design): resolve ~/.gstack via os.homedir() on Windows#1120
Open
scarson wants to merge 1 commit intogarrytan:mainfrom
Open
fix(browse,design): resolve ~/.gstack via os.homedir() on Windows#1120scarson wants to merge 1 commit intogarrytan:mainfrom
scarson wants to merge 1 commit intogarrytan:mainfrom
Conversation
31 sites across 6 files constructed paths for `~/.gstack/` state, chromium
profile, ngrok env, sidebar session dirs, worktrees, agent queue, openai
config, etc., using one of these patterns:
path.join(process.env.HOME || '/tmp', '.gstack', ...) (20 sites)
path.join(process.env.HOME || '', '.claude/skills/...', ...) (8 sites)
path.join(process.env.HOME!, '.gstack/openai.json') (1 site — non-null assertion)
path.join(process.env.HOME || "~", '.gstack', 'openai.json') (1 site — literal "~" never expands)
process.env.HOME || process.env.USERPROFILE || '/tmp' (1 site — already platform-aware)
On Windows, `process.env.HOME` is NOT set by default. Windows uses
`USERPROFILE` for the same purpose. Git Bash happens to set HOME, so
gstack state currently lands correctly when a user runs commands from a
Git Bash shell — which is the only supported dev path, since `./setup`
is a bash script. But:
- Compiled binaries (`browse.exe`, etc.) spawn detached subprocesses
(the server, the sidebar agent, chromium) with the environment of the
calling shell. When a user runs `browse.exe` from cmd.exe or PowerShell
(or from an IDE that doesn't inherit Git Bash's env), HOME is unset.
- `path.join('/tmp', '.gstack', 'x')` resolves to `\tmp\.gstack\x` on
Windows — literal directory that doesn't exist.
- `path.join('', '.claude/skills/...', 'x')` resolves to a relative path
from CWD — silent mislocation.
- `process.env.HOME!` crashes with a non-null assertion at load time.
- `path.join("~", ...)` creates a literal `~` dir under CWD; Node path
APIs never expand `~`.
Fix: replace all five variants with `os.homedir()`. Per Node docs, `os.homedir()`:
- on POSIX: returns `$HOME` if set, else consults `getpwuid(geteuid())`
- on Windows: returns `$USERPROFILE` if set, else calls the Win32 API
Strictly better than every variant above on every platform. Test-isolation
patterns that set `process.env.HOME = tmpDir` (e.g. the one in
`browse/test/security-review-flow.test.ts:30`) keep working on POSIX CI
because `os.homedir()` reads HOME there.
Added `import * as os from 'os'` / `import os from "os"` to the 6 files
that didn't already have it, matching each file's existing import style.
Empirical proof on Windows 11 (cmd.exe-equivalent env with HOME unset):
OLD (|| "/tmp"): \tmp\.gstack\sidebar-agent-queue.jsonl
NEW (os.homedir): C:\Users\Sam\.gstack\sidebar-agent-queue.jsonl
Test suite: 163 pass / 10 fail on this branch. Identical 163/10 on clean
upstream main with the same test selection (confirmed by stash + rerun).
The 10 failures are all pre-existing (batch.test.ts hook timeout,
bun-polyfill Bun.serve/spawn/sleep assertions, a few sidebar-integration
tests that hit a pre-existing beforeEach timeout) and don't touch the
paths this PR modifies. Zero regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
scarson
added a commit
to scarson/gstack
that referenced
this pull request
Apr 21, 2026
# Conflicts: # browse/src/browser-manager.ts # browse/src/server.ts
This was referenced Apr 21, 2026
Author
|
For context: opened #1122 today ("Boiling the Windows CI Lake") proposing a Phase-1 Windows smoke CI that would have caught this class of bug at PR time. This PR is cited in the RFC's coverage table as one of the five recent examples that motivated it — not asking for anything on this PR, it can merge on its own merits. Cross-linking because the "Why this shipped unnoticed" paragraph above reads differently with a systemic answer in flight. Also a companion issue at #1123 for higher-level discussion if the PR's review thread gets crowded. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Windows, gstack's persistent state directory (
~/.gstack/) lands at nonsense paths — chromium profile, sidebar session data, ngrok config, agent queue, worktree cache, OpenAI key file, browse state — whenever a user runs gstack binaries (browse.exe,pdf.exe,design.exe, sidebar agent) from any shell that isn't Git Bash. That covers cmd.exe, PowerShell, VS Code's integrated terminal when not explicitly configured for Git Bash, Windows Terminal default profile, and any IDE spawning subprocesses with its own env block.Root cause: 31 sites in
browse/src/anddesign/construct paths withprocess.env.HOME || <fallback>. On Windows,HOMEis unset (Windows usesUSERPROFILE), so the fallback kicks in. The fallbacks are all broken in different ways:process.env.HOME || '/tmp'path.join('/tmp', ...)→\tmp\.gstack\...— literal path, doesn't existprocess.env.HOME || ''path.join('', ...)→ CWD-relative — silently mislocatesprocess.env.HOME!process.env.HOME || "~"~directory under CWD; Node path APIs never expand~process.env.HOME || process.env.USERPROFILE || '/tmp'Repro
Minimal Node script simulating an invocation from a native-Windows shell:
Output on Windows 11:
The OLD path doesn't exist. Every
fs.writeFileSynctargeting~/.gstack/against the OLD path fails withENOENT, usually caught and logged as a confusing "couldn't save state" warning, and the feature appears partially-working to the user — tokens don't persist across restarts, the chromium profile resets every launch, the sidebar session directory rotates and loses chat history.Why this shipped unnoticed
HOMEby default. SoHOME || '/tmp'short-circuits onHOMEand the state lands correctly — until an IDE-spawned subprocess inherits a sanitized env withoutHOME, or the user runsbrowse.exefrom PowerShell to see what happens..github/workflows/job runs onubuntu-latestormacos-latest. CI never touches the Windows branch of this fallback. fix(make-pdf): Windows .exe resolution for browse + pdftotext #1118's analysis of the make-pdf-gate matrix comment applies identically here.The fix
Replace every variant with
os.homedir(). Per Node docs:Strictly better than every pattern it replaces, on every platform. Specifically:
process.env.HOME = tmpHome(seebrowse/test/security-review-flow.test.ts:30) keeps working —os.homedir()still readsHOMEon POSIX.HOME || USERPROFILE || '/tmp'collapses cleanly toos.homedir().HOME!non-null assertion indesign/prototype.ts:13stops being a load-time crash risk.Files changed
browse/src/cli.tsbrowse/src/server.tsbrowse/src/browser-manager.tsbrowse/src/sidebar-agent.tsdesign/prototype.tsprocess.env.HOME!→os.homedir()(crash-on-unset bug)design/src/auth.ts|| "~"→os.homedir()(literal-tilde bug)Added
import * as os from 'os'(orimport os from "os"for the design files, matching each file's existing default-import style) to the 6 files that didn't already import it.Test plan
bun test browse/test/ make-pdf/test/on Windows — 163 pass, 10 fail. Same 163/10 on clean upstream main (stashed + re-ran for exact comparison), identical failing test names. All 10 failures are pre-existing and unrelated to this PR's surface:batch.test.ts+compare-board.test.ts—beforeEach/afterEachhook timeouts (5s limit; appears to be a Windows-specific test-harness issue, not the code under test)bun-polyfill.test.tsx4 —Bun.serve/spawn/spawnSync/sleepassertions, all expecting"OK"or"HAS_STOP"and receiving"", suggesting a pre-existing Bun polyfill subshell stdout-capture issue on this platformsidebar-integration.test.ts+sidebar-agent-roundtrip.test.ts—agent_done transitions to idle,queues message when agent is processing,kill adds error entry— same failures on clean main, unrelated to path constructionos.homedir()produces the correctC:\Users\Sam\.gstack\...path where the OLD fallback produced\tmp\.gstack\...os.homedir()falls through to$HOMEon POSIX, same as the old short-circuit), so no regression expected. Test that overridesprocess.env.HOMEinsecurity-review-flow.test.tskeeps passing becauseos.homedir()on POSIX honors HOME overrides.browse.exestart from PowerShell/cmd.exe verified — not run. The unit-test pass rate + empirical before/after is sufficient confidence given the mechanical nature of the change.What this PR doesn't do
/tmpusage in dev-only scripts.design/prototype.ts:20uses/tmp/gstack-prototype-<timestamp>/as an output dir. It's a manual-run validation script (bun run design/prototype.ts), not user-facing production code. Separate concern./tmpusage incookie-import-browser.ts. That's the scope of @Gonzih's fix: use os.tmpdir() instead of hardcoded /tmp in cookie-import-browser #748 already open — deferring to that PR.~expansion (fix: use $HOME instead of ~ in preamble for Windows compatibility #558, fix: use $HOME instead of ~ for browse/design binary paths #843). Those are about bash path expansion in generated SKILL.md files, a different subsystem from TypeScript runtime paths.fs.chmodSyncno-op. Filed separately as fix(browse): restrict sensitive state file ACLs on Windows via icacls #1121 — sensitive files (auth tokens, canary tokens, chat history, agent queue) end up with 6 inherited ACEs on Windows becausechmodmode bits don't map to NTFS ACLs. fix(browse): restrict sensitive state file ACLs on Windows via icacls #1121 fixes it via a shell-out toicaclsand includes an empirical before/after showing 6 ACEs → 1.Commits
d3a6b5e—fix(browse,design): resolve ~/.gstack via os.homedir() not $HOME || /tmpSingle commit. The change is mechanical and atomic across the 6 files — no value in splitting further.
🤖 Generated with Claude Code