clawpatrol run on a host whose daemon can't boot prints:
clawpatrol: daemon connect: spawn daemon: daemon ready: EOF (read "")
(if this machine was joined with --whole-machine, run the command directly — `clawpatrol run` isn't needed; traffic already routes through the gateway)
Neither line is actionable, and the second one is wrong. The real cause was sitting in ~/.local/state/clawpatrol/run/daemon.log the whole time:
daemon pid=3862242 starting
daemon: transport: no mode file at ~/.clawpatrol/mode and no wg.conf — re-run `clawpatrol join`
That message is exactly what the operator needs, and it never reaches them. Diagnosing this took reading daemon_linux.go to learn the log existed.
Two separate defects:
1. The ready-pipe EOF is content-free. daemonSpawn (cmd/clawpatrol/daemon_linux.go:290) waits for ready\n on the inherited fd 3. The daemon boots its transport before binding the control socket — deliberate, per runDaemon's comment, so a parent that reads ready\n can go straight to a session START — and log.Fatalfs when daemonStartTransport fails. So every transport-boot failure reaches the client as an unadorned EOF, regardless of cause.
daemonSpawn already computes daemonLogPath() a few lines earlier. Sketch: stat the log before cmd.Start() to record its size, and on a ready-read failure read from that offset and fold the last non-empty line into the returned error. Reading from the recorded offset rather than tailing matters because the log is opened O_APPEND and shared across respawns — the last line may belong to an older daemon.
2. The --whole-machine hint is unconditional. run_linux.go:147 appends it to every daemon connect failure. On the host above it is actively misleading: there is no whole-machine marker, and isWholeMachineJoin() — already checked earlier in the same function at run_linux.go:119 — had returned false. Either gate the hint on that same predicate, or replace it with a pointer to the daemon log, which covers more failure modes than the one it guesses at.
Worth noting what triggered it, since it may deserve its own fix: the host was joined by a build predating the mode marker. daemonStartTransport's empty-mode fallback (daemon_linux.go:429) rescues legacy joins by stat'ing wg.conf and assuming WireGuard, but a legacy tailscale join has neither file, so it falls through to the fatal error. The remedy is a re-join either way; the point of this issue is that the operator should be able to learn that from the command's own output.
clawpatrol runon a host whose daemon can't boot prints:Neither line is actionable, and the second one is wrong. The real cause was sitting in
~/.local/state/clawpatrol/run/daemon.logthe whole time:That message is exactly what the operator needs, and it never reaches them. Diagnosing this took reading
daemon_linux.goto learn the log existed.Two separate defects:
1. The ready-pipe EOF is content-free.
daemonSpawn(cmd/clawpatrol/daemon_linux.go:290) waits forready\non the inherited fd 3. The daemon boots its transport before binding the control socket — deliberate, perrunDaemon's comment, so a parent that readsready\ncan go straight to a session START — andlog.Fatalfs whendaemonStartTransportfails. So every transport-boot failure reaches the client as an unadorned EOF, regardless of cause.daemonSpawnalready computesdaemonLogPath()a few lines earlier. Sketch: stat the log beforecmd.Start()to record its size, and on a ready-read failure read from that offset and fold the last non-empty line into the returned error. Reading from the recorded offset rather than tailing matters because the log is openedO_APPENDand shared across respawns — the last line may belong to an older daemon.2. The
--whole-machinehint is unconditional.run_linux.go:147appends it to everydaemon connectfailure. On the host above it is actively misleading: there is nowhole-machinemarker, andisWholeMachineJoin()— already checked earlier in the same function atrun_linux.go:119— had returned false. Either gate the hint on that same predicate, or replace it with a pointer to the daemon log, which covers more failure modes than the one it guesses at.Worth noting what triggered it, since it may deserve its own fix: the host was joined by a build predating the
modemarker.daemonStartTransport's empty-mode fallback (daemon_linux.go:429) rescues legacy joins by stat'ingwg.confand assuming WireGuard, but a legacy tailscale join has neither file, so it falls through to the fatal error. The remedy is a re-join either way; the point of this issue is that the operator should be able to learn that from the command's own output.