← all writing

Audit isn't one thing

I'd already audited my Go CLI three times. Once for a general first-pass review. Once for a broader sweep through medium-and-low findings. Once focused on filesystem and IPC surface. Then I did a five-commit security hardening sprint based on an external read of the codebase, and shipped to Windows under a separate cross-platform trial. After all of that, I sat down for a fourth structured audit round and found five HIGH-severity bugs. None of them had been surfaced by any prior pass.

The obvious read is that the prior rounds were sloppy. I don't buy it. The prior rounds were thorough — every finding they did surface was a real bug, and they collectively reduced the codebase's defect density meaningfully. The total review hours invested before Bughunt-1 was substantially more than what I spent on the fourth round.

What changed in the fourth round was the lens. The first three rounds and the hardening sprint had been running similar shapes of probe against the code. The fourth round — Bughunt-1, in my internal naming — added two lanes that didn't exist in any prior pass. Three of the five HIGH bugs came from those two new lanes.

This post argues that "audit" isn't one thing. It's a vocabulary of lenses, each catching a different shape of bug. The marginal value of audit round N isn't determined by total time spent — it's determined by which lens that round adds. A new one-hour lens outperforms an old lens you re-run for eight.

The implication, if the argument holds, is that the standard way most engineering orgs think about audit cadence is wrong. You don't get more out of audit by doing more of it. You get more out of audit by bringing a new lens.

#The question this post tries to answer

When a subsequent audit round catches bugs a prior round missed, what's actually happening? Three possible explanations:

  1. The prior round wasn't thorough. It rushed, or the reviewer was junior, or the methodology wasn't rigorous. The subsequent round did better simply by trying harder.

  2. The code changed between rounds. New bugs got introduced after the prior round ran. The "miss" was a temporal artifact — the bug didn't exist when the prior round ran.

  3. The lens changed. Both rounds were rigorous, but they probed different surfaces. The prior round looked at A and B; the subsequent round looked at C. The bugs in C were always there; nobody had pointed a probe at them.

The first explanation is the one most engineering retrospectives reach for, because it's the one that produces an actionable lesson — "be more thorough next time" — and because it pattern-matches to how performance reviews talk about quality. The second is true sometimes but usually checkable: you can look at the codebase state at each round's commit and see whether the bug was newly-introduced or pre-existing.

The third explanation is the most interesting and the least frequently named, because it points at a structural property of audit work rather than a discipline problem. If most "misses" are actually "different lens needed," then planning audit cadence is about diversifying lenses, not stacking iterations. You don't schedule audit round 4 by asking "are we due?" — you schedule it by asking "what lens haven't we run yet?"

For this post I'm going to walk through a single codebase — bosun, a Go CLI I started writing in March 2026 and shipped publicly in May — and characterize each of the prior audit rounds it received by what lens that round was running. Then I'll walk through each of the five HIGH bugs the fourth round (Bughunt-1) found and try to answer, for each one, the question: would any of the prior rounds have caught this bug if its lens had been pointed at the right surface, or was the bug structurally invisible to every prior lens?

If most of the five were invisible to all prior lenses, the data supports explanation (3). If most could have been caught by prior rounds if those rounds had been more thorough, the data supports explanation (1). If a substantial chunk turns out to be newly-introduced between rounds, explanation (2) is in play.

This is a single-codebase case study, not a survey. The conclusions are strongest as a hypothesis-generator about audit methodology rather than as proof — bosun is one codebase, written by one author, audited by largely one author (with one external read informing the hardening sprint). I'll flag what I think generalizes and what I think is codebase-specific as the analysis proceeds.


#The five prior lenses

I'll characterize each prior probe by what it was looking for — not what it found, but what shape of bug its methodology was equipped to surface.

#Lens 1: General code review (bug-hunt pass 1)

The first pass on bosun was a general-purpose human review. I sat down with the code in chunks of one or two days, walked through each package, and looked for whatever stood out — naming inconsistencies, error-handling that swallowed problems, places where the contract between modules wasn't clear. The output was a list of medium-and-low findings closed across batches 7ced1cd and 73a443b.

Methodology: read top-to-bottom in package order, file by file, looking for "huh, that's weird" moments. Static-analysis baseline running underneath: gosec flagged 12 issues that got triaged in 774175b with per-line nosec annotations where the flag was a false positive. Estimated effort: 8-12 hours across the wave.

