Dashboard stop command kills the MCP server
The dashboard stop command terminates the entire MCP server process, not just the dashboard.
The problem
The dashboard runs as a daemon thread inside the MCP server process. The pid file records the MCP server's pid. When you run the stop command, it kills that pid. This terminates the process serving Claude Code over stdio, along with any agents running under it. You asked to stop a dashboard and you lose your tool server.
The fix, two phases
Phase 1 is a guard. At stop time, resolve the real owner of the dashboard port and refuse when that owner is an MCP server. This verifies rather than trusts, so it sidesteps staleness and pid reuse for free. It closes the safety defect on its own and is worth shipping without phase 2.
Phase 2 is a format change. A structured pid file that carries the mode the writer already knows, instead of making the reader infer it from a process listing.
Why not the smallest fix
One option was rejected: having the standalone path write its own pid in the existing bare-integer format. It is the smallest diff and it does make the file true more often, which is exactly the problem. Today's staleness is an accidental safety net, because a stale file usually fails at the liveness probe before it reaches the kill. Making the recorded pid reliably true removes the net and leaves the trap in place. A correctness gain that is a safety regression.
Two measured constraints
The zero-signal liveness probe is genuinely non-destructive on this Python and OS combination. The initial expectation was the opposite. It was tested against a throwaway process and proven wrong. The wrong version is the more intuitive one, which is why it is written down.
Termination on Windows does not run atexit handlers or finally blocks. Measured, not inferred. This rules out every cleanup-on-shutdown design for the pid file, because our own stop command defeats both mechanisms.
I wrote the design doc at docs/internal/2026-08-01-dashboard-pid-and-stop-safety.md. The internal docs directory is gitignored, so a named-file add fails loudly rather than quietly committing internal notes. Phase 1 of the guard remains unstarted.