Problem
The parent-process watchdog (#808, 5bd05c9e) kills the browse server mid-workflow when the browse tool is invoked from skill scripts (e.g., automating League Lobster or GameChanger schedule updates).
How we use browse
We run a sports league (320 games). After editing the master schedule CSV, we sync changes to League Lobster and GameChanger via browser automation:
/setup-browser-cookies — imports authenticated cookies from the real browser
$B goto — navigates to the schedule page
$B js — finds games, opens edit modals, reads CSRF tokens, submits changes via fetch
$B snapshot — verifies edits persisted
This is a multi-step workflow with individual $B commands issued sequentially from a Claude Code skill. Each $B invocation goes through the CLI binary.
What breaks
The browse server is spawned with BROWSE_PARENT_PID: String(process.pid), where process.pid is the CLI invocation that first starts the server. The watchdog polls every 15s and calls shutdown() if that PID is gone.
Two failure modes:
1. Cookie picker dies mid-import. $B cookie-import-browser starts the cookie picker HTTP server (same port as browse). The CLI process that opened it exits. 15 seconds later, the watchdog kills the server. The user is still selecting cookies in their browser but the picker is dead.
2. Browse server dies between commands. Between $B js calls in a skill workflow, the original CLI process can exit. The next $B command finds a dead server and has to restart it (losing the browser session, cookies, and page state).
The pair-agent fix was scoped too narrowly
Commit 05d1a50e fixed this for pair-agent by setting BROWSE_PARENT_PID: '0' in the connect subprocess env. But the startServer() function still passes String(process.pid), so any non-pair-agent invocation still has the watchdog active.
The problem isn't specific to pair-agent. It's any scenario where the browse server needs to outlive the CLI invocation that spawned it. Skill-driven browser automation is a common case.
Our local fix
We patched startServer() in browse/src/cli.ts (two lines) to always pass BROWSE_PARENT_PID: '0':
- const extraEnvStr = JSON.stringify({ BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: String(process.pid), ...(extraEnv || {}) });
+ const extraEnvStr = JSON.stringify({ BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: '0', ...(extraEnv || {}) });
- env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: String(process.pid), ...extraEnv },
+ env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: '0', ...extraEnv },
This disables the watchdog entirely, which brings back the orphan risk from #808. But the 30-minute idle timeout (BROWSE_IDLE_TIMEOUT) already handles orphans for headless sessions, so in practice the watchdog is redundant for our use case.
Possible upstream fixes
-
Remove the PPID watchdog entirely. The idle timeout already prevents orphans. The watchdog adds a second, stricter lifecycle check that conflicts with legitimate long-running sessions.
-
Track "last attached CLI" instead of "spawning CLI." When a new CLI connects to an existing server, update the monitored PID. The server dies only when no CLI has connected for N seconds.
-
Opt-in watchdog. Default to BROWSE_PARENT_PID: '0' (no watchdog), let users or specific code paths enable it via env var when they know the server should die with the parent.
-
Activity-aware watchdog. Before self-terminating, check lastActivity. If there was recent activity (e.g., within the last 60s), skip the shutdown even if the parent is gone — something is still using the server.
Option 4 feels like the best balance: it prevents orphans (no activity = shutdown) without killing active sessions.
Environment
- macOS (Darwin 24.6.0)
- gstack browse invoked from Claude Code skills
- Workflow: cookie-import + multi-step JS automation on authenticated sites
Problem
The parent-process watchdog (#808,
5bd05c9e) kills the browse server mid-workflow when the browse tool is invoked from skill scripts (e.g., automating League Lobster or GameChanger schedule updates).How we use browse
We run a sports league (320 games). After editing the master schedule CSV, we sync changes to League Lobster and GameChanger via browser automation:
/setup-browser-cookies— imports authenticated cookies from the real browser$B goto— navigates to the schedule page$B js— finds games, opens edit modals, reads CSRF tokens, submits changes via fetch$B snapshot— verifies edits persistedThis is a multi-step workflow with individual
$Bcommands issued sequentially from a Claude Code skill. Each$Binvocation goes through the CLI binary.What breaks
The browse server is spawned with
BROWSE_PARENT_PID: String(process.pid), whereprocess.pidis the CLI invocation that first starts the server. The watchdog polls every 15s and callsshutdown()if that PID is gone.Two failure modes:
1. Cookie picker dies mid-import.
$B cookie-import-browserstarts the cookie picker HTTP server (same port as browse). The CLI process that opened it exits. 15 seconds later, the watchdog kills the server. The user is still selecting cookies in their browser but the picker is dead.2. Browse server dies between commands. Between
$B jscalls in a skill workflow, the original CLI process can exit. The next$Bcommand finds a dead server and has to restart it (losing the browser session, cookies, and page state).The pair-agent fix was scoped too narrowly
Commit
05d1a50efixed this for pair-agent by settingBROWSE_PARENT_PID: '0'in the connect subprocess env. But thestartServer()function still passesString(process.pid), so any non-pair-agent invocation still has the watchdog active.The problem isn't specific to pair-agent. It's any scenario where the browse server needs to outlive the CLI invocation that spawned it. Skill-driven browser automation is a common case.
Our local fix
We patched
startServer()inbrowse/src/cli.ts(two lines) to always passBROWSE_PARENT_PID: '0':This disables the watchdog entirely, which brings back the orphan risk from #808. But the 30-minute idle timeout (
BROWSE_IDLE_TIMEOUT) already handles orphans for headless sessions, so in practice the watchdog is redundant for our use case.Possible upstream fixes
Remove the PPID watchdog entirely. The idle timeout already prevents orphans. The watchdog adds a second, stricter lifecycle check that conflicts with legitimate long-running sessions.
Track "last attached CLI" instead of "spawning CLI." When a new CLI connects to an existing server, update the monitored PID. The server dies only when no CLI has connected for N seconds.
Opt-in watchdog. Default to
BROWSE_PARENT_PID: '0'(no watchdog), let users or specific code paths enable it via env var when they know the server should die with the parent.Activity-aware watchdog. Before self-terminating, check
lastActivity. If there was recent activity (e.g., within the last 60s), skip the shutdown even if the parent is gone — something is still using the server.Option 4 feels like the best balance: it prevents orphans (no activity = shutdown) without killing active sessions.
Environment