garrytan / garrytan/gstack

browse: EEXIST on Windows when .gstack exists — mkdirSecure rethrows EEXIST from recursive mkdir (Bun)

Open Beginner friendly
#2,048 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
133k
Forks
19.9k
Avg merge
18h 46m
Merged PRs (30d)
26

Description

## Summary
On **Windows (Bun-compiled `browse`)**, every `browse` command aborts with:

```
[browse] EEXIST: file already exists, mkdir 'C:\...\\.gstack'
```

once a `/.gstack/` directory already exists and contains a **stale daemon record** (a `browse.json` / `terminal-*` set whose recorded daemon PID is dead). The daemon-relaunch path then can't recreate the state dir and the whole CLI is dead until the user manually clears `.gstack`.

## Environment
- gstack **v1.58.1.0**
- Windows 11, prebuilt `browse/dist/browse.exe` (Bun-compiled)
- Invoked from inside a git repo that has a pre-existing `/.gstack/`

## Root cause
`mkdirSecure()` in `browse/src/file-permissions.ts`:

```ts
export function mkdirSecure(dirPath: string): void {
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
restrictDirectoryPermissions(dirPath);
}
```

On **Bun + Windows**, a *recursive* `mkdirSync` throws `EEXIST` when the directory already exists (Node returns silently — recursive mkdir is supposed to be idempotent). `ensureStateDir()` in `browse/src/config.ts` only special-cases `EACCES`/`ENOTDIR` and **re-throws everything else**, so the `EEXIST` becomes fatal. The function's own docstring even says *"Safe to call on an existing directory"*, so `EEXIST` should be a no-op.

## Reproduce
1. On Windows, run any `browse` command from a repo dir (creates `/.gstack/` + a live daemon record).
2. Let the daemon die uncleanly (machine sleep / crash / kill) so the record goes stale and no live daemon remains.
3. Run any `browse` command again from that dir → `EEXIST: mkdir '...\.gstack'`, repeats on every invocation.

Running from a fresh dir (no `.gstack` yet) works, which is the tell.

## Fix
Treat `EEXIST` as success in `mkdirSecure` (matches its idempotent contract; covers all call sites — `ensureStateDir`, auth dir, skill staging):

```ts
export function mkdirSecure(dirPath: string): void {
try {
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
} catch (err: any) {
// Bun on Windows throws EEXIST from a *recursive* mkdir when the dir
// already exists (Node returns silently). The dir existing is the
// desired end state for `mkdir -p`, so treat EEXIST as success.
if (err?.code !== 'EEXIST') throw err;
}
restrictDirectoryPermissions(dirPath);
}
```

## User workaround until a fixed binary ships
```bash
rm -f "/.gstack"/{browse.json,terminal-*,*.log,*.jsonl}
```

Contributor guide

Open the contributing guide

Research direction

Start with browse/src/file-permissions.ts and browse/src/config.ts, especially mkdirSecure() and ensureStateDir(). Reproduce the existing-directory case with Bun on Windows, then verify that an existing .gstack directory no longer aborts browse while unrelated filesystem errors still surface. Confirm the behavior across the mkdirSecure call sites mentioned in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, typescript
Domain
cli, devtools
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.