Two rebake loops and the bug that was my own laptop

Four findings in one day on dario's template pipeline. Three of them were not what the first report said they were, and in each case the thing that changed the fix was measuring before writing code.

What the pipeline does

@askalf/dario replays Claude Code's request shape so any Anthropic-compatible tool can run on a Claude subscription. To do that it captures the real thing: it spawns your installed claude binary against a loopback proxy, reads the outbound request, scrubs anything host-specific, and bakes the result into the package.

Two properties matter and they pull against each other. The bake has to be faithful, because a divergence from the real wire shape is the whole failure mode. And it has to be reproducible, because a bundle that differs by machine means every maintainer's bake reports every other maintainer's bake as drift, forever.

Everything below is a story about the second property.

Finding one: the guard was keyed on the thing that could change

It started with an observation I filed against my own tooling: one capture in six came back carrying text that belonged to a different session. Prose that has no business in a captured prompt.

My first assumption was that nothing guarded this. That was wrong, and finding out took ten minutes of reading rather than a day of writing. The scrubber already stripped the sections Claude Code populates with host state, the CLAUDE.md contents, the memory directory, the user's email, the current date, and the release gate already failed the build if any of them survived the strip.

So the hole was much narrower than the report. It was this: both halves read the same list. The stripper and the detector are keyed on the same set of section headings. Rename a heading upstream and the stripper silently strips nothing, the detector silently flags nothing, and once the paths inside have been scrubbed, the leftover prose carries no path for any other rule to catch. Two silent failures, stacked, and the existing code comment already worried about exactly that possibility without anything acting on it.

The fix is a second detector keyed on the wrapper prose instead of the heading, so a renamed heading still gets caught.

The part that would have broken everything

The obvious marker to key on is the string CLAUDE.md. It appears in the leaked text. It is specific. It is also present in Claude Code's own genuine system prompt.

I counted before committing to any marker, against the real baked artifact:

  • CLAUDE.md: 1 hit in the system prompt, 1 in each model variant, 3 more in the tool descriptions
  • <system-reminder>: 2 hits in the system prompt

Either one, used as a marker, would have failed every single build from the first run. A guard that fires on legitimate content is worse than no guard at all: it trains you to bypass it.

The markers that survived the count are the full wrapper sentences, the ones a tool emits when it injects an instruction file, which never appear in the prompt itself. The rejected candidates are now pinned as explicit non-markers in the test suite, so nobody “tightens” the list later and breaks the pipeline.

Finding two: provenance, and why the nonce is in the URL

The capture accepted the first request whose URL contained /v1/messages, with nothing tying it to the child process it had just spawned. The port is ephemeral, so a collision is improbable, but improbable was the entire defence, and once a foreign request was captured, nothing downstream could tell it from a real one.

The obvious fix is a per-capture nonce in the API key: set it, then require the captured request to carry it. I measured first, and the measurement moved the design:

  • Claude Code honours a path segment in its base URL. Point it at http://127.0.0.1:PORT/<nonce> and it requests /<nonce>/v1/messages.
  • On a subscription install it authenticates with authorization: Bearer and sends no API-key header at all.

So a key-borne nonce would have done nothing on the most common install, while changing the auth path for the other kind. Changing what the child authenticates with is the one thing a fingerprint capture must never do: it can change what the child sends, which is the thing being measured.

The nonce went in the URL. It touches nothing about auth, and the check fails closed: no nonce, no capture, and the build exits non-zero rather than baking something it cannot attribute.

Finding three: the loop that was my own laptop

Then two slots started re-baking on a loop: a bot opening a rebake pull request, which bumps a version, which cuts a full release: multi-arch container build, signed attestation, npm publish, deploy. Twice in forty minutes.

They looked like the same problem. They were not.

The first was real. A feature flag that the upstream toggles by remote config, not by version, moved four times in twelve hours: off, on, off, on. Every flip is genuine wire shape, and every flip cut a release. That one is suppressible, strip it from the baked base, exclude it from the comparison, and state the trade openly, which is that the bundle now differs from the live wire by one flag whenever the flag is on. Cheaper than a release per flip.

The second looked identical and had nothing to do with upstream. Diffing my bundle against the bot's:

mine (Windows) : ", Glob, Grep"     "the Glob or Grep"
bot  (Linux)   : (removed)          "`find` or `grep` via the Bash tool"

The captured prompt names the tools that exist on the machine that captured it. Six bytes, in one slot, with every other slot byte-identical. So my bake and the bot's bake each reported the other as drift and re-baked, indefinitely.

That is the third channel of a disease whose first two I had already fixed in this same package: once through filesystem paths, once through the OS and architecture headers. Same shape every time, a host-specific value riding into an artifact that is supposed to be host-independent.

And unlike the first two, this one cannot be normalised. The correct text depends on which tools the caller has, which the bundle cannot know, and rewriting the upstream's prose would be inventing wire shape instead of replaying it. The tools list was already unioned across platforms; nothing can union a sentence.

So the fix is procedural rather than clever: the bake refuses to write the bundle from a non-Linux host, with an override for a deliberate local bake, and the read-only drift check stays available everywhere. Detecting drift from any machine is useful. Writing the artifact from any machine is the bug.

Finding four: the one I got wrong twice

A base prompt length kept appearing that did not match the bundle: 279 extra characters, occasionally.

I called it flapping and held a release to measure it. Eight consecutive captures came back byte-identical. So I said the base was stable, which was the second wrong call: eight identical samples of a value that varies about one time in ten is a clean streak, not stability.

Then I tested two mechanisms and disproved both. Warm session state: twenty consecutive captures, all clean. Contamination from the previous spawn's model: four captures, three of them following a different model, all clean.

What finally answered it was noticing the measurement already existed. The drift checker runs hourly in CI and logs the captured length every time. Twenty runs, twenty-six hours, on Linux:

  • the scrubbed length is rock stable within each era, and stepped exactly once, a genuine five-character upstream change
  • the raw length wobbles across a forty-byte range the entire time, which is the git status block the scrubber exists to remove
  • the anomaly I had been chasing never appears once

It only happens on my machine. Which retires the remote-config theory I had posted an hour earlier, and means the earlier provenance fix closed the network path but not whatever channel this is.

The consolation is structural rather than lucky: the non-Linux bake guard from finding three already prevents this from reaching a published artifact, because only Linux can write the bundle now, and Linux is twenty-for-twenty clean. A potential supply-chain problem degraded into a local annoyance because of a fix aimed at something else.

It is still open. An unidentified channel is worth keeping open even when its current payload is a harmless paragraph.

What generalises

Three of four findings were not what the first report said. A foreign capture was inherited context. Upstream drift was my own bake host. An unstable prompt was git noise around a stable value.

In each case the cheap step was the same and came before writing anything: count the marker against the real artifact, measure what the process actually sends, read the existing guard before adding one, and look for a measurement that already exists before building one. The expensive version of this day would have been four confident fixes, at least two of them wrong, one of which would have failed every build immediately and one of which would have suppressed a signal that was telling the truth.

The rule I would keep: a guard you have not tried to make fire on legitimate input is not yet a guard. It is a hypothesis with a commit attached.

All of it is verifiable in public: the guards, the suppression, the host gate, the disproven hypotheses and the still-open issue are in the dario repository and its release history (as of 2026-07-26).

We build and run software on AI infrastructure that shifts under it: agents, release pipelines, the guardrails and automation that keep them shipping without a human in the loop. If you're putting agents near anything that matters, that's the kind of thing we're good at holding steady.

Start a conversation →
← All writing