What it was looking for: code-quality issues, error-handling inconsistencies, things a reader could spot from the file alone. What it was structurally blind to: anything that required running the system to manifest. Anything in the interaction between modules that's correct when each module is read in isolation but wrong when they meet. Anything that needs an adversarial mindset to surface.

A representative finding: a few places where errors got swallowed in defer blocks, masking real problems from the operator. Fix shape: log-and-return rather than silent suppression.

#Lens 2: Broader sweep (bug-hunt pass 2)

A second wave came after pass 1's fixes had landed. By that point I had a clearer mental model of the codebase, so the second pass was less "read top to bottom" and more "look at the places where I had questions during pass 1 and didn't resolve them." Commit 7e92272: "bug hunt pass 2: seven more fixes for medium/low findings."

Methodology: targeted review at the unresolved questions from pass 1, plus a fresh look at any new code added in the interim. Estimated effort: 4-6 hours.

What it was looking for: same general shape as pass 1, but with the benefit of having a mental model of the system. What it was structurally blind to: same things as pass 1. Two passes with the same lens find different surface findings but the same shape of bug. Anything that requires a different methodology — concurrency under load, cross-host attack scenarios, state-machine invariants — stays invisible.

This is the data point that should make you suspicious of cadence-based audit planning. Two passes of the same lens won't catch what one pass of a different lens would.

#Lens 3: Filesystem and IPC security audit

A focused security pass on the OS-level interface. Commit 0fff194 closed three findings from this lens: MCP socket permission bits not restrictive enough (0644 where 0600 was correct), webhook configuration env vars leaking into the log when the webhook target was unreachable, and the events log not rotating, growing unbounded over a long session.

Methodology: walk every place bosun touches the filesystem or talks to another process over a socket or pipe. For each touchpoint, ask: who can read this? Who can write this? What information leaks out? Estimated effort: 4-6 hours.

What it was looking for: the boundary between bosun's process and the operating system. Permissions, leakage, unbounded growth, signal handling. What it was structurally blind to: anything above the OS interface. The application-level state inside bosun, the protocol layer between bosun and its MCP clients, the HTTP layer of bosun serve — none of those surfaces were probed.

This is the lens where the discipline of "name what you're probing" pays off. A security audit that says "I'm probing filesystem and IPC" admits up front that it's not probing the application protocol. A subsequent round can pick up that lane without redundancy. The audit didn't fail to catch HTTP bugs — it correctly didn't try to.

#Lens 4: Bundle A-E hardening sprint (external-read-informed)

Five focused commits between 7b044a3 and d1864a9, each closing a class of issue that an external reader of the codebase had flagged. Bundle A: input caps + file modes + log scrubbing. Bundle B: bounded lock acquisition with holder diagnostics. Bundle C: HTTP security headers + body caps on bosun serve. Bundle D: sub-PIPE_BUF atomic-append on the usage ledger. Bundle E: bosun_attach PID validation.

Methodology: each bundle was a single class of hardening, scoped to a single commit's worth of work. The lens here was preemptive — close categories of bug before they're exploited, not respond to a found bug. Estimated effort: 8 hours total across the five bundles.

What it was looking for: the canonical hardening checklist for a long-running daemon — input bounds, file-mode tightness, secrets-in-logs, lock contention diagnostics, security headers, atomic appends, PID validation. What it was structurally blind to: anything not on the external reader's list. The reader had walked the code and flagged what they saw; the lens was bounded by what they noticed. If a class of bug existed in a corner the reader didn't visit, the bundle didn't address it.

A representative finding: Bundle A added a cap on the brief text passed to bosun_spawn so that a malformed agent couldn't fill memory with a single tool call. The cap is real defensive value; it doesn't help with anything other than that one input vector. Bundle C added security headers to bosun serve that close standard iframe-based abuse — but as we'll see, headers and enforcement are different things.

#Lens 5: Windows / cross-platform trial

A separate workstream around 2026-05-26 to 2026-05-28 (commits a760e2a..aabaf3d). Stand up a Windows VM, install bosun, walk through the same flows that work on macOS and Linux, and watch what breaks.

Methodology: actually run the binary on a real Windows environment. The trial caught a launcher bug where wt.exe (Windows Terminal) needed an interactive desktop session that SSH sessions don't have; switching to VNC let the trial complete. The trial also found a real bug in internal/proc/terminate.go where os.FindProcess(pid) has different semantics on Windows than POSIX. Two-line fix.

