Symptom
Started crit daemon at http://localhost:60045 (PID 16325)
Error: could not reach daemon on port 60045: Get "http://localhost:60045/api/session": dial tcp [::1]:60045: connect: connection refused
The daemon starts and binds successfully, but the client can't connect to it on macOS.
Root cause
- The server binds IPv4 only in
cli_serve.go:312:
listener, err = net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
- The internal HTTP client calls use
http://localhost:<port>.
- On macOS,
localhost resolves to ::1 (IPv6) before 127.0.0.1. Go's HTTP client hits IPv6 loopback first, gets ECONNREFUSED (nothing listening on ::1:<port>), and surfaces the error instead of falling back to IPv4.
Verified locally:
$ dscacheutil -q host -a name localhost
name: localhost
ipv6_address: ::1
name: localhost
ip_address: 127.0.0.1
Suggested fix
Switch the internal HTTP client URLs to 127.0.0.1 so they match the server bind. Five call sites need the change:
daemon.go:281 — daemonAlive health probe
daemon.go:302 — daemonHasBrowser health probe
main.go:1538 — waitForDaemonReady (/api/session)
main.go:1566 — runReviewClientRaw (/api/review-cycle)
main.go:1714 — review-cycle POST in runReviewClient
focus_cli.go:308 — fetchSessionFocus
User-facing display strings ("Started crit daemon at http://localhost:...") and openBrowser(...) calls can stay as localhost — browsers do happy-eyeballs fallback and localhost is friendlier to read/copy.
Alternative: bind the listener dual-stack (e.g. listen on both 127.0.0.1 and [::1], or on localhost). This is more invasive and changes the security posture documented in CLAUDE.md ("Server binds to 127.0.0.1 only"), so the targeted client-side fix is preferable.
Repro
macOS where localhost resolves to ::1 first (default on recent macOS). Run crit in any git repo with changes; the daemon spawns, the client immediately fails to reach it.
Symptom
The daemon starts and binds successfully, but the client can't connect to it on macOS.
Root cause
cli_serve.go:312:http://localhost:<port>.localhostresolves to::1(IPv6) before127.0.0.1. Go's HTTP client hits IPv6 loopback first, getsECONNREFUSED(nothing listening on::1:<port>), and surfaces the error instead of falling back to IPv4.Verified locally:
Suggested fix
Switch the internal HTTP client URLs to
127.0.0.1so they match the server bind. Five call sites need the change:daemon.go:281—daemonAlivehealth probedaemon.go:302—daemonHasBrowserhealth probemain.go:1538—waitForDaemonReady(/api/session)main.go:1566—runReviewClientRaw(/api/review-cycle)main.go:1714— review-cycle POST inrunReviewClientfocus_cli.go:308—fetchSessionFocusUser-facing display strings (
"Started crit daemon at http://localhost:...") andopenBrowser(...)calls can stay aslocalhost— browsers do happy-eyeballs fallback andlocalhostis friendlier to read/copy.Alternative: bind the listener dual-stack (e.g. listen on both
127.0.0.1and[::1], or onlocalhost). This is more invasive and changes the security posture documented inCLAUDE.md("Server binds to127.0.0.1only"), so the targeted client-side fix is preferable.Repro
macOS where
localhostresolves to::1first (default on recent macOS). Runcritin any git repo with changes; the daemon spawns, the client immediately fails to reach it.