Ten days after I shipped bosun publicly, the feature I shipped didn't work
The last post ended with a plan: let the public repo sit unannounced for a couple of days, watch what surfaced from having other eyes on it, then write the HN/LinkedIn/Reddit sequence we'd sketched.
That plan didn't survive contact with reality. Not because anyone broke anything — because nobody came.
The repo went public. The README looked good. The install path worked. bosun tour walked a stranger through the full lifecycle in 90 seconds on a fresh Linux box. And then... silence. Two GitHub stars from people who happened to be around. No issues, no PRs, no Twitter chatter. Exactly what you'd expect for an unannounced 0.11 release of a tool whose entire pitch ("coordinate parallel Claude Code sessions") requires you to already be running multiple Claude Code sessions, which most people aren't.
I had a choice. Spam HN with a Show post and hope the thread didn't get buried by lunch. Or take the ten days the repo had been sitting public and use them to make bosun better for whoever does eventually show up.
I picked the second one. What follows is what those ten days actually contained.
#Bundle A through E — five small commits that closed a class
The first thing I did was take a pre-launch gap analysis seriously.
An external read of the codebase had flagged a list of items I'd half-noted but never committed to: input caps for the MCP tools, file modes on the audit logs, log-line scrubbing for paths that could carry secrets, bounded lock acquisition with diagnostic info when waits time out, security headers on bosun serve, sub-PIPE_BUF atomic-append on the usage ledger, and PID validation on the new bosun_attach tool.
Each one was its own commit. Each one was small enough that the diff fit in one screen. The commit messages name them as v0.12 Bundle A through Bundle E.
Bundle A (7b044a3) — input caps on every MCP tool argument that could grow without bound. File modes set to 0o600 on .bosun/audit/spawn.log, .bosun/state/*.attached-pid, and the usage ledger. Log scrubbing for paths that match (?i)(api|secret|token|password|key)[_ -]*=. .gitignore rules for .claude/ so per-user MCP wiring stops leaking into commits. The kind of pass that should have happened pre-v0.10 and didn't.
Bundle B (40dfae5) — bounded lock acquisition. Pre-fix, if you tried to take the flock around spawn-tree.json and the lock holder was wedged, you'd block forever. Post-fix, the acquire path is a non-blocking poll loop with a 30-second budget that returns a typed LockTimeoutError{Path, Timeout, HolderPID, HeldFor}. The diagnostic message now names the holder PID and how long it's been holding, so an operator can actually do something about it.
Bundle C (0dff01c) — security headers on bosun serve. X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Content-Security-Policy locked down to 'self', Referrer-Policy: no-referrer. MaxHeaderBytes and a MaxBytesReader body cap. The threat model: a malicious browser tab open in the same browser the operator hit bosun serve from. Standard defense-in-depth.
Bundle D (7d0e9d8) — sub-PIPE_BUF atomic-append on the usage ledger. The ledger is append-only and gets written from multiple Claude sessions concurrently. macOS guarantees atomic writes only up to 512 bytes (Linux is 4096). Pre-fix, a long TurnLabel could push a single line over 512 bytes and risk interleaving when two sessions appended at once. Post-fix, the encoder truncates TurnLabel then Model to keep every line under 480 bytes.
Bundle E (d1864a9) — bosun_attach PID validation. The new in-container heartbeat path lets a Docker-based agent register its PID with bosun so the liveness gate sees it. Pre-fix, you could pass pid=1, pid=99999999, or pid=2 (a kernel thread) and bosun would dutifully write the file. Post-fix: hard refusal for PID 1, best-effort cwd-validation that the registered PID is actually inside the worktree on Linux (where /proc/<pid>/cwd is readable), graceful fallback on macOS/Windows where it isn't.
Five bundles, five commits, ~1,200 LOC total. Each one closed a class of bug, not just a single instance. The pattern by Bundle E was familiar: cap, validate, type the error, log the diagnostic.
#The Windows trial — what SSH didn't tell me
The second thing I did was actually run bosun on Windows.
CI had been green on windows-latest for months. The Windows launcher code had been in the repo since v0.9. The README said "Windows supported." None of that means anything until you actually run the binary on a real Windows box and watch what happens.
I had a dockur Windows VM going on the Mac mini's k3s cluster. SSH worked. I tried running the trial via SSH first — and bosun's launcher silently did nothing.
The bug isn't bosun's. It's that wt.exe (Windows Terminal) is an AppX App Execution Alias — a 0-byte reparse point that requires an interactive desktop session to work. SSH sessions on Windows don't have one. The launcher was correctly invoking wt.exe; wt.exe was silently no-op'ing because nobody was logged into the console.
I switched to VNC. From an actual interactive desktop, the launcher worked end-to-end: three terminal windows opened, each cd'd into its worktree, each ran claude (or a marker script for the test). The empirical "yes, the launcher works on Windows" finding I'd been claiming was finally true.
The trial also found a real bug in internal/proc/terminate.go. On POSIX, os.FindProcess(pid) always succeeds (it just returns an opaque handle); the kill happens later. On Windows, os.FindProcess actually calls OpenProcess, which fails with ERROR_INVALID_PARAMETER when the PID is gone. The pre-fix code returned that error to the caller as "find process N: ..." instead of the documented "already-gone PID is a no-op." Two-line fix: short-circuit on !IsAlive(pid) before touching os.FindProcess.
Two other findings got filed for a future pass:
W1: bosun's cmd.exe fallback launcher constructs cmd /c start "" cmd /K cd /D "<worktree>" && marker.cmd as a single argv element. That falls into cmd.exe's >2-quote-with-special-chars stripping rule, which means the spawned window ends up cd'd to the drive root and && marker.cmd fails. Affects Win10 boxes that don't have Windows Terminal installed.
W2: bosun cleanup on Windows fails with "Access is denied" when a launched terminal still has the worktree open as its cwd. NTFS refuses to delete a directory any process has open. The error message doesn't name the blocker; an operator gets a generic permission denied and has to guess.
Both are filed. Both are real. Neither would have surfaced from a CI run.
The lesson from this part of the ten days: "tests pass on Windows" is not the same claim as "the binary works on Windows." A VM trial costs a couple of hours and finds things the test suite cannot.
#Bughunt-1 — the audit framework + 54 findings
By day eight I'd run out of cleanup work that felt obvious. So I sat down with the codebase the way I'd sat down with the three MCPs in the audit-methodology post, and ran a structured bug-hunt round.
This time I called it Bughunt-1 — the start of a numbered convention, mirroring Leonard's audits/ directory. The format is consistent: brief, per-lane findings, consolidated rollup, severity scale documented at the top of every round. The next round will be Bughunt-2 against the same convention. Future-me reading this in six months should be able to walk the audits dir and reconstruct what we knew and when.
Eight lanes, weighted toward the surface bosun actually exposes:
- L1 — build invariants. Version constants, ldflags drift, the things that quietly go stale.
- L2 — MCP tool cap edges. Now that Bundle A added the caps, did they actually hold?
- L3 — spawn / worktree state correctness. The heaviest lane. Where the v0.9 spawn code lives.
- L4 — lockfile / ledger / daemon lifecycle. Where Bundles B and D live.
- L5 —
bosun serveHTTP hardening. Where Bundle C lives. - L6 — MCP protocol fuzzing. JSON-RPC framing edge cases.
- L7 — real dogfood. Use bosun to coordinate a real-feeling parallel-session run; capture UX friction.
- L8 — Windows / cross-platform. Continuation of the trial work.
The harness reused the unix-socket JSON-RPC client from the MCP audits, plus a small bash logging convention. The sandbox lived at /tmp/bosun-redteam/ because bosun (correctly) refuses to init under ~/Documents (iCloud-synced — issue #15). About 140 sub-tests across the eight lanes.
Total findings: 54. Severity mix: 0 CRITICAL, 5 HIGH, 20 MEDIUM, 27 LOW.
I closed the five HIGH this week. Each was a commit on its own branch (audit/bughunt-1). Each closed a different class of bug. The five collectively are interesting enough that I'm putting them in their own post — but the headline is below.
#The headline: I'd shipped a broken feature
The most consequential finding was F009: bosun_spawn — the v0.9 differentiator, the thing the launch post called out as "the v0.9 spawn lifecycle" — was completely broken on every default install since v0.11.
The mechanism is short. v0.11 changed the default worktree naming scheme to include a round timestamp (scheme C, UID-per-worktree, <repo>-bosun-<timestamp>-<sub>). The change was load-bearing for a different bug — preventing operators from collision-recreating a worktree at a path that already existed. The reader side (bosun status, bosun list) got updated to look up worktree paths via git worktree list and the branch ref. The new path resolver worked.
The bosun_spawn MCP tool's parent-liveness gate did not get updated. It called session.WorktreePathForLabel(repoRoot, *s.cfg, parent, "") — passing an empty round timestamp, which deterministically produced the legacy path shape <repo>-bosun-<sub>. Then it asked proc.Running whether an agent process was running with that cwd. The agent's actual cwd was the timestamped path. The lookup never matched. The gate refused every spawn call with "no live agent detected in <parent>'s worktree".
Every single default-init session since v0.11 hit this. The error message gave the operator no reason to suspect a path-resolution bug — the natural read is "the daemon doesn't see my agent" and you go hunt PIDs.
I shipped v0.7 through v0.12 with this bug live. The launch post celebrated v0.11 going public. bosun_spawn was the feature in v0.9 I'd been most excited about. The launch post mentions "the v0.9 spawn lifecycle had two open bugs from trial #3c" and that those two bugs were closed in Bundle A of the launch sprint. They were. F009 was a third bug, in a different file, that the launch sprint didn't touch.
The fix was small: thread worktree-path resolution through a worktreePathFn indirection on the MCP server. Production wires it to session.ResolveWorktreePathByBranch, which queries git worktree list the same way session.Derive already does. Two files, ~40 LOC of real logic, one regression test that builds a real git repo with a scheme-C-shaped worktree and pins that the resolver finds it.
The harder question is how I shipped a broken feature across five versions and didn't notice.
The honest answer: my own usage of bosun is mostly the numbered-session flow (bosun init 4, work in four worktrees), not the spawn flow (one session spawning sub-sessions mid-work). My own dogfood didn't exercise spawn enough to find it. The bug hunters who did three rounds before this one weren't looking at the spawn-vs-status path-resolution divergence. The CI tests for the spawn tool used mocked runningFns that pre-seeded the canonical path; they never queried git for what the real path was.
The bug found a real reader (Bughunt-1 lane 3) running the actual flow against the actual default-init shape. The fix would not have come from another unit test or another security pass. It came from a structured external-perspective read.
#What I'd do differently if I were the same person ten days ago
Three things.
Bug hunt rounds don't compound additively. I had three prior bug-hunt rounds. I had Bundles A–E. I had a Windows trial. Bughunt-1 found 54 things none of them caught. Each round finds different shapes because each round comes from a different angle. The fourth round will find shapes the first three didn't. Bug-hunting is not "we already did it."
The launch post claims hold up better than I expected, except for the one that didn't. The safety contract, the install path, the tour, the README — those all survived contact with a structured audit. The bosun_spawn feature outage is the one thing that didn't. That's better than I feared and worse than I hoped, which is probably honest for a six-month build.
Public-but-unannounced is a real state and it produces a real outcome. Nobody came. That's a finding too. The launch post's hopeful "watch what surfaces from having other eyes on it" plan was based on a model where curious eyes find new repos. Curious eyes don't find new repos. Curious eyes find announcements. The corollary: the work I did during the ten days of silence wouldn't have happened the same way under HN-attention pressure. The trade was real.
Bughunt-1 ships on a branch called audit/bughunt-1 with five clean commits closing the five HIGH. The 20 MEDIUM and 27 LOW are filed for the next round. The audits directory is now a real convention, not a one-off doc.
Next week I take v0.12 into a separate dogfood: someone else's repo, an actual workflow I don't control, see what new frictions surface. The HN/Reddit/LinkedIn sequence the launch post promised is still on the list. It's just not the next thing.
Bosun lives at github.com/jasondillingham/bosun. The Bughunt-1 round and the audit convention live at audits/. The five HIGH closures are on the audit/bughunt-1 branch.
What's the bug in your project that you've shipped across multiple versions without noticing? What lens haven't you looked through yet?