Estimated effort: 4-8 hours including VM setup, SSH-vs-VNC investigation, the actual bug fix.

What it was looking for: cross-platform runtime behavior. OS-specific assumptions that hadn't been called out as cross-platform. What it was structurally blind to: anything not platform-specific. The lens was about "what breaks on Windows specifically." Bugs that happen on every platform stayed invisible to this lens because the comparison was "does this work on Windows like it works on macOS," not "does this work at all."

#A note on the lens taxonomy itself

Five distinct lenses, each catching a different shape of bug. The first two share structure — both human review, just with different mental models of the system. The third focuses on a specific OS-level surface. The fourth is scoped to an external reader's flagged list. The fifth probes a specific runtime environment.

What's notable is what's not in this list. No active probing of bosun serve over HTTP. No JSON-RPC framing fuzz against the MCP transport layer. No end-to-end UX walkthrough where I pretend to be an operator and chain real commands together. Those lenses didn't exist before Bughunt-1.


#The two lenses Bughunt-1 added

Bughunt-1 was structured as eight lanes (L1–L8). Five of them sharpened or extended lenses that prior rounds had already run — checking that Bundle A's caps actually held under malformed arguments, verifying Bundle B's bounded lock under concurrent appenders, re-auditing Bundle E's PID validation against PID 2 and launchd descendants. The cross-platform lane (L8) continued the Windows trial work with the same lens.

Three of the eight lanes were genuinely new. Of those, two produced HIGH findings; one didn't, which is itself useful data.

#Lane L5: bosun serve HTTP active probing

Bundle C had added security headers to bosun serve — X-Frame-Options: DENY, a tight CSP, X-Content-Type-Options: nosniff, MaxBytesReader for the body. The Bundle C threat model named "the malicious-browser-tab vector" explicitly.

Bundle C's lens was header completeness: did the response carry the headers that would close standard browser-side abuse vectors? That's a real lens. It does a real job. But it answers a different question than "what happens when I actually probe this server with active HTTP requests."

L5's lens was active probing. Send a request with a Host: evil.com header to the loopback bind and see what the server returns. Open a slow-read connection and watch whether the server holds it. Issue cross-origin requests against the JSON API endpoints and see whether any data crosses the boundary.

This is a different shape of work than verifying headers. The headers tell you what defenses the server advertises. Active probing tells you what defenses the server enforces. The two answers are not always the same. Bundle C correctly identified the attack class ("malicious browser tab") but closed it with a defense aimed at iframe-based abuse — which is a subset of "malicious browser tab" but not all of it.

L5 produced one HIGH finding — F044 — with two distinct exposures. DNS rebinding lets any origin read the JSON endpoints (/api/status, /api/show/<label>). The same Host-header bypass exposes the SSE event stream at /api/events to cross-origin readers. Same root cause, two endpoints, one defense missing.

Estimated lane effort: 4-6 hours. The lane was new in Bughunt-1 because no prior round had run a probe-driven HTTP harness — only header-completeness review.

#Lane L7: real dogfood / end-to-end UX walkthrough

L7 was the simplest lane to describe and the hardest one to skip. Use bosun the way an operator would — chain bosun init, bosun spawn, bosun merge, bosun cleanup against a real-feeling workflow — and write down every friction, every weird message, every command that returned the wrong exit code.

This is not a review of the code. It's a review of the system's behavior under intended use. Prior rounds had all read the code, run unit tests, and verified that specific subsystems behaved correctly under their tests. None had executed a full bosun workflow end-to-end and watched what actually happened from an operator's seat.

Estimated lane effort: 2-3 hours. Cheaper than any other lane. Two HIGH findings came from L7 — one was a default-init workflow that was completely broken across six tagged versions, and one was an exit-code lie where a command that did real work exited 0 on a failure mode that left the working tree wedged.

The L7 finding I think about most is the broken default-init flow. Six tagged versions had shipped with that bug. Three prior audit rounds had not caught it. None of my own use had caught it because my own use happens to not exercise the spawn path that triggered the bug. The cost of running L7 in any of the prior rounds would have been about an hour. The cost of not running it was a feature outage live across the entire post-v0.11 release line.

#Lanes that found nothing — also data

