Summary
On a headless Linux box running as root, ./chrome-ws start fails silently: Chrome spawns, immediately dies, and the script reports a misleading "Chrome started but remote debugging not accessible" — when in fact Chrome was never able to start at all. The user has no supported way to pass --no-sandbox / --headless=new to the CLI's start path, because the CLI's start path does not honor CHROME_EXTRA_ARGS even though the MCP-mode launcher in this same repo does.
Environment
- Digital Ocean Linux droplet, Ubuntu, running as
root
/usr/bin/google-chrome → Chrome 147.0.7727.137
- Node v22.22.2
superpowers-chrome cloned from main at HEAD on 2026-05-28
- No prior Chrome processes; clean
/tmp/chrome-debug
Repro
cd skills/browsing
./chrome-ws start
# (also tried with CHROME_EXTRA_ARGS set — no effect)
CHROME_EXTRA_ARGS="--no-sandbox --headless=new --disable-dev-shm-usage" ./chrome-ws start
Both produce:
Starting Chrome: /usr/bin/google-chrome
Chrome started but remote debugging not accessible
Try: curl http://127.0.0.1:9222/json/version
After either invocation:
ps -ef | grep '[c]hrome' # nothing — Chrome is gone
ss -tln | grep 9222 # no listener
curl http://127.0.0.1:9222/json/version
# curl: (7) Failed to connect to 127.0.0.1 port 9222
Running Chrome directly on the same box with the right flags works fine — proving Chrome itself is healthy and the problem is in how the CLI launches it:
google-chrome --no-sandbox --headless=new --disable-gpu \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-test-profile about:blank &
sleep 2 && curl -s http://127.0.0.1:9222/json/version
# {"Browser":"Chrome/147.0.7727.137", ...}
What was hard about diagnosing this
-
The error message is misleading. "Chrome started but remote debugging not accessible" implies Chrome is alive and the debug port is the issue (maybe firewalled, wrong host, etc.). In reality Chrome was never alive past a few ms. Spending time on the port is a dead end; the real failure mode is process exited immediately.
-
The Chrome child's stderr is silently discarded. chrome-ws spawns Chrome with { detached: true, stdio: 'ignore' } (skills/browsing/chrome-ws ~line 335). So even when Chrome prints something like [FATAL] Running as root without --no-sandbox is not supported., the user never sees it. The only signal back is a curl failure 2 seconds later, which is a much more ambiguous symptom.
-
CHROME_EXTRA_ARGS looks supported but isn't (for the CLI start path). The README documents CHROME_EXTRA_ARGS as the way to pass extra flags. lib/chrome-launcher-helpers.js reads it (~line 290) and merges it into the args. But skills/browsing/chrome-ws's start command builds its chromeArgs array inline (~line 329) and only adds --remote-debugging-port and --user-data-dir — it never consults CHROME_EXTRA_ARGS, never adds --headless, never adds --no-sandbox. So setting the env var as documented appears to do nothing, which is hard to tell apart from "I set it wrong."
-
There's no obvious indication the two launch paths exist. Once I found the inconsistency between lib/chrome-launcher-helpers.js and skills/browsing/chrome-ws, the fix was obvious — but discovering the inconsistency required reading the source. From a user's vantage, "the README says CHROME_EXTRA_ARGS" + "setting CHROME_EXTRA_ARGS does nothing" reads like a bug in my shell, not a code path divergence.
What I think the right fix is
Primary: make chrome-ws's start command honor CHROME_EXTRA_ARGS exactly like lib/chrome-launcher-helpers.js does. Five-line change around line 332 of skills/browsing/chrome-ws:
const chromeArgs = [
`--remote-debugging-port=${effectivePort}`,
`--user-data-dir=${userDataDir}`
];
+
+ // Honor CHROME_EXTRA_ARGS (matches lib/chrome-launcher-helpers.js used by MCP mode).
+ // Needed on headless Linux / running-as-root for --no-sandbox / --headless=new.
+ if (process.env.CHROME_EXTRA_ARGS) {
+ chromeArgs.push(...process.env.CHROME_EXTRA_ARGS.split(/\s+/).filter(Boolean));
+ }
That single change is what unblocked me — with CHROME_EXTRA_ARGS="--no-sandbox --headless=new --disable-dev-shm-usage --disable-gpu" the CLI launch succeeds and every other chrome-ws command works against it.
Secondary, more valuable long-term: improve the failure path so the user gets a real signal when Chrome dies. Two small changes that together would have turned this from "spent an hour reading source" into "got a one-line error":
- Don't discard Chrome's stderr. Pipe Chrome's stderr to a tempfile or to the script's own stderr, at least until the debug port comes up. Then the user sees
Running as root without --no-sandbox is not supported. (or whatever Chrome chose to say) instead of a 2-second timeout. Same fix would help every future "Chrome can't start" cause — missing shared libs, profile lock conflicts, etc., not just the as-root case.
- Race Chrome's exit against the port poll. Today the script
setTimeout(2000) then curls /json/version. If the child exits before that timeout, surface Chrome exited with code N before opening debug port rather than the current message. With detached: true you can still chrome.on('exit', ...) before unref(). Knowing Chrome exited (vs is running but unreachable) cuts the diagnostic search space in half.
Optional polish: in the README's "Linux/WSL2 tip" callout, add a one-liner: "Running as root or in some containers? You'll need CHROME_EXTRA_ARGS=\"--no-sandbox --headless=new --disable-dev-shm-usage\"." That makes the docs match what lib/chrome-launcher-helpers.js already supports and what chrome-ws would after the primary fix.
Happy to PR the primary fix if useful — the stderr-surfacing change is a bigger design call (where do the logs go? rotate them? truncate? log-on-failure-only?) that I'd rather leave to you.
Summary
On a headless Linux box running as root,
./chrome-ws startfails silently: Chrome spawns, immediately dies, and the script reports a misleading "Chrome started but remote debugging not accessible" — when in fact Chrome was never able to start at all. The user has no supported way to pass--no-sandbox/--headless=newto the CLI's start path, because the CLI's start path does not honorCHROME_EXTRA_ARGSeven though the MCP-mode launcher in this same repo does.Environment
root/usr/bin/google-chrome→ Chrome 147.0.7727.137superpowers-chromecloned frommainat HEAD on 2026-05-28/tmp/chrome-debugRepro
Both produce:
After either invocation:
Running Chrome directly on the same box with the right flags works fine — proving Chrome itself is healthy and the problem is in how the CLI launches it:
What was hard about diagnosing this
The error message is misleading. "Chrome started but remote debugging not accessible" implies Chrome is alive and the debug port is the issue (maybe firewalled, wrong host, etc.). In reality Chrome was never alive past a few ms. Spending time on the port is a dead end; the real failure mode is process exited immediately.
The Chrome child's stderr is silently discarded.
chrome-wsspawns Chrome with{ detached: true, stdio: 'ignore' }(skills/browsing/chrome-ws ~line 335). So even when Chrome prints something like[FATAL] Running as root without --no-sandbox is not supported., the user never sees it. The only signal back is a curl failure 2 seconds later, which is a much more ambiguous symptom.CHROME_EXTRA_ARGSlooks supported but isn't (for the CLI start path). The README documentsCHROME_EXTRA_ARGSas the way to pass extra flags.lib/chrome-launcher-helpers.jsreads it (~line 290) and merges it into the args. Butskills/browsing/chrome-ws'sstartcommand builds itschromeArgsarray inline (~line 329) and only adds--remote-debugging-portand--user-data-dir— it never consultsCHROME_EXTRA_ARGS, never adds--headless, never adds--no-sandbox. So setting the env var as documented appears to do nothing, which is hard to tell apart from "I set it wrong."There's no obvious indication the two launch paths exist. Once I found the inconsistency between
lib/chrome-launcher-helpers.jsandskills/browsing/chrome-ws, the fix was obvious — but discovering the inconsistency required reading the source. From a user's vantage, "the README says CHROME_EXTRA_ARGS" + "setting CHROME_EXTRA_ARGS does nothing" reads like a bug in my shell, not a code path divergence.What I think the right fix is
Primary: make
chrome-ws'sstartcommand honorCHROME_EXTRA_ARGSexactly likelib/chrome-launcher-helpers.jsdoes. Five-line change around line 332 ofskills/browsing/chrome-ws:const chromeArgs = [ `--remote-debugging-port=${effectivePort}`, `--user-data-dir=${userDataDir}` ]; + + // Honor CHROME_EXTRA_ARGS (matches lib/chrome-launcher-helpers.js used by MCP mode). + // Needed on headless Linux / running-as-root for --no-sandbox / --headless=new. + if (process.env.CHROME_EXTRA_ARGS) { + chromeArgs.push(...process.env.CHROME_EXTRA_ARGS.split(/\s+/).filter(Boolean)); + }That single change is what unblocked me — with
CHROME_EXTRA_ARGS="--no-sandbox --headless=new --disable-dev-shm-usage --disable-gpu"the CLI launch succeeds and every otherchrome-wscommand works against it.Secondary, more valuable long-term: improve the failure path so the user gets a real signal when Chrome dies. Two small changes that together would have turned this from "spent an hour reading source" into "got a one-line error":
Running as root without --no-sandbox is not supported.(or whatever Chrome chose to say) instead of a 2-second timeout. Same fix would help every future "Chrome can't start" cause — missing shared libs, profile lock conflicts, etc., not just the as-root case.setTimeout(2000)then curls/json/version. If the child exits before that timeout, surfaceChrome exited with code N before opening debug portrather than the current message. Withdetached: trueyou can stillchrome.on('exit', ...)beforeunref(). Knowing Chrome exited (vs is running but unreachable) cuts the diagnostic search space in half.Optional polish: in the README's "Linux/WSL2 tip" callout, add a one-liner: "Running as root or in some containers? You'll need
CHROME_EXTRA_ARGS=\"--no-sandbox --headless=new --disable-dev-shm-usage\"." That makes the docs match whatlib/chrome-launcher-helpers.jsalready supports and whatchrome-wswould after the primary fix.Happy to PR the primary fix if useful — the stderr-surfacing change is a bigger design call (where do the logs go? rotate them? truncate? log-on-failure-only?) that I'd rather leave to you.