The Bristlenose extension
The whole connect UX, after the hand-paste path failed three times in one sitting. Proposal for discussion — nothing here is built. 31 July 2026.
0 · What happened tonight THE MOTIVATION
Three attempts to connect Claude Desktop by hand. All three failed, all from one cause, and the second one cost unrelated settings.
- JSON has no append-safe form. Adding a key means a comma on the previous line and none after. No wording of an instruction removes that.
- The failure is destructive. Claude Desktop doesn't skip the bad key — it rewrites the whole settings file and drops what it can't parse.
- We can't help. The App Sandbox keeps Bristlenose out of another app's container: reading that file is as blocked as writing it.
- The restart costs more than we said. That config is the host app's own state — quitting to apply it can end the researcher's own working session.
1 · The new flow PROPOSED
-
Bristlenose ▸ Settings ▸ MCP Agents
Or Bristlenose ▸ Connect MCP agent… in the menu bar, which goes to the same place. Setup is a once-ever, app-level act — it does not depend on which project is open, or on any project being open at all.
-
The Claude Desktop tab is one button
Connect Agent — Claude DesktopAgents read whichever project is selected in Bristlenose
Now showing: IKEA discovery · 6 sessions · 214 quotes
Claude DesktopClaude CodeChatGPT & CodexGeneric MCPAdds Bristlenose to Claude Desktop as an extension. Claude Desktop will ask you to confirm.
BBristlenoseAn agent has asked about this project recentlyInstall Extension…Done- The footnote is gone entirely — a fourth tab replaced it.
“Works with any MCP-compatible agent” was doing the work of a missing tab, and beside an
Install button it was actively wrong (a
.mcpbis Claude Desktop-only). The picker is now the statement, and it's actionable. - No address, no token, no JSON — nothing to mistype, so nothing to get wrong.
- The Anonymise switch stays exactly where it is: it's the researcher's per-project compliance choice and has nothing to do with transport.
- Cancel became Done. Nothing here is cancellable any more — you either install or you close the sheet.
- The footnote is gone entirely — a fourth tab replaced it.
“Works with any MCP-compatible agent” was doing the work of a missing tab, and beside an
Install button it was actively wrong (a
-
Claude Desktop runs its own install
Claude DesktopInstall “Bristlenose”?This extension will be able to run on your computer and access the Bristlenose project you have open.CancelInstallApproximation of Claude Desktop's own dialog — we don't draw this, it draws it. That's the point: the consent moment belongs to the app being modified, not to us. -
Done — it appears where Figma appears
Claude Desktop → Settings → DeveloperLocal MCP serversAdd and manage MCP servers that you're working on.Bristlenose running This server is managed by an extensionFigma running This server is managed by an extension
The two entry points — wording and placement
LLM tab is part of the proposal, and it earns its churn.
- It makes the two axes obvious. Bare “LLM” next to “Agents” reads as two flavours of the same thing. LLM Provider vs MCP Agents says it plainly: which model does the analysis (in), versus which agents can read the results (out).
- “LLM” names a domain, not a setting — it tells you the topic but not what you do there. The pane is a provider list with a detail pane (the Mail Accounts shape), so Provider is what it actually configures, and it's already Bristlenose's own word.
- The singular/plural asymmetry is deliberate. One Provider — only one is active at a time, by design. Several Agents — Claude Desktop and Codex can both be connected at once. The labels encode a real difference rather than just matching shape.
- Cost, stated: one string × 20 locales, right after the seeding pass. Small, and better now than once the cohort has muscle memory.
- Icon: the antenna — the same glyph as the sidebar badge. One concept, one symbol, two surfaces.
- Last in the list. Appearance is chrome, LLM and Transcription are the engines; who can read your work is a fourth concern, and last is least disruptive to muscle memory.
| Setup — once | Enablement — per project | |
|---|---|---|
| Where | Bristlenose menu | Project menu / right-click |
| Wording | Connect an Agent… | Turn On / Turn Off Agent Access |
| Ellipsis? | Yes — opens a window | No — it toggles immediately |
- “Connect an Agent…” over “Connect MCP Agent…”: the menu is where a researcher looks when they don't yet know the jargon, and the pane they land on says MCP everywhere. Put the plain word where discovery happens and the precise word where the work happens.
- It's honest that the item only opens Settings — that's the same shape as Add Account…, which also opens a pane rather than adding anything.
- The Project menu's old “Connect Agent…” retires. Its job is now enablement, and Turn On / Turn Off Agent Access names it truthfully — a verb swap, not a window. (“Share” was rejected: on macOS it means the share sheet, and its punctual tense misreads a standing permission. See the decided menu above.)
- Vendor-neutral on purpose: never “Connect Claude”. The whole premise is any MCP agent.
Before / after
| Hand-paste (today) | Extension (proposed) | |
|---|---|---|
| Steps for the researcher | Copy · find file · edit JSON · fix commas · save · quit · relaunch | Click Install · confirm |
| Ways to get it wrong | Comma, brace, wrong nesting, wrong file, stale port | None available |
| Cost of a mistake | Claude Desktop rewrites its settings file | — |
| Where the token lives | Plaintext in another vendor's config, on the pasteboard, in Time Machine | 0600 file in Bristlenose's own container, read at connect time |
| When the port changes | Config is dead; re-paste | Nothing to do |
| Switching project | Re-paste per project | Nothing to do |
| Revoking | Hunt through a foreign config | Bristlenose deletes one file |
2 · Every state the sheet can be in PROPOSED
Paste in any terminal:
Add to ~/.codex/config.toml:
bristlenose mcp-proxy, then every client's config becomes address-free and
token-free — a command, not a secret. Claude Code and Codex stop needing a re-paste when the port
rotates too, and the three tabs finally say the same thing in three dialects.
- Cost: one CLI subcommand wrapping the same proxy logic. Open question 3 in the plan.
- Without it, these two tabs keep tonight's URL + token payload — which works, but leaves the token in two more config files.
The fourth tab — Generic MCP
Give these two values to any MCP-compatible agent:
The port number changes each time Bristlenose starts — copy the address again after a restart. Your token stays the same.
- It's also the fallback that makes “replace hand-paste” safe. A researcher whose extension silently fails isn't stranded — and the escape hatch hands two values to a client that accepts them, rather than sending anyone back into another app's JSON. Honest limit: it's a fallback across clients, not within Claude Desktop.
- Scope is already correct under Option B: each project's serve has its own port and its own token, so a URL+token pair names one project.
- It keeps the rotating-port problem — no proxy in front to re-resolve — so this is the re-copy-after-restart tier.
- That caveat is
info.circle/ secondary, not a yellow caution triangle. The HIG reserves warnings for negative consequences and data loss (“don't warn when data loss is the expected result”), and our ownMessageKind.WARNINGmeans something soft-degraded in this run — which hasn't happened. It would also show on every single view of this tab, and permanent warnings stop being read. The words carry the load, not the colour: name the moment and the remedy.
Settings ▸ MCP Agents — with and without a project selected
- Installing is a higher-order concern than which project is open. A researcher who opens Settings from the Welcome screen is doing setup — the pane has no business changing shape, showing an empty state, or telling them to go and pick something first.
- “Agents read whichever project is selected in Bristlenose” stays as the header. That switching projects changes what Claude can see is not obvious and not intuitive — saying it plainly is the v1 answer.
- “Now showing: …” is a for-example line: concrete, and it makes the header's rule legible by demonstrating it. With nothing selected it simply disappears — no placeholder, no dash, no “no project selected”. Absence is the information.
- Cut with it: the old “Bristlenose isn't running a project” sheet state. That was a hangover from the per-project sheet, and under a global home it was answering a question nobody asked.
3 · What the agent sees PROPOSED
Bristlenose isn't serving anything yet VERIFIED IN SPIKE
Serving — and the project is named in the answer
The full message vocabulary — five states, found by running it
Not a dialog and not a Bristlenose window: our proxy returns these as ordinary tool results, Claude reads them, and the model relays them in the conversation. So they're written addressed to the model, and the researcher sees Claude say it.
| State | Signal | What the agent tells the researcher |
|---|---|---|
| Ready | 200 | The real answer, from real quotes. |
| Not shared / BN closed | no handshake | “Bristlenose isn't open, so there is no study data available… Do not answer from memory.” |
| BN starting | port silent | “Bristlenose is starting — ask again in a moment.” |
| No agent support in this build | 404 | “This copy of Bristlenose was built without agent support. No setting will enable it.” |
| Credential rejected | 401 | “Bristlenose rejected this connection. Tell the person to open Bristlenose ▸ Settings ▸ MCP Agents (⌘,) and check this project is still shared.” |
404 on /mcp/ and a corrupted token returned
401, and a proxy that lumps both into “upstream error” hides the difference at
exactly the moment it matters.
- 404 and 401 mean opposite things. One is “your installation can't do this”, the other is “your access was withdrawn”. Same HTTP-shaped failure, completely different next action.
- Name the app, not just “Settings”. This text is read inside Claude Desktop, where “Settings” means Claude's. It has to say Bristlenose ▸ Settings ▸ MCP Agents — app, then pane. (Which makes the pane's name a dependency of this copy, not just a label.)
- Correction from tracing it: unsharing deletes the handshake, so that path gives the “not shared” message, not a 401. A 401 means the credential is stale or was rotated — so the remedy is checking the share state, not reinstalling anything. Getting that wrong would send the researcher down a reinstall rabbit-hole.
- 404 has no remedy, and should say so. Pointing at a Settings pane that cannot fix it is worse than admitting the build lacks the feature.
- We don't control the final wording — the model paraphrases. So the facts have to survive rewording (app name, pane name), and we can't lean on exact phrasing, formatting or markdown. It also can't be a button: we can name where to go, never take them there. That's the price of the only channel we have into another app.
- Bristlenose's server does see the 401, so it could raise something too — but a failed-auth notice on a single-user machine is noise. Left to the agent's reply on purpose.
- Every message ends with the same anti-fabrication clause. A model handed “no data” will otherwise apologise and answer from memory — the worst possible failure for a tool whose output lands in a research deliverable.
The researcher switches project mid-conversation
- “Every tool payload carries the project” is passive metadata. A model summarising for a human drops it nine times in ten. An in-band note it will relay.
- The anonymisation clause appears only when it differs from the previous project — otherwise the governance boundary moves without a gesture, and real names start appearing in a chat the researcher believes is anonymised.
- ~10 lines in the proxy. The sidebar antenna moving is the same message in Bristlenose; no toast, no banner.
4 · How it works PROPOSED
mcp-handshake.json (port · token · project) into its own container at 0600.
Deletes it on stop..mcpb.
Reads the handshake, speaks stdio to the client and HTTP to Bristlenose. If there's no
handshake, serves the four tools with "open Bristlenose" answers.- The token stops travelling. It never enters another vendor's config, never touches the pasteboard, never lands in a Time Machine backup of Claude's settings.
- The port stops mattering. Kernel-assigned per launch is fine when nobody has to write it down — which retires the “this address changes when Bristlenose restarts” caveat we shipped yesterday.
- Revocation becomes a file delete, not a hunt through a foreign config.
- Same shape as Figma's shipping extension, which proxies to
127.0.0.1:3845. They pin a port; we can't, so the handshake file is the one part we're inventing. - “Does the researcher need Node installed?” No —
checked on this machine. Claude Desktop ships its own Node (24.18.0) and uses it for
MCP servers:
isUsingBuiltInNodeForMcp is true and built-in node is compatiblein its log. This was the one risk that could have sunk the approach for a non-engineer audience, and it's answered.
5 · The decision to take NEEDS A CALL
One connector. It reads whatever project Bristlenose is showing.
Switch project in Bristlenose, and the agent's subject switches with it.
| Matches “ask about what I'm looking at” |
| Nothing to reconfigure, ever |
| Per-project Anonymise still applies correctly |
| Subject can change mid-conversation |
The extension is bound to one study at install time.
Claude Desktop collects the choice through its
own settings UI (user_config).
| Narrow, explicit grant |
| Subject can never change under you |
| Per-project setup returns — the thing we're removing |
| Two studies = two installs? |
- “The researcher can already see both projects on the same trackpad” is true and irrelevant. What changes isn't who can see the data — it's whether it crosses the network. That's the line they signed something about.
- v1 = one project, picked through Claude Desktop's own directory picker.
- Phase 2 = a folder grant, which is the better end state: grant a folder, then move projects in and out of it. Exposure becomes a drag — the most legible consent gesture there is — and it unlocks asking questions across a whole folder.
5a · Two surfaces, two jobs PROPOSED 31 JUL
Setup once, then share per project. The unlock: enablement is Bristlenose writing the
handshake file, not Claude Desktop's config. That means BN owns it completely — no
user_config, no directory picker, no “one install = one project”.
- The secret Ollama-only study is safe by construction: never shared, no handshake, invisible.
- Residual risk shrinks to switching between projects you deliberately shared — which is exactly what the in-band announcement above is for.
- Naming, decided: Turn On / Turn Off Agent Access. Never a vendor — several agents can be connected at once, so “Share with Claude” would be factually wrong, not merely narrow. And not “Share”, which macOS has already claimed for the share sheet.
- Doesn't fix the TCC question — the proxy still reads a file BN wrote. That spike still goes first.
The badge — three states, and two that were cut
| State | Meaning | Treatment |
|---|---|---|
| Off | Cannot be reached, ever | No badge — absence is the information |
| On, project not open | Open it and it's reachable | Pale / outline |
| On, project open | Exposed now | Solid |
- Also cut: a flourish when a query lands. They asked the question; they know.
- Deliberately off the badge: the Anonymise state. “Exposed with names” vs “codes only” is a real governance difference, but it's a second axis, and two axes on one glyph is unreadable. Tooltip and the Settings list.
- Known hole: during a run the activity ring takes this slot and wins, so an exposed project shows no antenna. Exposure is still true, just invisible — acceptable, but as a decision rather than a discovery.
- The pale tier is only possible because the flag lives host-side. The obvious move
was to put it in the project database beside the Anonymise switch — but that database is
only readable while that project's serve is running, so the app could never render
“on, but not open”. It belongs in
projects.jsonwith the other host-side facts (path, icon, folder). Same reason the Settings list can show every project's state without booting a serve for each one.
The project context menu — decided MAC REVIEW
Drawn from
buildProjectMenu. Conditional items appear only in their state, each with its own
separator; the stable tail is below.
- Music.app ships both
Turn On Home SharingandTurn Off Home Sharing— a network-exposure boolean as a symmetric verb swap. That is exactly our class. Notes does the same for governance (Lock Note/Remove Lock). - Finder uses a checkmark for
Use Stacks— but that's a view mode, and the same file verb-swapsShow Sidebar/Hide Sidebar. Within one Apple app: checkmark for modes, verb swap for exposure. - The argument that decides it: an unchecked checkmark item looks identical to an ordinary action. In the OFF state “Agent Access” would sit among Rename and Choose Icon looking like one of them. The misread — clicking a checked item believing you're enabling, and silently revoking — is the one we can't afford.
- State display isn't lost, because it was never this menu's job: the antenna is permanent while exposed, and Settings ▸ MCP Agents is the audit list. The menu is the act.
- Apple lists Share beside Copy and Delete as an action with one meaning and
one glyph everywhere. Borrowing it obliges
square.and.arrow.up; using the antenna instead breaks that contract, and using the share glyph would lie about what happens. - Wrong tense. Share is punctual — I sent a thing, it went. This is a standing permission to read, pulled repeatedly by whoever is connected. Exactly the misreading a contractually-barred study cannot afford.
- Rejected alongside: Expose to Agents (security-incident register), Readable by Agents (engineer-speak, negates badly), Allow Agents (allow them to what?). Agent Access is already the app's own noun — Settings ▸ MCP Agents.
- Title case, no ellipsis (it acts immediately), no vendor — several agents can be connected at once.
Connect Agent… sat — adjacent to Show in Finder it inherited that item's
reading, two neighbours about this project's files, one revealing them in Finder and one letting
a cloud vendor read them. That adjacency asserts an equivalence that isn't true.
- Everyday items keep the top (HIG: most-frequent first). Flipping exposure is once per study.
- It lands at exactly three groups in the common case — HIG's guidance for context menus is “no more than about three”.
- Hide it, don't dim it, when the project can't be shared — the context-menu rule (“displays only the actions that are relevant”), which the file's own lifecycle block already follows and the trailing block breaks.
- Gate on the right predicate:
canShowInFinderis file-presence, so a never-analysed folder passes it with no quotes to read. NeedscanShareWithAgents— locatable and analysed.
“Turn on access when I have no agent?” — a permission, not a connection
| Condition | Can we know? | What the menu does |
|---|---|---|
| Project not analysed / not locatable | Yes | Hide the item. Nothing to read; a context menu shows only relevant actions. |
| This build has no MCP support | Yes — mcp.mounted | Hide the item. Genuinely impossible, permanently, for this build. |
| No agent installed | No | Show it. Turning it on is not an error. |
- We can't know. The extension lives in Claude Desktop's container, and reading a foreign app's install state is the fragility class this whole plan exists to avoid. Hiding on a fact we don't have would make the feature invisible to someone who does have an agent.
- And the gate would be backwards. “Has an agent ever connected?” is knowable — but the first-time order is turn access on, then ask a question. Gating on it blocks the exact flow it's meant to enable.
- It's a permission, not a connection. Turning it on with nothing installed is an unused permission, which is coherent. System Settings ▸ Sharing is the precedent: enable Screen Sharing with no client anywhere and macOS doesn't hide, grey or nag — it turns on and shows you the address.
- Where “you need an agent” lives instead: Settings ▸ MCP Agents, which is where install already lives. That's discovery's job, not the toggle's.
Where the control lives — decided: both
A checkmark item, exactly where Show in Finder lives. One gesture, no window.
The whole posture on one screen — the answer to “what have I exposed?”
Right-click toggles one project. Settings ▸ MCP Agents lists every project with its state, alongside the install button and connection status.
- Precedent: macOS does exactly this for Sharing, Login Items and Screen Time — act in context, audit in Settings.
- The badge stops meaning “an agent asked just now” and starts meaning “this is exposed” — permanent while shared and open. That's the fact worth showing: the researcher already knows they just asked a question; what they can't otherwise see is that a project is reachable at all. It makes the sidebar an audit surface — what's exposed right now — pairing with Settings, which answers what have I shared.
- Supersedes what's built: the shipped badge reads
mcp.active, a 120-second activity window. Exposure is a different input.
6 · For the morning
bristlenose mcp-proxy in the same pass? It makes Claude
Code and Codex address-free and token-free too — the §2 sleeper win. Costs one CLI subcommand
wrapping the proxy that has to exist anyway.user_config is per-extension, identity keyed on a unique manifest name).
Of the six jobs the sheet does — install, point-at-a-project, the Claude Code / Codex command, the
raw primitives, Anonymise, and the badge — only install is machine-wide. That single
mismatch is the whole problem.
| Option | Verdict |
|---|---|
| 1 · App-level only Settings ▸ Connections, project picker inside |
Four of six jobs are per-project, so it re-asks “which project?” — a selection the sidebar already makes. |
| 2 · Split by lifetime Install → Settings; sheet keeps the rest |
Recommended. Two places, but two genuinely different lifetimes. The Claude Desktop tab becomes instructional — the folder path plus Reveal in Finder, which is exactly what's needed in Claude's picker. |
| 3 · Menu bar only One window, project selector on top |
Self-contained, but duplicates the sidebar's job of expressing which project you mean. |
| 4 · The antenna badge is the entry point | Flagged, not proposed — collides with the settled sidebar rule that status is attention, not affordance (the Mail model). |
| 5 · Leave as-is Keep the right-click |
Cheapest, and honest if the copy says what to do in Claude Desktop. The right answer if we want zero structural change before the TCC spike settles. |
SystemPolicyAppData
protects Apple's own app containers (Notes, Safari, Mail all denied) but not third-party ones
(Bristlenose, Adobe and others readable, from a shell with no Full Disk Access). Same-UID is the
actual gate, and the proxy runs as the researcher.
- No prompt, no sticky denial — and the handshake stays in the container, so no token is ever written into a project folder. The Dropbox-sync problem never arises.
- The mechanism was proved end to end too: real data through a 90-line, zero-dependency proxy, three states, self-heal, and a wrong token correctly rejected.