L1 (build invariants) found one stale version constant, which is a real bug but in the noise compared to the HIGH findings. L2 (MCP tool cap regression check) found that Bundle A's caps held under every adversarial argument the lane could construct — useful evidence that the prior bundle had worked, not a new finding. L6 (PID validation extension) extended Bundle E with PID 2 (kthread) and launchd-descendant cases; the existing validation handled them correctly.

Three lanes finding nothing is also data. It tells me that Bundle A, Bundle B, Bundle D, and Bundle E did what they claimed. It also tells me where Bughunt-1's lens taxonomy might have wasted effort — though the "wasted effort" of confirming a prior round did its job is cheap insurance, not a real cost.


#The cross-round matrix

For each Bughunt-1 HIGH finding, the question is: would any of the five prior lenses have caught this bug if it had been pointed at the right surface? "Yes" means the lens's methodology was structurally equipped to surface this bug class. "Partial" means the methodology could have found it if the reviewer happened to ask the specific right question — which is asking a lot of a general-purpose review lens. "No" means the bug was structurally invisible to that lens.

Finding General review (pass 1) Broader sweep (pass 2) Filesystem/IPC audit Bundle A-E hardening Windows trial Bughunt-1 lane
F009 — worktree path scheme migration miss (default-init broken across 6 versions) No No No No No L7
F032 — exit-code lie on bosun merge conflict Partial Partial No No No L7
F018 — unbounded MCP frame buffer DoS Partial Partial No (lens scope didn't include IPC reads) No (Bundle A capped tool inputs; transport layer was out of scope) No MCP transport sub-lane
F044 — DNS rebinding on bosun serve No No No No (Bundle C correctly named the attack but solved with headers, not Host-header gating) No L5
F038 — bosun init session-<word> accepted by writer, rejected by readers Partial Partial No No No L7

Five findings, 25 cells, zero clean "yes" entries. The "partial" entries are not free — they require a reviewer to have asked the specific question that surfaces the bug, which isn't a property of the general-review lens itself.

The pattern is the answer to the question the post opened with. The prior rounds didn't fail to catch these bugs because they were sloppy. They failed to catch them because their lenses were structurally not equipped to surface them. The bugs were always there. Nobody had ever pointed a probe at the relevant surface.

The Bundle A-E hardening sprint is the most interesting "no" in the matrix. Bundle A's lens — cap unbounded inputs — would have caught F018 if it had been applied to the transport layer rather than the tool-argument layer. Same lens, wrong scope. The miss wasn't in the lens; it was in where the lens was pointed. That's a different kind of failure than "we needed a new lens" — and it suggests a useful sub-question: when a lens catches one class of bug at one scope, is it worth re-running the same lens at every other scope where it could apply?


#The sharpened insight

When I started this analysis I expected the data to support a soft version of explanation (3) — "the lens changed." What I found was the strong version. Of 25 lens-finding cells, zero were clean "yes" entries. The five prior probes were not under-thorough; they were structurally not probing the surfaces where these bugs lived.

The implication runs in two directions.

First, the marginal value of audit round N is determined by which lens it adds. A new one-hour lens outperforms an old lens you re-run for eight. The cadence-based way of thinking about audits ("we're due for another round") produces less value than the lens-based way ("we haven't probed surface X yet"). If you've done three general code reviews and the next round is also a general code review, the third round will find fewer things than the first two — not because the codebase improved, but because the lens is exhausted.

Second, the rate at which new lenses become available is bounded by your model of what surfaces exist. You can't probe a surface you haven't named. The taxonomy of lenses in this post is bounded by what I've thought to label: general code review, external-reader hardening checklists, filesystem and IPC boundary, cross-platform runtime, HTTP active probing, MCP transport framing, end-to-end UX walkthroughs, version-string drift. There are categories of bug whose existence I haven't named yet. Those bugs are in the codebase right now, and Bughunt-2 won't catch them either unless it brings a new lens.

The honest version of the insight: an audit program's quality is determined by the size and diversity of its lens vocabulary, not by the number of rounds it has run. The cost of running an existing lens twice is almost always less productive than the cost of designing a new one — but designing new lenses is harder than re-running old ones, which is why most audit programs default to re-running.


#Per-finding walkthroughs: why the lens taxonomy holds

I'll walk through three findings in detail and two more in shorter form. F009 is the cleanest case of "structurally invisible to every prior lens." F044 is the cleanest case of "named the right attack, used the wrong defense." F018 is the cleanest case of "same lens, wrong scope." Each is a distinct failure shape that the matrix groups together as "no" but that mean different things in practice. F032 and F038 both follow the F009 pattern at different layers — writer/reader disagreements only an end-to-end run could surface — and get shorter treatments.

#F009 — the broken default-init flow

The bug: a default-init bosun session, on every release from v0.11 through v0.12.5, refused to accept any bosun_spawn call. The error message was "no live agent detected in session-1's worktree; bosun_spawn requires the caller to be running inside the named parent". The agent was alive. It was running in the parent's worktree. The gate that checks for it was computing the worktree's path one way; the agent was sitting at the worktree's actual path, computed a different way.

The root cause is one expression. bosun_spawn's parent-liveness gate at internal/mcp/tool_spawn.go:136 calls:

path := session.WorktreePathForLabel(repoRoot, *s.cfg, parent, "")

That trailing empty string is a round-timestamp parameter that v0.11 introduced when worktree naming migrated from <repo>-bosun-<sub> to <repo>-bosun-<timestamp>-<sub>. The migration was load-bearing for a different bug — operators were colliding when bosun re-created worktrees at paths that already existed. The new timestamp made each worktree path unique per round.

session.Derive, the reader that bosun status and bosun list use to find worktrees, got updated correctly. It calls git worktree list and matches by branch ref. It doesn't care about the path-naming scheme; it asks git directly.

bosun_spawn's liveness gate did not get updated. The empty-string call to WorktreePathForLabel deterministically reproduces the legacy <repo>-bosun-<sub> shape. The agent's actual cwd is the new timestamped shape. The lookup against proc.Running checks the wrong path. The gate refuses.

Six tagged versions shipped with this bug. The launch post for v0.11 celebrated the public release. bosun_spawn was the feature in v0.9 I'd been most excited about. The launch sprint commit messages mention closing two prior bosun_spawn bugs. F009 was a third bug, in a different file, that the sprint didn't touch.

Now the question: could any of the five prior lenses have caught this?

Pass 1 (general code review). A reviewer reading tool_spawn.go from top to bottom sees a call to WorktreePathForLabel(.., ""). The function's signature accepts a string; the empty string is a valid value; the call compiles. To recognize this as a bug, the reviewer would need to know that the empty string deterministically produces the wrong path, and know that the wrong path is wrong because the worktree naming scheme migrated, and connect those two facts to the proc.Running lookup that follows. The reviewer would have to hold three pieces of state in their head while reading two lines of code. General code reviews don't naturally do that.

Pass 2 (broader sweep). Same blind spot. Pass 2 had the benefit of a clearer mental model of the codebase, but the bug isn't visible from any local mental model. It's visible from running the system and watching the lookup fail.

Filesystem/IPC security audit. Worktree path resolution isn't the surface this lens probed. Even an exhaustive version of this lens would have walked the socket and filesystem boundaries and stopped before reaching the spawn logic.

Bundle A-E hardening sprint. The external reader's list didn't include this. Bundle A capped inputs; Bundle B bounded locks; Bundle C secured HTTP; Bundle D atomic-appended the ledger; Bundle E validated PIDs. None of the five touched the spawn-vs-status path-resolution disagreement. The lens didn't even have a category called "reader/writer disagreement on a migrated file-format."

Windows trial. The bug happens identically on macOS, Linux, and Windows. The Windows trial's lens is "what breaks on Windows specifically." A same-everywhere bug is invisible to a does-this-work-on-Windows comparison.

The bug needed L7 because L7 was the only lens that ran a full bosun init → bosun_spawn flow against the actual worktree git wrote. Every prior lens probed pieces. L7 ran the whole.

The deeper observation: when you migrate a file format, audit every reader. Not every writer — the writers are usually one place. The readers are everywhere. The reader you forget will silently produce the wrong answer; the migration's success criteria didn't include "every consumer agrees about where to look." This is a class of bug I now look for whenever a path-naming scheme or schema or version field changes. It wasn't on any prior lens's checklist because it's not a code-quality problem and it's not a security problem. It's a state-consistency problem, and it lives in the gaps between subsystems.


#F044 — naming the right attack, shipping the wrong defense

The bug: bosun serve accepted any value in the Host: header and returned the same response. A request with Host: evil.com against http://127.0.0.1:18080/api/show/session-1 returned the same JSON — including the session's BOSUN_BRIEF.md body and absolute worktree paths — as a request with Host: localhost.

This matters because the textbook defense against "loopback-only services accessible only on the operator's machine" is DNS rebinding. The operator opens any browser tab on a malicious site; that site's DNS rotates from a benign IP (which the browser pins as the origin) to 127.0.0.1 (low TTL); the site's JavaScript issues fetch('http://attacker-rebind.example:8765/api/show/session-1'); the browser treats the origin as attacker-rebind.example so the same-origin policy allows reading the response; the request lands on the operator's loopback bosun server because the IP is now 127.0.0.1. Without a Host-header check, the server has no way to distinguish that fetch from a legitimate one.

The fix is a one-middleware Host-header allowlist that returns 421 Misdirected Request for any Host not in the loopback set.

What makes this the interesting case in the matrix isn't the bug itself. It's that Bundle C — the HTTP hardening bundle — explicitly named the threat class this bug lives under. Bundle C's commit message and inline commentary described the defense as closing "the malicious-browser-tab vector for an operator who hits bosun serve while another tab is open." Bundle C shipped four response headers: X-Frame-Options: DENY, a Content Security Policy with frame-ancestors 'none', X-Content-Type-Options: nosniff, and MaxBytesReader on the request body.

Those four headers do real work. They close the iframe-based subset of "malicious browser tab" — an attacker site that tries to embed bosun's dashboard in a hidden <iframe> and read the rendered DOM. The headers are correct, the CSP is well-formed, the defense is shipped. The retrospective on Bundle C reads like a clean win.

But "malicious browser tab" isn't a defense scope. It's a threat category that fragments into at least four distinct attack mechanics:

  1. Iframe-based abuse — the attacker site embeds bosun's dashboard in an iframe. Blocked by X-Frame-Options: DENY and CSP frame-ancestors 'none'.
  2. Top-level navigation + cross-origin fetch() — the attacker site is the top-level document and uses fetch() to read bosun's JSON. The browser enforces the same-origin policy on the response. DNS rebinding defeats this by making the browser think the origin is the attacker's site even though the IP resolves to 127.0.0.1.
  3. postMessage-based exfiltration — the attacker site opens a window pointed at bosun and reads postMessage callbacks. Bosun doesn't use postMessage, so this mechanic doesn't apply.
  4. Stale-cookie cross-origin requests — relevant when the server authenticates with cookies, which bosun doesn't.

Three of those four mechanics either don't apply or are closed by the four headers. The fourth — DNS rebinding — is exactly the one that needs Host:-header gating, which Bundle C didn't ship.

What this looks like inside the lens taxonomy: Bundle C correctly identified the threat category, picked a real defense, and shipped it cleanly. The miss was in the implicit assumption that one defense closed the whole category. The category had members the defense didn't address. The defense's scope and the threat's scope weren't the same; nobody had written down the gap.

Could a prior lens have caught this? Walk through:

This isn't a story about a sloppy bundle. It's a story about the difference between named the right attack and enumerated the attack's members. The discipline that would have caught F044 is something like: for every threat category named in a hardening bundle's commentary, write down the smallest set of distinct attack mechanics in that category and confirm each one is closed by the shipped defense, or explicitly note which mechanics are out of scope. Bundle C didn't do that — and the unscoped member was the one that mattered.

The observation generalizes past audit: when a defense is framed as "closes X" rather than "closes the iframe-based subset of X," the framing absorbs threat-model space the defense doesn't actually cover. The retrospective reads as a win. The threat is still live.


#F018 — same lens, wrong scope

The bug: bosun's MCP transport reads framed JSON from a Unix socket using bufio.Reader.ReadBytes('\n'). There's no maximum-line guard, no read deadline, no per-connection memory ceiling. A single client that connects and sends 64 MB of 'A' without a trailing newline forces the daemon to allocate 64 MB of heap inside the bufio reader. The daemon's RSS climbs linearly with the byte count. Eight parallel connections at 1 GiB each makes a typical 16 GB laptop swap-thrash or OOM-kill.

The reproducer is one Python script. Open the socket, complete a clean MCP initialize handshake, then send N bytes of 'A' without a newline. The daemon's RSS in ps climbs in real-time, ~1 KB of RSS per 1 KB sent. Close the connection and RSS climbs higher again as the SDK's jsonrpc.DecodeMessage allocates more buffers during the failed final parse pass. Eight concurrent attackers compose linearly.

The fix is two lines. Cap the bufio buffer size and add a read deadline. Every production Go server that reads framed input does this. Bosun was missing it because the example code from the mcp-go SDK that bosun adapted was for the trusted-stdin case — stdin from a known parent process doesn't need an unbounded-buffer defense, and that assumption got carried into the Unix-socket transport where it shouldn't have.

What sets F018 apart in the matrix is that Bundle A was the right lens. Bundle A's stated goal was cap unbounded inputs. Bundle A capped the brief field on bosun_spawn. Bundle A capped a few other tool arguments. Bundle C added MaxBytesReader to the HTTP body. Three places where the unbounded-input lens was pointed; three caps shipped. None of them touched the MCP transport.

Look at where unbounded byte flows can enter bosun:

  1. Tool arguments — the JSON fields inside an MCP tools/call payload. Bundle A capped this.
  2. HTTP request body — bosun serve ingests POST bodies. Bundle C capped this with MaxBytesReader.
  3. MCP transport frames — the bytes between newlines on the Unix socket. Not capped. F018.
  4. Event-log appends — bosun writes events to a file. Length-bounded in the writer, not in any reader that consumes the file.

Bundle A's lens was the correct lens for (3). The exact same shape: bytes flowing in, no upper bound on accumulation. The fix at (3) is structurally identical to the fix at (1): add a bound. The reason Bundle A didn't touch (3) is the reason most audit lenses don't cover all the scopes their methodology could apply to — the external reader who informed Bundle A had walked the tool-argument code and noticed the missing cap there. They hadn't walked the transport layer. Bundle A was bounded by what its informer noticed.

This is the matrix entry I marked "no" instead of "partial." Bundle A was the right lens, but the scope it ran at was bounded by the informer's coverage, not by the lens's potential coverage. The lens's potential coverage is "every input that crosses a boundary into the daemon." Bundle A ran the lens at the tool-argument boundary. The transport-frame boundary was structurally invisible because nobody had written it down as a candidate scope.

The lesson, narrower than F009's or F044's: when a hardening sprint applies a single lens to a single scope, write down every other scope where the same lens could apply, and either run the lens at each one or note explicitly which scopes were skipped. The list of scopes-where-this-lens-could-apply is the inventory most teams skip. Bundle A would have included F018 in scope if the candidate-scope inventory had been written down — the lens was already understood; only the scope was missing.

A useful sub-discipline I started practicing after F018: every hardening bundle commit message lists the scopes the bundle applied its lens to and the scopes it did not. If a bundle caps tool arguments, the commit message names that the same lens hasn't yet been applied to the transport layer, the HTTP body (or notes Bundle C did it), and any future surfaces. Future audit rounds can use the explicit "not applied to" list as a candidate scope inventory. The lens persists in the project's institutional memory rather than getting re-derived from scratch each round.


#F032 and F038 — same shape as F009 at different layers

Two more HIGH findings rounded out Bughunt-1. I'll keep them brief because each follows the F009 pattern — writer/reader disagreement surfaced only when an operator runs the full workflow end-to-end.

F032 — exit-code lie on bosun merge conflict. When bosun merge runs and the underlying git merge returns conflicts, bosun's wrapper exits 0 and prints "merge complete" while leaving the operator's working tree wedged with unresolved conflict markers. The operator's next command — usually bosun cleanup — sees a successful prior exit code and tears down the worktree, taking the conflict resolution with it. The bug is a missed propagation of the git merge exit code through the wrapper's defer chain. Two-line fix.

L7 (end-to-end UX walkthrough) was the only lens that found this because the bug only manifests when an operator runs the full init → spawn → merge → cleanup flow against branches that conflict. None of the prior lenses ran that flow. The unit tests for the merge wrapper mocked the git invocation; the mock returned exit 0 because that's what the test was set up to check. The mocked test passed. The real merge would have failed if it had been run.

The lesson is the F009 lesson: end-to-end walkthroughs surface a class of bug no per-module review can. The cost of L7 was a few hours. The cost of shipping F032 across two tagged versions was every operator who hit a conflict losing their resolution work and not knowing why.

F038 — bosun init session-<word> accepted by the writer, rejected by every reader. When an operator runs bosun init session-clean, init validates the name, creates the worktree, creates the branch, and writes the entry to spawn-tree.json. Then bosun list shows nothing. bosun status session-clean says "not found." bosun show session-clean says "not found." Even bosun doctor reports "all checks passed." The session exists in every artifact init wrote, but every reader treats it as nonexistent.

The root cause is a format mismatch between init's validator and the readers' filter. Init accepts a broad label pattern. The readers expect the canonical system-generated session-N shape and silently drop anything else — treating user-provided session-<word> names as noise to ignore. The writer accepts a superset; the readers consume a subset; the difference is silent orphan sessions on disk.

This is the F009 shape at a different layer. Writer and reader disagree about what's valid, both are internally consistent, neither rejects the other's output explicitly. The disagreement becomes visible only when an operator runs the writer and then a reader and sees the difference. That's L7's dogfood lane. Every prior lens read code in chunks — one file, one function, one subsystem at a time. None put init's output beside list's filter to see whether they agreed.

The diagnostic discipline this points at: for any data type validated in multiple places across a codebase — session names, paths, URLs, version strings — list the validators and verify they agree on the accepted set. If a writer accepts more than its readers do, the difference is a silent orphan generator. The orphan won't error; it'll just be invisible.


#What Bughunt-1 didn't probe

It would be dishonest to claim Bughunt-1's lens taxonomy is complete. Three categories of bug I think are in bosun right now that Bughunt-1's eight lanes don't probe:

Operator-cognitive errors. Most of bosun's bugs that reach an operator aren't code bugs — they're situations where the system did exactly what it was asked and the operator didn't realize what they'd asked. The classic shape: bosun_spawn against a parent that hasn't acquired a claim yet. The agent gets a worktree, starts working, and merges back into a parent that's also been doing work in the same files. The conflicts are real and expected — but the operator didn't model the race because the spawn happened without a claim-acquisition step. No lens in Bughunt-1 catches this shape because it isn't a code bug; it's a UX-affordance bug. The system's behavior is correct; the system's documentation of what's about to happen is missing.

Goroutine lifecycle and shutdown races. Bughunt-1 ran one concurrency lane (L4) that checked lock acquisition, ledger appends, and socket relifecycle. It did not run a lane that probed goroutine lifetime under signal handling, process termination, and graceful-shutdown paths. Bosun spawns goroutines in at least eleven places — event publishers, ledger appenders, HTTP handlers, the MCP accept loop. I don't have a complete picture of which goroutines are owned by which contexts, whether they're cancellable, and whether bosun_cleanup waits for them all. A future audit lens — "for every goroutine spawned, identify its owner context and its termination guarantee" — would almost certainly produce findings. That lens didn't exist in Bughunt-1.

Performance regressions and latency tails. Bughunt-1's reproducers measured RSS for memory-DoS scenarios but didn't probe steady-state latency under load. If the MCP transport adds 200ms of P99 latency on a tools/list call once the event log crosses 100 MB, no lens in Bughunt-1 would catch it. The lens that would catch it — sustained-load profiling with latency histograms — isn't in the project yet.

This is the honest version of the lens-vocabulary argument: every round of audit expands the vocabulary, but the vocabulary is bounded by what you've thought to name. The categories above are the ones I can name because Bughunt-1 forced me to articulate the methodology. There are categories I can't name yet because I haven't done the audit round that would surface them.

What I think generalizes from this case study: the discipline of writing down what each lens is probing — including what it's structurally not probing — is the load-bearing methodology change. The rest is detail. If you can produce the equivalent of this post's "five lenses + what each was structurally blind to" table for your own codebase's audit history, you can plan the next round of audit by asking "which surface haven't we named yet" rather than "are we due."

What I think is bosun-specific: the lens vocabulary's exact contents. A backend service with no MCP transport wouldn't have F018. A CLI with no HTTP server wouldn't have F044. The specific lenses are domain-dependent. The taxonomy discipline — characterize lenses by what they probe, run new lenses rather than re-running old ones, write down each lens's structural blind spots — generalizes.

The conclusions in this post are strong as a hypothesis-generator: an audit program's quality is bounded by the diversity and explicitness of its lens vocabulary. The cleanest test of that hypothesis is to try producing the equivalent cross-round matrix for a codebase you've audited multiple times. If the matrix is similarly sparse with "no" entries, the hypothesis holds. If it's mostly "partial," the hypothesis weakens — sloppy methodology and missing lens become harder to distinguish. I haven't seen anyone else's cross-round matrix yet. If you produce one for your own audit history, I'd like to see what shape it takes.


← all writing