fix(brains): stop claiming automatic capture on claude.ai, document the real behavior [BRNS-MCPWEB-018] - #22
Conversation
core.md told the model "Capture is automatic … You do **not** need to call `save_chat_session`" with no client scoping. Capture is the ingest hook, and hooks do not run in claude.ai web chat — on either install path, the connector or the full plugin (support article 13837440: "Hooks and sub-agents run only in Cowork, so they appear grayed out in chat"). The sentence was therefore false wherever the hooks are absent, and it actively suppressed the one substitute that works there. It was also wrong in a shipped Claude Code configuration: brains-turn.sh exits early with no token, so capture is off for anyone who never set one — exactly where save_chat_session is the only path. Bump the core marker v=5 -> v=6 so the section is redelivered, and pin the new claim in both suites. The old text survived because nothing asserted it: the contract test's signal list covers routing and delegation but never covered capture. Add the signal plus a paraphrase-proof backstop on the unconditional form. [BRNS-MCPWEB-018]
Four prose surfaces advertised capture as a flat product capability. Capture is
the ingest hook; it runs in Codex and Claude Code and nowhere else, so on
claude.ai the claim was simply wrong — and it is what a user reads while
deciding whether to trust the plugin with their conversations.
Naming the mechanism is the fix: "hook-driven" stops the phrase asserting that
capture happens wherever the words are read.
- README.md:4 top blurb, plus a pointer to the new
web section
- .claude-plugin/marketplace.json:9 the marketplace catalog card — a
surface distinct from the manifest;
fixing only the manifest leaves this
one lying to anyone browsing
- plugins/brains/.claude-plugin/plugin.json:3
- plugins/brains/.codex-plugin/plugin.json interface.longDescription, which
said "automatic context and capture"
while Codex capture needs a token
None of these four strings is asserted by any test, which is how three of them
drifted into agreement on the same false claim. Pinning them is deliberately
left out of scope here; core.md now carries the pinned version of the rule.
[BRNS-MCPWEB-018]
…there
This README had no web section at all, so a claude.ai reader had nowhere to
learn that the capture the rest of the page describes does not reach them.
Both install paths are named. The custom connector works on any plan; the full
marketplace-sync plugin is paid-only and carries the skills. Hooks are inert in
chat on both, so neither delivers core.md or the ingest hook.
The capture copy is measured, not inferred (BRNS-MCPWEB-018, live on claude.ai):
- an explicit "save this chat to brains" works, verified end to end
- unprompted capture fired on ONE of five passive trials, with a direct
replication of the success failing in the same tool-access mode
- one trial ended by announcing a save it never performed
That last one is why the section tells the reader how to CHECK. A user cannot
distinguish a real save from a claimed one by reading the chat, so a disclaimer
alone would not have been honest.
The section links the install guide rather than reproducing its instruction
block. That block is 23k characters and already lives in ssvlabs/brains; copying
it here would make this the third hand-maintained expression of one ruleset.
Guard: the Claude Code slice now ends at the web heading instead of the shared
layout section. Without that the new section would inherit the Claude-Code-only
rules — the version-floor bans and the `bash -n` sweep — while none of its own
claims were pinned. It gets its own assertions instead, including both halves of
the capture finding: drop the explicit path and the section reads "capture is
broken", drop the caveat and it reads "capture just works". Each assertion was
mutation-tested to confirm it fails when its claim is removed.
[BRNS-MCPWEB-018]
The self-hosting notes were nested under "Install for Claude Code" while their last paragraph instructs Codex users: Codex has no `--config` mechanism, so `BRAINS_ENDPOINT` is the only lever a self-hosting Codex user has. A reader following the Codex path had no reason to read past that section and never saw it — their capture would keep posting to mcp.mybrains.ai while their tools talked to their own server, silently. Same shape as the capture claims this PR fixes: correct text, structurally unreachable by part of its audience. Promoted verbatim to a top-level section after "Shared layout", with a pointer from the Codex capture section where BRAINS_API_TOKEN is already being set. The body is unchanged byte-for-byte; only the heading level and position moved. [BRNS-MCPWEB-018]
Hosts update on SemVer precedence, not on content: without a bump this PR's core.md and copy changes merge and reach zero installed users. Both manifests move together — the contract test asserts they stay version-aligned, and the delivery guard requires forward movement on both for any change under plugins/brains/. Minor, matching 2.4.0 through 2.7.0, which were content changes of this size. Follow-up this creates, tracked on BRNS-MCPWEB-018: core.md's marker moved v=5 -> v=6, and the server's canonical section version needs a companion db/init migration in ssvlabs/brains modeled on 338_catalog_core_v5.sql. Ordering is fixed by that migration's own header — the public release must exist first, or devices get told to update to a marker the marketplace does not yet serve. So: publish this, then land the migration. [BRNS-MCPWEB-018]
An independent review of the committed diff found three real defects. All three were reproduced against source before fixing. 1. core.md still over-promised for a shipped configuration. The v6 paragraph said the ingest hook "saves every turn" in Codex and Claude Code and told the model not to call `save_chat_session` there. Capture is opt-in: brains-turn.sh:43 exits early with no token, and that token is `required: false`. brains-start.sh:27 cats core.md with no token check at all, so the users reading the promise are exactly the ones not getting it. Same bug class as the one this branch set out to fix — a false "automatic" claim suppressing the working substitute — in a configuration the first fix did not reach. The prohibition was also too broad: a blanket "do not call save_chat_session" suppressed the tool even when the user asked outright, which on claude.ai is the one path measured to work reliably. Now conditional on both axes, and the prohibition is narrowed to "routinely". Both conditions and the narrowed ban are pinned; the backstop regex also rejects "saves every turn". 2. The web section's nine signal assertions were paraphrase-gameable. The reviewer demonstrated it rather than asserting it: a rewrite opening "Capture is handled automatically for you … there is nothing to do" passed ALL NINE, because every keyword survived inside a trailing parenthetical while the meaning was reversed. This is the failure mode this file's own doctrine already names — "hand-written phrasing checks proved both evadable … the wording itself is the contract" — and the reason CLAUDE_INSTALL_REGION is pinned verbatim. The capture paragraphs now get the same treatment. The signal assertions stay alongside it: they give a precise failure on a legitimate edit, ahead of the whole-region diff. 3. Promoting "Self-hosting" past "## Shared layout" moved its shell block outside every slice this file sweeps, so nothing syntax-checked the one command a self-hoster copies. Widened the `bash -n` sweep to the whole README, which also covers the six Codex blocks — never swept at all, so the entire Codex install was unchecked. Also softened two claims the review flagged as unmeasured. "Works on any plan" went beyond the evidence: the research records Free-tier custom-connector access as our own guide's assertion, never product-observed, and still an open item. "Recall works on both" was measured on the connector path only. "Paid plans only" stays — that one is documented by Anthropic and verified. Each fix was mutation-tested: reverting any of the three, or softening the copy by a single clause, now fails. [BRNS-MCPWEB-018]
…ment the rule
No new capability or contract change here — corrected false claims, added a
docs section, strengthened test guards. Minor was picked by matching prior
bumps, but that precedent doesn't hold up: several past "fix" commits bumped
minor anyway, and the one clear feature bump ("New brains-build capability")
went patch. The guard itself doesn't care which digit moves.
Documents the convention going forward in generated-artifact-guard.sh's header
so the next bump has a rule to follow instead of inconsistent history to guess
from: fix -> patch, feat -> minor, breaking contract change -> major.
Five findings from review at the PR tip. Each was reproduced independently before fixing, and each fix now fails against the probe that exposed it. F1 (blocking) — core.md's capture guard was evadable on the surface a model EXECUTES. The three keyword signals plus a backstop banning the literals "capture is automatic" and "saves every turn" all survived this, green: **Capture.** Capture happens automatically for you in Codex and Claude Code — the ingest hook saves each turn (only when the user configured capture, which is the default, so you can assume it is on). … That is the false promise this branch exists to delete, restored and passing. core.md is now pinned VERBATIM, the same treatment the web section already had. Cheap here: editing core.md is already deliberate because it forces the v= marker bump and two suites' marker assertions. The backstop is also widened — "happens automatically" walked straight past a "capture is automatic" ban. F2 (blocking) — the web pin started at the capture paragraphs, so the intro, both install paths and the "Recall works" paragraph were keyword-guarded only. Enough to smuggle the claim back beside them rather than inside them: Capture works the same way: once connected, Claude saves your conversations to brains automatically, so there is nothing for you to do. The whole section is pinned now, heading to "## Shared layout". A partial pin was also easy to overstate when describing the guarantee. F3 — core.md said capture "is off without a token", which is wrong for a shipped Codex config: brains-turn.sh:35-42 scavenges the Authorization header from `codex mcp get brains --json` when no capture token is set, so a Codex user who configured MCP auth has capture ON while core.md told the model it was off. Capturing while denying it is the worse direction. Now "only where a capture credential resolves", with a "never deny it" counterpart to the existing "never promise it", and `list_pages type=chat_session` named as the way to actually know. core.md 2,762/3,000. F4 — "a server-driven inbox" survived unqualified in the very sentences this branch rescoped. brains-inbox.sh:35-36 carries the identical credential gate, so the inbox is false in exactly the same two configurations as capture; only capture got the qualifier. Both card surfaces now say "hook-driven turn-by-turn capture and inbox delivery". These descriptions were unpinned — which is how three of them drifted into agreement on one false claim — so the qualifier is pinned now, with a ban on reintroducing the bare form. F5 — the sweep comment claimed every block while the regex matched only ```sh. Widened to sh/bash/shell/zsh, plus an allow-list assertion so a shell dialect nobody listed fails loudly instead of going unswept. Not re-touched, clean per the same review: the version bump, char cap, region anchor uniqueness, CLAUDE_INSTALL_REGION coverage after the boundary move, the Self-hosting move, hygiene. [BRNS-MCPWEB-018]
Closes #14 (BRNS-CORE-038), hook-map half only. Massimo's 2026-07-28 note records the act-contract-mirror half as already landed in #13 (b4ba6f7). Three holes, each reproduced against this branch's own tip before fixing, each one letting published behaviour reach zero users with every suite green: 1. AN EVENT COULD BE DELETED. Only PostToolUseFailure and SessionEnd were asserted for Claude — SessionStart, UserPromptSubmit and Stop never were. Deleting SessionStart passed plugin-contract AND inbox-v2, and SessionStart is the only delivery path for core.md (brains-start.sh:27 cats it). So the verbatim copy pins this branch just added proved core.md's text was correct while nothing proved it ever loads. Guard-works is not guard-runs, and the hole was directly under this PR's own work. 2. AN EVENT COULD BE GUTTED. `includes("PostToolUseFailure")` is satisfied by the key existing, so `PostToolUseFailure: []` passed with its own assertion intact — it guaranteed a name, not a hook. Setting both SessionStart and PostToolUseFailure to [] passed. 3. THE MAPPING WAS UNPINNED. Pointing SessionStart at brains-end.sh passed. Codex carried all three identically — the ticket names only the Claude map, but one flattening helper feeds both — so both are checked the same way now. Replaces the includes-list with a per-client event -> script constant, asserted as an exact set. That closes all three at once: a deletion or an addition fails the set, an empty array fails the per-event command count, and a swapped script fails the mapping. Adding an event is now a deliberate two-line edit, the same shape as the copy pins above it. The check is beside hookScripts(), not inside it, on purpose: that helper flattens every event into one list, which is what its callers want and what loses the event identity this needs. A vacuous `SessionStart: []` contributes nothing to the flattened list, so it is invisible to anything built on it — which is how hole 2 survived. The three positive `includes` assertions are gone, subsumed by set equality. The two Codex negatives stay, deliberately redundant: those absences are a capability fact, and the set check would report adding them as a set mismatch rather than saying why. Mutation matrix, 29 cases across both clients — delete each event, empty each event, mis-map each event, add a bogus event — plus three vacuous shapes (`[]`, `[{hooks: []}]`, `[{matcher}]` with no hooks key). All 32 now fail; 0 holes remain. Test-only, no version bump. [BRNS-MCPWEB-018]
Normalizes the last three `BRNS-*` refs in the contract test to the form already used for the hook-map work: the traceability lives in the commit body and the PR, and the comment carries what a reader can actually act on. These three sat behind a gap in the hygiene gate rather than a decision — its hard-fail list was keyed on area codes and MCPWEB was never added, so these only ever warned while a CORE ref hard-failed. The gate now matches any `BRNS-` ref on any area, which is what surfaced them. No GitHub issue exists for the board row, so there is no `#N` to substitute. The measurement date replaces it where the anchor was evidentiary — more useful in source than an opaque ID a reader cannot open — and is dropped entirely where the following lines already spell out the two configurations. Test-only, comments only. No assertion changed.
…ry, fix the trust-prompt capture claim
nir-ssvlabs
left a comment
There was a problem hiding this comment.
LGTM. No other reviews on this one, so I went after the things that would actually be critical rather than the prose: the publish mechanics, the one runtime file, and whether the new guards are as total as they claim.
Verified, not assumed:
- Publish is well-formed. Both manifests move
2.7.0 → 2.7.1, aligned and forward, and patch-shaped matches the convention this PR itself documents (fix(…)→ patch).core.mdis not one of the digest-pinned artifacts (only the twoSKILL.mdfiles are), sogenerated/capability-catalog.jsonbeing untouched is correct here rather than an omission — worth stating because a reviewer pattern-matching on "artifact changed, manifest didn't" would flag it wrongly. - The only runtime file is comment-only.
brains-turn.shchanges its header block and nothing else — no gate, no payload, no exit path moved. So the behavioural surface of this PR really is copy plus tests. - The README composition pin is genuinely total, which is the load-bearing claim given this file's history of evadable pins.
normalizeRegion(readme) === README_REGIONS.join("\n\n")is whole-file equality, not a set ofincludes— so there is no inter-region seam to write into, and the failure mode the two adversarial rounds kept finding is structurally gone rather than patched. The per-region pins staying ahead of it purely as locators is the right call. - Ordering for
v=6is the safe direction, and this PR is the correct first step.db/init/338_catalog_core_v5.sql's own header states the rule verbatim — the public release must exist before the catalog migration deploys, or devices are told to update to a marker the marketplace does not serve. Nocatalog_core_v6migration or PR exists yet, which is right.
One consequence worth stating plainly, because it bears on when this fix actually lands for users: publishing 2.7.1 fixes the claim for new installs, but existing devices keep serving core.md v=5 — with the "Capture is automatic … do not call save_chat_session" text — until the companion catalog_core_v6 migration ships and the SessionStart drift check starts emitting the update command. Since suppressing save_chat_session on claude.ai is the specific harm this PR exists to remove, the fix is only half-delivered at merge. BRNS-MCPWEB-021 is named in the description, but it's the step that makes this reach the installed base — worth filing as soon as this publishes rather than treating it as cleanup.
The measured-rate section is the right way to report n=3/n=5, and keeping the two Instructions-field conditions unpooled is the honest form. Agreed that collapsing those two README paragraphs into one reassuring sentence should be treated as a regression.
Approving.
Five public surfaces in this repo advertised conversation capture — and the inbox — as things that just happen. Both run only through the bundled hooks, and both are off without a credential, so the claims were false in two shipped configurations: claude.ai web (no hooks at all, on either install path) and any CLI install with no capture token (
brains-turn.shexits early; the token isrequired: false).Two more of the same class — the
/install/claude-webDoneCard and the walkthrough's paste step — live inssvlabs/brainsand were fixed in ssvlabs/brains#1230, not here.core.mdwas the worst of them, because it is injected into a model's context rather than read by a human: it told the model capture was automatic and that it should not callsave_chat_session— suppressing the only mechanism that works where the hooks don't run.What changed
core.md— the capture rule is now conditional on a credential resolving, and the prohibition is narrowed to "routinely". The blanket form suppressedsave_chat_sessioneven when the user asked for it outright, which on claude.ai is the one path measured to work. Markerv=5→v=6.hook-driven turn-by-turn capture and inbox deliveryreplaces the unqualified phrasing in the plugin manifest, the marketplace catalog card (a distinct surface: a fix touching only the manifest leaves the card lying to anyone browsing), the Codex manifest'slongDescription, and the README blurb.## Install for claude.ai web— the README had no web section at all, so a web reader had nowhere to learn that the capture the rest of the page describes does not reach them. Names both install paths, links the install guide rather than forking its 23k-character instruction block, and states what capture actually does there.Self-hostingpromoted to a top-level section — it was nested under Install for Claude Code while its last paragraph instructs Codex users (BRAINS_ENDPOINTis the only lever Codex has). A reader on the Codex path never saw it, and their capture would keep posting to the wrong host silently. Body moved byte-for-byte.generated-artifact-guard.sh's header —fix(…)is a patch,feat(…)a minor, a declared breaking change to the plugin's contract a major. The guard only ever required forward SemVer movement, on any digit, so which digit to move was never written down. The header says plainly that it is a convention rather than something the script enforces, and that past bumps in this repo do not follow it — going forward, not a description of history.brains-turn.shheader comment that still said "Nosave_chat_sessioncall needed".What capture actually does on claude.ai — measured, not inferred
Verified live on a real account, both tool-access modes, checked against
list_pagesrather than the chat UI. All of it measured on 2026-08-05 — before carrier B landed, so these are the rates the tool description had to improve on, not the rates after it:Explicit ask — "save this chat to brains" — works, reliably.
Unprompted capture is unreliable, and the rate depends on which block was in the Instructions field, so the conditions are kept apart rather than pooled:
Pooled across both blocks that is 1 of 5, but the pooled figure understates the real carrier and the split is the honest form. Deliberately not expressed as a percentage: n=3 and n=5 do not support one.
One trial said "Now saving this to brains" and never called the tool. That is why the README tells the reader how to verify a save rather than only disclaiming — from the chat alone, a real save and a claimed one look identical.
The README copy says exactly this, in two separate paragraphs. Collapsing them into one reassuring sentence would be technically defensible and would leave a user believing unprompted capture is dependable; please treat that as a regression.
Guards
Keyword assertions proved evadable twice during review, both times demonstrated rather than argued — a rewrite opening "Capture is handled automatically for you" passed every signal check while reversing the meaning. So the load-bearing copy is now pinned verbatim, the pattern this file's own doctrine already prescribes for this claim class — and after review probes kept finding unpinned text beside every partial pin, the pin unit became the whole artifact:
CORE_BODY— core.md entire, its delivery execution-proven: deletingbrains-start.sh'scatline now fails the suiteREADME_REGIONS— the README as the ordered composition of ten pinned regions; text added anywhere in the file fails compositioninterface.shortDescriptionwas one) cannot ship unreadHook event map (#14). Three separate holes let published behaviour reach zero users with every suite green: an event could be deleted (only 2 of Claude's 5 were asserted — deleting
SessionStart, the only delivery path forcore.md, passed both suites), gutted (includes("PostToolUseFailure")is satisfied by the key existing, so[]passed with its own assertion intact), or mis-mapped (SessionStart→brains-end.shpassed). Codex carried all three identically. Replaced with a per-client constant pinning each event's complete declared shape — matcher (or its required absence), exact command strings, group count, and the whole group array by canonical JSON — so a narrowed matcher, a rider command, or a straytimeoutfails too.Worth stating plainly: those holes sat directly under this PR's own work. The verbatim pins proved
core.md's text was correct while nothing proved it ever loads.Also widened the README
bash -nsweep to the whole file — promoting Self-hosting had moved its block outside every checked slice, and the six Codex blocks were never swept at all.Verification
2.7.0 → 2.7.1on both manifests (patch-shaped).This does not close BRNS-MCPWEB-018 — but no PR is outstanding
The ticket closes on claims matching reality. The code for that is all merged:
brains-plugins) — the five surfaces above, plus the guards;/install/claude-webDoneCard and walkthrough copy, and carrier B:save_chat_session's eager tool description now carries the capture rule itself, the last always-on surface that had never been tried;CORE_CAPTURE_REGIONwork, found independently, which is the strongest evidence that the evadable-keyword-pin problem was not a one-off here.Both monorepo PRs are on
stage, neither onmainyet.What remains is verification, not another PR. Once stage promotes, the passive rate has to be re-measured on prod — 8–10 passive trials per tool-access mode, not 3. At a 1-in-3 base rate a three-trial run cannot distinguish a real improvement from noise, which is the same pooling-and-small-n mistake that would make this PR's own copy wrong in the reassuring direction.
If that measurement shows carrier B raises the passive rate, the README's "only sometimes" paragraph becomes wrong and should be retracted; whoever runs the verification should re-read that paragraph against the result.
BRNS-MCPWEB-021, after this publishes:
core.md's marker moved tov=6, which needs a companiondb/init/NNN_catalog_core_v6.sqlinssvlabs/brains, modeled on338_catalog_core_v5.sql. Ordering is fixed by that migration's own header — the release must exist first, or devices are told to update to a marker the marketplace does not serve.BRNS-MCPWEB-020, deliberately deferred: the full install-path documentation. It is blocked on two things nobody has measured — what a paid user with both the plugin and the older pasted block active actually gets, and whether a Team or Enterprise member can add a marketplace by URL at all, which is the mechanism that whole path depends on. Writing that section now would mean publishing install guidance ahead of its own verification, which is the defect this PR exists to remove.
Known, and not addressed here:
brains-turn.shreuses Codex's persisted MCPAuthorizationheader as a capture credential when no capture token is set;brains-inbox.shdoes not. A Codex user with header auth and noBRAINS_API_TOKENtherefore has capture on and the inbox off — so the two are credential-gated but not by the same credential set. Too fine a distinction to carry in card copy, and worth its own ticket if that configuration turns up in practice.