Repository navigation
Description
Hello!
I ran an audit with ClaudeCode Opus 4.8.
Apologies in advance if this is noise / AI-slop.
It might be worth 5 mins of your time to run it thru your own agentic AI.
❤️
π
dario wire-fidelity — empirical findings
Independent measurement of the claim that dario's requests match Claude Code "byte-for-byte" / "packet-identical." Every result below is measured on one machine and reproduced; the harness scripts are included so anyone can re-run.
Rig: macOS arm64 · claude 2.1.214 (Mach-O standalone, embeds Bun/1.4.0, node-compat v26.3.0) · Node v23.7.0 · public Bun 1.0.9 / 1.3.14 / canary(→1.4.0) · dario @ HEAD (2901bc9).
Instruments (all local, no sudo, zero subscription cost except where noted):
capture-ch.mjs+ja3.mjs— grab a client's TLS ClientHello (first TCP payload, cleartext) without completing the handshake → JA3.mitm-proxy.mjs— env-scopedHTTPS_PROXYCONNECT proxy that TLS-terminatesapi.anthropic.comwith a trusted local cert, logs decrypted HTTP/1.1 (raw header order+case + body, token redacted), returns a stub. Only the one subprocess is affected.collector.mjs+hdrtest.js/btest.js— local TLS server to compare Node-vs-Bun fetch behavior.- Real CC
/v1/messagescaptured by running the user's authenticatedclaudethrough the MITM (stub reply ⇒ nothing billed). dario egress captured by running dario from source with its upstream const pointed at the collector and a dummy account (shape-only; no real creds, no cost).
Layer 0 — TLS ClientHello (JA3). Deterministic (3/3, no GREASE).
| Client | Runtime | JA3 |
|---|---|---|
claude 2.1.214 |
Bun 1.4.0 (embedded) | e97f5146a7009cc2918b50e903b6ff8d |
| bare Bun 1.3.14 / canary | BoringSSL | e97f5146… (byte-identical to CC) |
| bare Bun 1.0.9 | BoringSSL | 2ae7eb4b… (diverges: +3DES, +ECH, +padding) |
| bare Node v23.7.0 | OpenSSL | 53321030… (52 ciphers) |
- Node ≠ Bun, massively. The TLS gap is closable with public Bun ≥ 1.3.14 (reproduces CC exactly). No public
bun-v1.4.0exists (canary line). - Gap dario misses:
runtime-fingerprint.tstreatsbun-matchas "under Bun at all" and never checks the Bun version's JA3. On this box dario would auto-relaunch under the Bun 1.0.9 on PATH and report a greenbun-matchwhile emitting2ae7…≠ CC'se97f…. → measure JA3, or pin/warn a min Bun version.
Layer 1 — protocol. HTTP/1.1 (ALPN http/1.1 only). No H2/HPACK axis; header order+case are literal on the wire. CC does not cert-pin.
Layer 2 — the actual /v1/messages request: CC vs dario (measured)
Header order — real CC (SDK insertion order, preserved on the wire):
Accept · Authorization · Content-Type · User-Agent · X-Claude-Code-Session-Id · X-Stainless-Arch · X-Stainless-Lang · X-Stainless-OS · X-Stainless-Package-Version · X-Stainless-Retry-Count · X-Stainless-Runtime · X-Stainless-Runtime-Version · X-Stainless-Timeout · anthropic-beta · anthropic-dangerous-direct-browser-access · anthropic-version · x-app · x-client-request-id · Connection · Host · Accept-Encoding · Content-Length
dario under Node (undici):
host · connection · accept · content-type · user-agent · x-claude-code-session-id · x-stainless-*(8) · anthropic-beta · anthropic-dangerous… · anthropic-version · x-app · Authorization · x-client-request-id · accept-language · sec-fetch-mode · accept-encoding · content-length
→ replays dario's captured order, BUT: all names lowercased; Authorization appended near the end (pos ~19, vs CC pos 2); host/connection hoisted to front; undici injects accept-language + sec-fetch-mode (CC never sends these); accept-encoding: br, gzip, deflate (CC: gzip, deflate, br, zstd).
dario under Bun 1.3.14 (fetch sorts):
Accept · Authorization · Content-Type · User-Agent · anthropic-beta · anthropic-dangerous… · anthropic-version · x-app · x-claude-code-session-id · x-client-request-id · x-stainless-*(8) · Connection · Host · Accept-Encoding · Content-Length
→ alphabetically sorted; x-*/x-stainless-* lowercased and placed after anthropic-*, unlike CC. dario's captured header_order is discarded by Bun's fetch.
Root cause (measured): plain fetch() under both public Bun 1.3.14 and canary (== CC's 1.4.0) sorts headers (zeta,accept,m-custom,Authorization → Accept,Authorization,m-custom,zeta). Real CC on the same Bun is not sorted ⇒ the Anthropic SDK bypasses fetch's Headers sort via raw dispatch. dario uses fetch(), so it cannot reproduce CC's header order under Bun — its header_order machinery is a no-op there. Under Node it survives but undici adds its own fingerprint.
Verdict grid
| Property | dario/Node | dario/Bun 1.3.14 | matches CC? |
|---|---|---|---|
| TLS JA3 | ✗ (5332…) |
✓ (e97f…, if Bun≥1.3.14) |
Bun only |
| Body top-level key order | ✓ | ✓ | yes |
metadata.user_id shape, system-prompt/billing-header injection |
✓ | ✓ | yes (rebuild works) |
x-client-request-id, x-claude-code-session-id present |
✓ | ✓ | yes |
| header name casing | ✗ lowercased | ✗ lowercased | no (CC mixed-case) |
| header order | replayed but Authz misplaced | ✗ alphabetized | no |
| transport-injected headers | ✗ accept-language,sec-fetch-mode, host/conn front |
✓ clean | Bun only |
Accept-Encoding value |
✗ br,gzip,deflate |
(Bun default) | Node no |
x-stainless-arch/os |
✗ x64/Linux on fallback path |
✓ arm64/MacOS (live) |
live-capture only |
anthropic-beta set/order |
advisor-tool-2026-03-01 vs CC advanced-tool-use-2025-11-20; missing extended-cache-ttl-2025-04-11; oauth flag at tail vs pos 2 |
same | no |
What dario gets right (credit)
Body key order exact; live value overlay (UA (external, sdk-cli), pkg 0.94.0, runtime v26.3.0) correct; arch/os correct on the live path; injects the CC/Agent-SDK system prompt + x-anthropic-billing-header + thinking/context_management/output_config to rebuild a bare client into CC shape; sends x-client-request-id, session-id, presence pings; TLS reproducible under a recent Bun.
Bottom line
- "byte-identical body": substantially TRUE (key order + serialization). ✅
- "packet-identical" on the wire: FALSE as stated. Best achievable today is structurally close under a recent Bun, still distinguishable by: header-name casing (lowercased), header order (Bun's fetch sorts; dario's replay is defeated),
anthropic-betaset, and — under Node — undici-injected headers. Plus the unverified-Bun-version JA3 gap. - The single highest-leverage fix: dario cannot match CC's header order/casing through
fetch(). Matching CC requires the same raw-dispatch path the Anthropic SDK uses (or an http/1.1 client that preserves order+case). Everything else (casing, Authz position, beta set, JA3 version check) is incremental.
Bugs/observations to file
bun-matchdoesn't verify Bun version/JA3 (false-green on old Bun).fetch()transport re-sorts (Bun) / injects (Node) headers, silently defeatingheader_orderreplay — the documented v3.13 wire-fidelity axis is a no-op on the recommended runtime.- Header names emitted lowercase; CC is mixed-case.
- OAuth
Authorizationappended at tail, not CC's position 2 (template carries capture-artifactx-api-keyinstead). anthropic-betadrift:advisor-tool-2026-03-01vs realadvanced-tool-use-2025-11-20; missingextended-cache-ttl-2025-04-11.- Bundled-fallback
x-stainless-arch/os=x64/Linux(clobbers correct values when live capture unavailable). - Hardcoded fallback
x-stainless-runtime-version: v24.3.0(real: v26.3.0). - Context: CC itself phones home (
/api/event_logging/v2/batch163–252 KB + Datadog) — relevant to "nothing phones home" positioning.
Irreducible
Adversary feature set is unobservable (we can prove wire-identical across observed layers, never "undetectable"); results are point-in-time (CC/Bun version drift).
Activity
askalf commentedon Jul 18, 2026
This is the good kind of issue — thank you. Ran it back through our own agentic pass against
2901bc9and currentmaster, and it holds up. Definitely not slop; the included harness scripts are what made it checkable, so extra thanks for those.What reproduced against the code:
#1 — version-blind
bun-match. Confirmed.classifyRuntimeFingerprintreturnedbun-matchfor any Bun version behind a hardcoded "matches Claude Code" label, with nothing checking the JA3 floor — a relaunch into an old Bun on PATH false-greens exactly as you describe. Shipped a fix in #814: below1.3.14(your measured floor) is nowbun-ja3-unverified, so doctor and the startup banner warn and--strict-tlsrefuses.#2 — header order/casing defeated by
fetch(). Confirmed, and this is the real one.orderHeadersForOutbound()builds the ordered pair-array specifically to replay CC's sequence and then hands it straight tofetch()— the docstring literally asserts "the array form is the one variant where wire order is part of the spec," which your measurement disproves on both runtimes (Bun sorts; undici lowercases and injectsaccept-language/sec-fetch-mode). So that machinery is a no-op on the wire. One clarification in our favor and one against: header order was never a documented axis — the six indocs/wire-fidelity.mddon't include it, so it's aspirational code rather than a shipped claim — but it is dead effort as written, you're right. The bind you surfaced is the part worth sitting with: Bun gives the matching JA3 but itsfetchsorts with no raw-dispatch escape, while Node lets you control order/casing through undici's low-level dispatch but carries the wrong JA3. You can't win both axes in one process on the current stack. Matching CC's raw dispatch under Node and eating the JA3 cost is the direction; tracking it separately since it's an architectural change, not a patch.#3 — lowercased names. Confirmed, same root cause as #2.
#6 / #7 —
x64/Linuxand runtimev24.3.0on the fallback path. Confirmed but fallback-only, as you flagged yourself. The live-overlay path is correct; these only bite bundled-only / no-live-capture installs. Thev24.3.0is stale against yourv26.3.0— version drift the cc-drift bot should be chasing. Real, low priority.Two I'd push back on slightly:
#4. Right that OAuth
Authorizationlands at the tail and the captured order carries anx-api-keycapture artifact — but it's moot on the wire today becausefetch()destroys order regardless. It becomes real the moment #2 is fixed, so it rides along with that work rather than standing alone.#5. Partly.
advisor-tool-2026-03-01is in our template and differs from youradvanced-tool-use-2025-11-20capture, so the template does need a drift reconcile. But two things your dummy-account/shape-only capture wouldn't have shown: in production the beta set is merged with the client's ownanthropic-betaheader, so a real CC client contributes its flags and dario ships a superset rather than the wrong flag; andextended-cache-ttl-2025-04-11isn't missing — it's mirrored conditionally when the client sends it, matching CC's send-the-pair behavior. So "drift," yes; "missing," no.#8. Fair. That's positioning, not a bug, and worth being straight about.
Net: you're right that "packet-identical on the wire" doesn't survive contact. Body fidelity is strong (glad that came through clearly), header-layer fidelity is defeated by the transport, and the honest framing is closer to "byte-identical body, structurally close headers under a recent Bun." Taking #1 now (#814), #2 as the architectural track, #5 as a template reconcile. Genuinely appreciate the rigor. ❤️
fix(tls): flag Bun below the JA3-verified floor instead of a false-gr…
p-i- commentedon Jul 18, 2026
Ah, that really made my day.
:)
I forced Opus to verify by experiment, which shaved off a layer of bullshit.
Do feel welcome to close out the issue, and thanks for your work!
askalf commentedon Jul 18, 2026
Following up on #5 — I need to correct myself. I said the template "needs a drift reconcile." That's not right, and the real story is more interesting.
I checked what our account's CC 2.1.214 actually emits: the self-hosted drift-watch runner re-baked the template from a live 2.1.214 capture earlier today, and it still carries
advisor-tool-2026-03-01. That bot explicitly diffs the installed CC's per-modelanthropic-beta(it assertsbetaForModelreproduces the captured header) and isn't flagging drift. So on our account,advisor-tool-2026-03-01is what 2.1.214 sends.Your capture on the same 2.1.214 showed
advanced-tool-use-2025-11-20. Both are correct — it's remote-config bucket variance: Anthropic flips these flags server-side within a single CC version (afk-mode does the same thing), which is exactly the drift class our template-watch exists to chase. Different account → different rollout → different flag, same binary.And it self-corrects in prod regardless: dario live-captures each machine's own CC to
~/.dario/cc-template.live.jsonand forwards whatever that deployment's bucket sends, rather than hard-coding one. The baked set is only the fallback — so there's no stale flag to reconcile; the base already tracks whichever bucket the operator is in. Good catch either way, since it's what made me go verify it.feat(transport): raw HTTP/1.1 over Bun.connect for faithful header or…
askalf commentedon Jul 18, 2026
Correction, and I owe you a clean one. In my reply above I said the "you can't win both axes" bind dissolves — that raw HTTP over
Bun.connectgets CC's JA3 and CC's header order together. That was wrong, and it was my error, not a subtlety.My JA3 harness had a bug: its mode switch only recognized
tls, so thebun-connectmode silently ran thefetchpath. I measuredfetchtwice and read it as "Bun.connect==fetch." Measured correctly:fetch(what CC uses): JA3d871d02c, extensions include ALPN (16), status_request (5), SCT (18), padding (21)Bun.connect: JA3203503b7, extensions0,23,65281,10,11,35,13,51,45,43— none of thoseAnd no
Bun.connectTLS option adds them (alpn/ALPNProtocols/alpnProtocolsare ignored or the wrong type).Bun.connect==node:tls== a leaner profile that can't be pushed to fetch's exact ClientHello.So the bind is real, exactly as your #2 framing implied. On Bun you get CC's JA3 (via
fetch, which sorts headers) or raw header order/casing control (viaBun.connect, wrong JA3) — not both. Header-order fidelity isn't reachable on Bun without regressing the TLS axis. I've reverted the change (it was default-off, so nothing shipped enabled) rather than ship a knob that trades one fingerprint for the other.Your original point stands unqualified: "packet-identical on the wire" isn't achievable through
fetch, and the header axis specifically isn't fixable on the recommended runtime. Thanks for the rigor that made this worth chasing.Revert "feat(transport): raw HTTP/1.1 over Bun.connect for faithful h…
feat(wire): forward a genuine CC client's identity headers instead of…
docs: credit the 13 contributors the table had missed (#1253)