Chrome binds remote-debugging port to IPv6 [::1] only — CDP connection fails (skill assumes IPv4)

Author: fengrui358Created Jun 27, 2026Updated Jun 27, 2026

Environment

  • macOS (Darwin 25.x)
  • Chrome 149, launched with --remote-debugging-port=9222 (dedicated user-data-dir)
  • web-access @ mainbrowser-discovery.mjs (138 lines) and cdp-proxy.mjs getWebSocketUrl are IPv4-only

Problem

On recent macOS, Chrome's remote-debugging port is bound to IPv6 [::1]:9222 only — there is no IPv4 127.0.0.1 listener. The skill assumes IPv4 everywhere, so check-deps reports the browser as not connected:

bash
$ lsof -nP -iTCP:9222 -sTCP:LISTEN
Google  46577  free  ...  TCP [::1]:9222 (LISTEN)     # IPv6 only

$ curl http://127.0.0.1:9222/json/version            # fails — nothing on IPv4
$ curl 'http://[::1]:9222/json/version'              # works
{"Browser":"Chrome/149.0.7827.196","webSocketDebuggerUrl":"ws://[::1]:9222/devtools/browser/<uuid>",...}

Three places all assume 127.0.0.1:

  1. browser-discovery.mjscheckPort(port)net.createConnection(port, '127.0.0.1'); an IPv6-only port is invisible to it.
  2. browser-discovery.mjsdetectAll() reads wsPath from the DevToolsActivePort file, but that UUID goes stale after the previous Chrome instance exits (it changes every launch).
  3. cdp-proxy.mjsgetWebSocketUrl() builds ws://127.0.0.1:PORT..., which can't reach an IPv6-only listener.

Relation to #128

#128 (thanks @szxsq-728) fixes the stale-wsPath problem in fallback mode by fetching /json/version. But it (a) only covers the fallback branch, not the normal DevToolsActivePort discovery path, and (b) hardcodes http://127.0.0.1:PORT/json/version and rewrites the host to 127.0.0.1. On an IPv6-only bind, #128's fetch itself fails, and even if it got the wsPath, the rewritten ws://127.0.0.1 URL still can't connect. IPv6 is the blind spot — this issue complements #128.

Proposed fix (implemented & verified locally)

  • browser-discovery.mjs: add probeDevtools(port) — HTTP-probe /json/version trying 127.0.0.1 first, then ::1 as fallback; return the actually-reachable host + the real wsPath parsed from webSocketDebuggerUrl. Use it in both detectAll() and findFallbackPort() (now returns {port, host, wsPath}). IPv4-first ⇒ zero behavior change for IPv4 users.
  • cdp-proxy.mjs: getWebSocketUrl(host, port, wsPath) builds ws://${host} and brackets IPv6 (ws://[::1]:PORT/...); thread chromeHost through connect().
  • cdp-proxy.mjs enablePortGuard: add http://[::1]:PORT/* to the port-probe interception patterns.
  • check-deps.mjs: adapt the empty-branch to the new findFallbackPort() return shape.

Verification (macOS, Chrome bound to [::1]:9222)

bash
$ node check-deps.mjs --browser chrome
node: ok (v22.22.3)
browser: ok (Chrome, port 9222)
proxy: ready (Chrome)

$ curl -s http://127.0.0.1:3456/targets | jq 'length'
2

getWebSocketUrl output for IPv4 inputs is byte-identical to before (no regression).

Happy to turn this into a PR if the approach looks right. cc @szxsq-728 — it overlaps with #128 on the stale-wsPath part but adds the IPv4/IPv6 host handling, which I believe is the more fundamental issue here.