docs(analysis): evaluate a Claude Code plugin as a fourth transport - #129
Conversation
|
Reviewed by fact-checking every structural claim against current code/docs, and by settling the doc's own stated "Gate 0" empirically — building a real probe plugin and a real Claude Code session, checked against the literal outbound wire bytes rather than assumption. CI is red
Gate 0 — the doc's own central open question — is now empirically confirmed, in the doc's favorThe doc names this exactly right as the gate everything else depends on, and flags two competing hypotheses: persistence (the doc's hoped-for case) vs. single-request-only (the "counter-hypothesis" that "collapses the whole proposition"). We ran the actual experiment: a Result: an object-shaped {"tool_use_id": "toolu_bdrk_01SC6Vg1gZAtY1WEVe7xsbsR", "type": "tool_result", "content": "SENTINEL-OBJ-999-REPLACED", "is_error": false}— the real content never appears in any resent To be precise about what this does and doesn't establish: it confirms the doc's own preferred hypothesis, it doesn't refute a claim the doc made — the doc never asserted persistence, it flagged non-persistence as the risk to rule out. This is real, first-party evidence — stronger than what the doc itself cites for this point (docs silence) — and it upgrades the central recommendation from a reasoned bet to an empirically-grounded one. We also went further and shipped a real, working The cache-envelope claim is correct, confirmed at the binary level and empirically, with one precision nuanceExhaustively checked: no hook input or output, across all 11 tested events (including three the doc didn't test — One nuance worth a one-line sharpening in the doc: Check 1's own cap figure is off by 3-4x, and mischaracterizedThe doc's "~10,000-char cap" doesn't match what actually happens. Measured directly (bisected with distinct-letter-per-5000-char-segment payloads): up to ~30,000 chars, Structural claims — all four checked directly against code/data, all hold up
Bottom linePartially the doc's own framing needs sharpening, not partially wrong: the doc is right that no plugin surface touches the cache envelope (now confirmed at the binary level and empirically, today), it correctly anticipated its own biggest operational risk (the silent-shape-mismatch failure mode, hit exactly as predicted), and its central recommendation — gate the build on Gate 0, then build — is now backed by a real positive result rather than an open question. The net technical implication worth stating plainly for whoever reads this next: a Claude Code plugin can do real, persistent, content-scoped context compaction (demonstrated and measured above) but categorically cannot do envelope-scoped cache management — so the strongest version of this proposal is "plugins can replicate the offloader half of this repo," not "plugins can replicate what the proxy does." |
f99bab0 to
9678582
Compare
|
Thanks — that review did more than check the doc; the Gate 0 experiment and the cap measurement are both things I flagged and couldn't settle. All four points addressed in CI — fixed, and verified rather than assumedTook option (a), exclusion, but by moving the file rather than widening Against your read that it's closer to Verified with a real Gate 0 — folded in, and it changes the shape of the docThe wire capture is stronger evidence than anything the doc had, so the "three things to verify" section is now a resolved-gate section plus the risks that actually remain, and the recommendation stops being conditional. Recorded the The object-vs-string gotcha is now a hard requirement on You're right that this confirms rather than refutes — the doc flagged non-persistence as the risk to rule out. Kept that framing. The cap figure — my error, corrected"~10,000 chars" came from the Both precision notes taken
The frozen-measurement caveat now covers the class, not just the 7,017 figure — Scope claimAdopted your wording as the lead of the recommendation, because it's more precise than mine was: a plugin can replicate the offloader half of this repo, persistently and measurably, and categorically cannot replicate the cache-management half — not "a plugin can do what the proxy does." Assisted-By: Claude Opus 5 (1M context) noreply@anthropic.com |
|
Reviewed against main This is the best-reasoned document in the current batch. It identifies the one constraint that The CLI's own error string independently corroborates the object-shape gotcha at I also confirmed the capability boundary the doc rests on: hooks see tool name/input/output, Three things to fix before this functions as a decision record. 1. Blocking — #141 did not build this transport, and nothing in either PR says soThis document analyses and recommends a { "hooks": { "SessionStart": [ { "hooks": [ {
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/start-proxy.sh",
"timeout": 60 } ] } ] } }No And the headline blocker at Three consequences: (a) the "fourth transport" framing is wrong for what shipped — #141 is transport Fix: add a section distinguishing (A) plugin-as-interceptor ( 2. Blocking — a warm-regime figure is quoted as the expectation for a definitionally cold-regime userThe −34.1% figure is not wrong, and I want to be precise about that: benchmark harnesses run The defect is the transfer. Provenance:
Three further facts bound the cold case further: the env snapshot is captured once per session So
3. Blocking — Gate 0 is closed in two places and still open in two othersThe review commit rewrote §0 and the Recommendation but missed "What the plugin is actually for" Worth fixing because it strengthens the recommendation
Smaller items
Nothing here re-chases refuted work: Recommendation: rewrite, do not close as superseded"Superseded" would be factually wrong. #141 built a different thing, so the transport this document Carry a header note so the doc cannot be read as the justification for #141 — e.g. "Evaluates VerdictApprove with changes. Must-fix: items 1, 2 and 3. The two "strengthens the recommendation" items |
Asks whether the `components` core can ship as a Claude Code plugin — a thin transport beside `proxy/`, the AuthBridge plugin and `adapters/bifrost`, reusing components via configuration. Not a replacement for the proxy. One fact decides the whole evaluation. No plugin surface can read or write the outbound request: across all ~34 hook events there is no field that rewrites `messages[]`, `system[]`, `tools[]` or `cache_control`, and plugin `settings.json` accepts only `agent` and `subagentStatusLine`, so a plugin cannot even point a session at the proxy. What IS interceptable is one tool result at the moment it is produced, via `PostToolUse` → `updatedToolOutput`. That draws the line between content-scoped and envelope-scoped work, and it is the line between the two component families. Offloaders distribute. `cmdfilter`, `format`, `toon`, `extract`, `collapse`, `smartcrush`, `skeleton` and `dedup` are pure functions from one tool output to a shorter one, which is exactly what the hook hands over and accepts back. Two get better than they are in the proxy: the hook supplies `tool_input.command` and `tool_input.file_path` instead of making `cmdfilter` and `skeleton` infer both from transcript text. Cache management does not distribute, and this is the finding worth arguing with. `cachesplit` restructures the top-level `system` array; `cacheinject` reasons over `messages[]` positionally and emits request metadata; the keepalive on `feat/keepalive-strategies` must originate a request that byte-exactly reproduces a prefix and price it from `CachedTokens`, which only the provider's response carries. None of that is a tool output — it is the envelope, assembled after the last hook runs and never persisted. Verified rather than assumed: a session transcript carries `messages` and `toolUseResult` and no system prompt or tool schemas (`scripts/inspect_transcript.py`). The failure modes are asymmetric too, which is the deeper reason. A wrong offload wastes one expand round-trip, bounded and type-enforced reversible. Wrong cache work inverts: a mistimed keepalive creates at 1.25x instead of refreshing at 0.1x, a breakpoint over budget is a 400, a representation flip inside a cached prefix re-writes the suffix at 11.5x. Envelope work has no merely-no-saving fail direction. The compensating result is that the defensive half of our KV-cache layer stops being necessary rather than being ported. `state.go` names its own premise — an offloader must re-emit identical bytes "otherwise the agent (which re-sends the ORIGINAL each turn)" flips the representation. A hook rewrites the output before it enters the transcript, so the agent never holds the original. Freeze/replay, `MaxCachedIdx`, `Tracker`, `frozen_flips` and sticky ids have nothing left to defend, and `extract_llm`'s sampling nondeterminism stops disqualifying it from repair. Also `PreCompact`/`SessionEnd` replace `proxy/agentcompaction.go`'s string match against Claude Code 2.1.215 internals, which has a documented reachable false positive. What is given up: `cachesplit` (-34.1% cost, 0% -> 96.7% hit, and in every preset), plus `mask` (27.5% Terminal-Bench, 12.5% SWE-bench) and `failed_run`, both of which rewrite EARLIER messages and so cannot work at a hook that fires once at birth. On implicit prefix-cache backends (vLLM/llm-d) the cache loss is zero, because `prefixsplit` is already a no-op there. Also documented: permanence cuts both ways (an expand's restored original joins the transcript for good, so the plugin wants a more conservative pipeline than the proxy), `/stats` cost tiers are unobtainable so plugin mode cannot be benchmarked the way the proxy is, and the whole thing is gated on one unverified fact — whether `updatedToolOutput` persists into the transcript. If it does not, the proposition collapses to an expand-only MCP server. That experiment is named as gate 0 and should run before any adapter code. On DAM: land the proxy in the gateway. DAM is harness-plural and its egress already matches our gateway credential model, so a Claude-Code-only plugin covers one harness of four and none of the bring-your-own-ACP case. Ship the plugin as the Claude-Code-session layer on top, never as the DAM integration. Docs only — no code, no behavior change. Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com> Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…p figure Review found one broken thing, settled the doc's central open question, and caught a wrong number. All four points addressed. CI was red: mkdocs build --strict rejected docs/analysis/claude-code-plugin.md because it was in no nav entry, and there is no docs/analysis convention to copy. Moved it to docs/superpowers/specs/2026-08-30-claude-code-plugin-transport-design.md, which exclude_docs already covers and whose stated purpose — "working plans/specs ... not published site content" — is what this doc is. That also matches the dated-spec naming of the keepalive design doc and of the local-distribution proposal in #130. The reviewer offered the alternative of a nav entry under Results:; exclusion is the better fit, because publishing a page that evaluates a plugin we have not built would read on the docs site as a product that exists. Verified with a real strict build (1.45s, no warnings) and a negative control: the same file under docs/analysis/ still aborts strict mode, so the move is the fix rather than something incidental. Gate 0 is closed, in the doc's favour. Review ran the experiment the doc asked for — a PostToolUse hook replacing a Bash output with a sentinel, captured through a raw-logging reverse proxy so the evidence is the literal outbound body rather than the transcript file — and the replacement persists and is resent verbatim on later turns. A working collapse plugin then measured -6,285 tokens on a real session, appearing as the same reduction on turn 1's cache-write and turn 2's cache-read, which is what separates a permanent reduction of resent context from a one-turn display trick. The "three things to verify" section becomes a resolved-gate section plus the risks that actually remain, and the recommendation stops being conditional. The predicted failure mode arrived on the first attempt, which is worth recording rather than smoothing over: updatedToolOutput must be the object tool_response shape ({stdout, stderr, interrupted, isImage, noOutputExpected} for Bash), and a bare string is silently ignored. That is now a hard requirement on adapters/cchook — emit the object shape, count your own rejections — instead of a general warning. The 10,000-character cap figure was wrong, and "cap" was the wrong frame. That number came from the additionalContext / systemMessage / plain-stdout cap, which does not govern this field. Measured: verbatim and uncapped to ~30,000 chars, real threshold in (30,000, 40,000] and most likely 32,768, and above it neither truncation nor rejection — the CLI's ordinary large-output handler produces a ~2,260-char <persisted-output> wrapper with a 2KB preview and a disk pointer while the local record stays intact. For an oversized hook emission that is a token-cost improvement, not a hazard. Two precision fixes. The envelope claim now records that it was verified by a stronger method than the docs (11 events exercised live, plus the installed CLI's own hookSpecificOutput validation schema: 33 events, 22 with output fields, none touching the envelope) and carries the nuance that promptCacheTtl / CLAUDE_CODE_PROMPT_CACHE_TTL is a reachable cache-TTL lever via settings or env — not via any hook or manifest field, and session-wide rather than per-breakpoint — so a flat "zero cache control" reading would overstate the gap. And the component figures quoted throughout (cachesplit's -34.1%, mask's 27.5%/12.5%, the ~7,017-token system block) are now labelled as this repo's frozen historical measurements, quoted accurately but not re-verified against current traffic by this evaluation. Finally, the recommendation leads with the scope the reviewer articulated better than the doc did: a plugin can replicate the offloader half of this repo, persistently and measurably, and categorically cannot replicate the cache-management half — not "a plugin can do what the proxy does." Docs only — no code, no behavior change. Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com> Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…by regime, and add rtk Addresses the three blocking items from review, plus the cheap ones. 1. Two candidates, and only one is a new transport. The document analysed plugin-as-interceptor (PostToolUse -> updatedToolOutput) without ever weighing plugin-as-installer-for-the-proxy (a skill writing env.ANTHROPIC_BASE_URL into the user's own settings.json), which is what #130 proposes and which gives up none of the component set. New section states the distinction, the component tables are scoped explicitly to (A), and a header note stops this page being read as the justification for #130. The headline blocker is rescoped: a plugin *manifest's* settings.json accepts only agent/subagentStatusLine -- that is not the same as "a plugin cannot set ANTHROPIC_BASE_URL", and the difference is the option that won. Title said "third transport" against the body's "fourth"; now fourth throughout. 2. cachesplit is priced by regime. -34.1%/0%->96.7% is a warm-regime figure -- one Terminal-Bench task x 3 trials (cacheinject.md:203, and :209 says treat it as one task, not a fleet average) with the A/B run back-to-back inside the TTL (dashboard.md:219). Cold interactive traffic is $0.0298 across 1,127 sessions / 11,361 requests (dashboard.md:204). Both are right; the document transferred the warm number onto a definitionally cold-regime user, called cachesplit "the best-evidenced component we have" where cacheinject.md:209 says otherwise, and claimed the figures were "not re-verified against current traffic" -- which for cachesplit is false. Corrected, the Anthropic row reads close to the vLLM row. The DAM recommendation stands but is now justified on spendgate/tenancy/limits and harness-plurality rather than on that figure. 3. Gate 0 is closed consistently. The ranked go/no-go section still called the case conditional and told the reader to run the experiment that already ran. Also: rtk is this architecture and we benchmarked it as a full arm -- -9.0% billed cost, reward-neutral, zero request-path latency (results/rtk.md:11) -- so expected value has a floor instead of resting on one session's -6,285 tokens, and the doc now claims the edge it was missing: rtk is a shell hook, so Read/Grep/Glob bypass it, while matcher ".*" does not. mask quoted as ~27.5-29.5%, single-task replay, never enforced in a benchmark arm. extract_llm 8x -> 82x underwater. Hook events ~34 -> 33. Dropped the guessed 32,768 threshold, keeping the measured (30,000, 40,000] bracket. Noted that inspect_transcript.py reports key names, not record types, so its `system` record type is a transcript event and not a request system array. Keepalive branch reference replaced with #126; extract_llm_sweep (#118) added to the offload table. inspect_transcript.py moved to deploy/harbor/, this repo's convention for analysis Python. Rebased onto main (3ebc65d). Docs-only; no code and no behaviour change. mkdocs is unaffected because docs/superpowers/ is excluded from the site build (mkdocs.yml:88-90); the three in-page anchors were validated against the file's own heading slugs and all other links are external. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
9678582 to
cdab623
Compare
|
All three blockers addressed in 1. Two candidates — the section you asked forYou were right that this was the real defect: the document analysed one use of the plugin surface and never weighed the other, and the one it skipped is the one that won. There's now a The headline blocker is rescoped rather than softened: a plugin manifest's One deliberate deviation from your text. You wrote the section against #141's hook manifest as it stood at review time; that PR is actively being worked on and its contents have already moved since, so pinning the doc to a snapshot of it would go stale immediately. The doc describes (B) from #130's spec and says the authority on what (B) ships is #130 and its implementing PR, never this page. The design distinction — which is what you were actually after — is unchanged. 2. Regime, namedChecked your provenance and it holds all the way down, so this is corrected rather than caveated. So the Anthropic row now reads "worth −34.1% warm, $0.0298 cold" and lands close to the vLLM row; the "best-evidenced component we have" line is gone, since The DAM recommendation stands, but on the grounds you identified as the load-bearing ones — 3. Gate 0Fixed in the ranked go/no-go section, which was the part still giving the pre-review answer: "and it is conditional" and "rests entirely on the unverified persistence check" are gone, and rtk — the best catch of the roundYou're right that omitting it was the significant gap, and worse than you put it: And the edge you pointed at is now claimed: rtk is a shell hook, so Smaller items
CI should stay green: docs-only apart from the file move, Assisted-By: Claude Opus 5 (1M context) noreply@anthropic.com |
What this is
A design evaluation, docs only — no code, no behavior change. It asks whether the
componentscore can ship as a Claude Code plugin: a fourth transport besideproxy/, the AuthBridge plugin andadapters/bifrost, reusing components via configuration rather than reimplementing them.Framed deliberately as a deployment/transport question. Nothing here proposes replacing the proxy.
The one constraint
No plugin surface can read or write the outbound request. Across all ~34 hook events there is no field that rewrites
messages[],system[],tools[]orcache_control, and pluginsettings.jsonaccepts onlyagentandsubagentStatusLine— so a plugin cannot even point a session at the proxy.What is interceptable is one tool result at the moment it is produced:
PostToolUse→hookSpecificOutput.updatedToolOutput.Why this applies to offloaders but not to cache management
This is the part worth reviewing, and it is a structural argument rather than a list of blockers. The plugin sits upstream of Claude Code's session transcript; the proxy sits downstream of it. The full request body exists only on the arrow between them.
Offloaders are content-scoped.
cmdfilterfilteringterraform plannoise,collapsewindowing a 2,000-line log,skeletonreducing a source read — each is a pure function from one blob of text to a shorter blob of text. No neighbouring message, nosystemarray, no provider response.PostToolUsehands over exactly that blob and accepts a replacement, so the unit of work and the unit of interception are the same size. Two components get better than they are in the proxy: the hook suppliestool_input.commandandtool_input.file_path, socmdfilterandskeletonstop inferring both from transcript text.Cache management is envelope-scoped. Not one message — the shape of the entire prefix:
cachesplitrestructures the top-levelsystemarray, which components never see; onlyapplydoes, from the raw body.cacheinjectreasons overmessages[]positionally (divergence point, turn-stable anchors, the 4-slot budget counted acrosssystem+tools+messagestogether) and its output is acache_controlkey — request metadata, not content.feat/keepalive-strategiesmust originate a request that byte-exactly reproduces a prefix, and price the decision fromObservation.CachedTokens, which only the provider's response carries.None of that is a tool output. It is the envelope — assembled after the last hook runs and never written to disk. Verified rather than assumed: a session transcript carries
messagesandtoolUseResultand no system prompt or tool schemas (scripts/inspect_transcript.py). Sincetools+systemis the front of the cumulative hash and, perprefixsplit.go, ~7,017 tokens of it, the missing piece is not a skippable tail — it is what you would have to reproduce first. (That token figure, like the component percentages below, is this repo's own recorded measurement quoted from a code comment — accurate, but frozen and not re-verified against current traffic here. The argument turns on its order of magnitude, not the digits.)So the rule is not lossy-vs-lossless or cheap-vs-expensive:
There is a second asymmetry in how the families fail, and it is the deeper reason. A wrong offload wastes one expand round-trip — bounded, and reversibility is type-enforced. Wrong cache work inverts: a mistimed keepalive creates an entry at 1.25× instead of refreshing at 0.1×, a breakpoint over budget is a hard 400, a representation flip inside a cached prefix re-writes the suffix at 11.5×. Envelope work has no fail direction that is merely "no saving", which is exactly why it wants the host that sees the whole request and the provider's answer to it.
The compensating result
The defensive half of the KV-cache layer stops being necessary rather than being ported.
state.gonames its own premise: an offloader must re-emit identical bytes "otherwise the agent (which re-sends the ORIGINAL each turn)" flips the representation. A hook rewrites the output before it enters the transcript, so the agent never holds the original. Freeze/replay,MaxCachedIdx,Tracker,frozen_flipsand sticky ids have nothing left to defend, andextract_llm's sampling nondeterminism stops disqualifying it from repair.PreCompact/SessionEndalso replaceproxy/agentcompaction.go's string match against Claude Code 2.1.215 internals — which has a documented reachable false positive (our own docs page quoting the phrase).What is given up
cachesplit— −34.1% cost, 0% → 96.7% hit rate, and it is in every preset. The best-evidenced component in the repo.mask(27.5% Terminal-Bench, 12.5% SWE-bench) andfailed_run— both rewrite earlier messages, which a hook firing once at birth cannot do./statscost tiers — they come from the provider's response, so plugin mode cannot be benchmarked the way the proxy is, anddeploy/harbor/*.pyhas nothing to parse.On implicit prefix-cache backends (vLLM/llm-d) the cache loss is zero, because
prefixsplitis already a no-op there. On Anthropic it is real.Gate 0 is closed — persistence confirmed
The proposal's one blocking unknown was whether
updatedToolOutputpersists into the transcript or applies to a single request. Review ran it: aPostToolUsehook replacing a Bash output with a sentinel, captured through a raw-logging reverse proxy so the evidence is the literal outbound body rather than the transcript file. Turn 2,messages[3]:{"tool_use_id": "toolu_bdrk_01SC6Vg1gZAtY1WEVe7xsbsR", "type": "tool_result", "content": "SENTINEL-OBJ-999-REPLACED", "is_error": false}The real content appears in no resent
tool_result, and a later turn asked for the exact output answered with the sentinel atinput_tokens: 69— from resent history, not regeneration. A working collapse plugin then measured −6,285 tokens on a real session, showing as the same reduction on turn 1's cache-write and turn 2's cache-read: a permanent reduction of resent context, not a one-turn display trick.So the defensive KV-cache half really is free, and the recommendation is no longer conditional.
The predicted failure mode arrived on the first attempt.
updatedToolOutputmust be the objecttool_responseshape —{stdout, stderr, interrupted, isImage, noOutputExpected}forBash— and a bare string is silently ignored. That's now a hard requirement onadapters/cchook: emit the object shape, count your own rejections.And a number in this PR was wrong. I cited a "~10,000-character cap" on
updatedToolOutput; that figure governsadditionalContext/systemMessage/plain stdout, not this field. Measured: verbatim and uncapped to ~30,000 chars, real threshold in (30,000, 40,000] (likely 32,768), and above it neither truncation nor rejection — the CLI's ordinary large-output handler emits a ~2,260-char<persisted-output>wrapper with a 2 KB preview and a disk pointer, local record intact. For an oversized hook emission that's a token-cost improvement. "Cap" was the wrong frame.On DAM
Land the proxy in the gateway. DAM is harness-plural (Claude Code, Pi, Bob, Codex, plus any ACP runtime) and its egress already matches our gateway credential model, so a Claude-Code-only plugin covers one harness of four and none of the bring-your-own case. Ship the plugin as the Claude-Code-session layer on top of the gateway, never as the DAM integration.
Status after review
CI is green: the doc moved to
docs/superpowers/specs/, whichexclude_docsalready covers and whose stated purpose — "working plans/specs … not published site content" — is what this is. Verified with a realmkdocs build --strict(1.45s, clean) plus a negative control confirming the same file underdocs/analysis/still aborts strict mode. The alternative offered was a nav entry underResults:; exclusion is the better fit, since publishing a page evaluating a plugin we haven't built would read as a product that exists.Also sharpened per review: the envelope claim now records the stronger verification method (11 hook events exercised live, plus the installed CLI's own
hookSpecificOutputvalidation schema — 33 events, 22 with output fields, none touching the envelope), and carries the nuance thatpromptCacheTtl/CLAUDE_CODE_PROMPT_CACHE_TTLis a reachable cache-TTL lever via settings or env — not via any hook or manifest field, and session-wide rather than per-breakpoint — so "zero cache control" would overstate the gap. Component figures quoted throughout are now labelled as frozen historical measurements.The scope claim, stated the way the review put it better than I had: a plugin can replicate the offloader half of this repo — persistently and measurably — and categorically cannot replicate the cache-management half. Not "a plugin can do what the proxy does."
Related: #130 (local distribution — how an evaluator installs any of this).
Assisted-By: Claude Opus 5 (1M context) noreply@anthropic.com