The Bristlenose extension

The whole connect UX, after the hand-paste path failed three times in one sitting. Drawn 31 July 2026 as a proposal — shipped in 0.23.0 on 1 August 2026, and trued against what got built on 2 August.

Artefact — framed. What ships; no labels that wouldn't appear in the product. Commentary — tinted, ruled. Why it works. Deliberately not-the-product. As shipped — blue. Where the built thing differs from the drawing.
Most of this shipped as drawn. Five things did not — each marked in place below, so the drawing stays readable as the argument it was and you can still see where the argument lost.

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.

attempt 2
⚠️
Could not load app settings
There was an error reading or parsing claude_desktop_config.json: Unexpected non-whitespace character after JSON at position 16135 (line 279 column 1)
OK
the cost
before 298 lines after 107 lines ← rewritten by Claude Desktop lost codeGroups lost simulatorDeviceConsent kept preferences

1 · The new flow SHIPPED

  1. 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.

  2. The Claude Desktop tab is one button

    Connect Agent — Claude Desktop

    Agents read whichever project is selected in Bristlenose

    Now showing: IKEA discovery · 6 sessions · 214 quotes

    Claude DesktopClaude CodeChatGPT & CodexGeneric MCP

    Adds Bristlenose to Claude Desktop as an extension. Claude Desktop will ask you to confirm.

    B
    BristlenoseAn agent has asked about this project recently
    Install 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 .mcpb is 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.
    Drawn correctly — the install row really does live inside the Claude Desktop tab. It shipped above the picker first and had to be moved back down, because a .mcpb-only action was showing while three tabs it does nothing for were selected. The pane's fixed payload height is sized to the tallest tab (ChatGPT & Codex), so switching client never reflows it.
    • The Anonymise bullet is wrong. It did not stay where it was. On the Mac it became one global switch at the top of the pane, off by default — a posture toward agents rather than a property of a project. The CLI still reads the per-project database flag, so the two channels differ deliberately.
    • The subtitle drawn here (“An agent has asked about this project recently”) shipped verbatim, and still reads our own server's activity — never Claude's install state.
  3. Claude Desktop runs its own install

    Claude Desktop
    Install “Bristlenose”?
    This extension will be able to run on your computer and access the Bristlenose project you have open.
    CancelInstall
    Approximation 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.
  4. Done — it appears where Figma appears

    Claude Desktop → Settings → Developer
    Local MCP servers
    Add and manage MCP servers that you're working on.
    Bristlenose running This server is managed by an extension
    Figma running This server is managed by an extension

The two entry points — wording and placement

Settings — the fourth tab
🖌
Appearance
🧠
LLM Provider
〰️
Transcription
📡
MCP Agents
Agents read whichever project is selected in Bristlenose
Now showing: IKEA discovery · 6 sessions · 214 quotes
B
BristlenoseExtension · 4 KB
Install Extension…
Appearance · LLM Provider · Transcription · MCP Agents. Renaming the shipped LLM tab is part of the proposal, and it earns its churn.
Bristlenose menu — setup
About Bristlenose
AI & Privacy…
Connect an Agent…
Settings…⌘,
Project menu / right-click — enablement
Show in Finder
Turn Off Agent Access
Rename…
Move to  ▸
Two menus, two verbs, because they're two jobs.
Setup — onceEnablement — per project
WhereBristlenose menuProject menu / right-click
WordingConnect an Agent…Turn On / Turn Off Agent Access
Ellipsis?Yes — opens a windowNo — it toggles immediately

Before / after

Hand-paste (today)Extension (proposed)
Steps for the researcherCopy · find file · edit JSON · fix commas · save · quit · relaunchClick Install · confirm
Ways to get it wrongComma, brace, wrong nesting, wrong file, stale portNone available
Cost of a mistakeClaude Desktop rewrites its settings file
Where the token livesPlaintext in another vendor's config, on the pasteboard, in Time Machine0600 file in Bristlenose's own container, read at connect time
When the port changesConfig is dead; re-pasteNothing to do
Switching projectRe-paste per projectNothing to do
RevokingHunt through a foreign configBristlenose deletes one file

2 · Every state the sheet can be in SHIPPED, TWO PANES CHANGED

Claude Code — unchanged

Paste in any terminal:

claude mcp add bristlenose \ -- bristlenose mcp-proxy
DoneCopy Command
ChatGPT & Codex — unchanged

Add to ~/.codex/config.toml:

[mcp_servers.bristlenose] command = "bristlenose" args = ["mcp-proxy"]
DoneCopy Config
This is the sleeper win. If the proxy also ships as 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.
The two panes above are drawn as they would be with the sleeper win. It wasn't taken. bristlenose mcp-proxy does not exist, so both tabs shipped carrying “tonight's URL + token payload” — the second bullet's case, not the first.

The fourth tab — Generic MCP

Connect Agent — connection details revealed

Give these two values to any MCP-compatible agent:

http://127.0.0.1:58735/mcp/ Authorization: Bearer EXAMPLEtokenNOTREALaaaaaaaaaaaaaaaaaaaaaaaa

The port number changes each time Bristlenose starts — copy the address again after a restart. Your token stays the same.

DoneCopy
The plan nearly starved this path. Once Claude Desktop is an Install button and the other two tabs are commands, no tab shows the raw URL and token — and “works with any MCP-compatible agent” would point at nothing.

Settings ▸ MCP Agents — with and without a project selected

a project is selected
Agents read whichever project is selected in Bristlenose
Now showing: IKEA discovery · 6 sessions · 214 quotes
B
BristlenoseExtension · 4 KB
Install Extension…
Claude DesktopClaude CodeChatGPT & CodexGeneric MCP
no project selected (Welcome screen)
Agents read whichever project is selected in Bristlenose
B
BristlenoseExtension · 4 KB
Install Extension…
Claude DesktopClaude CodeChatGPT & CodexGeneric MCP
Same screen. The only difference is one line that isn't there.

3 · What the agent sees SHIPPED — SIX MESSAGES, NOT FIVE

Bristlenose isn't serving anything yet VERIFIED IN SPIKE

Claude Desktop
you What are people saying about the checkout flow? → bristlenose · search_quotes Bristlenose isn't showing a project right now. Open Bristlenose and select the study you want to ask about, then ask me again.
Stolen from Figma's extension, which does exactly this. The tools always appear in the client; when the app isn't up, each one returns a sentence that names the fix. There is no state where the researcher sees a broken connector — that's what makes it non-error-prone rather than merely easy.

Serving — and the project is named in the answer

Claude Desktop
you What are people saying about the checkout flow? → bristlenose · get_project_overview → bristlenose · search_quotes section="Checkout" In IKEA discovery (6 sessions, 214 quotes) the checkout section carries 14 quotes, 9 of them frustration… “I got to the payment bit and just gave up.” — p3, 00:14:22
The project name in every answer is load-bearing, not decoration — it's the mitigation for the scope model below. If the subject can change, the answer must always say what the subject is.

The full message vocabulary — five states, found by running it

tool results — relayed by the model, in Claude's chat

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.

StateSignalWhat the agent tells the researcher
Ready200The real answer, from real quotes.
Not shared / BN closedno handshake“Bristlenose isn't open, so there is no study data available… Do not answer from memory.
BN startingport silent“Bristlenose is starting — ask again in a moment.”
No agent support in this build404“This copy of Bristlenose was built without agent support. No setting will enable it.”
Credential rejected401“Bristlenose rejected this connection. Tell the person to open Bristlenose ▸ Settings ▸ MCP Agents (⌘,) and check this project is still shared.”
The last two are new — the spike surfaced them by hitting them. A stale sidecar returned 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.
All five shipped, and a sixth arrived from the first live install. When the reader is Claude Desktop's own Node process, macOS asks “Claude would like to access data from other apps” before the proxy can read the handshake — so a permission-denied state exists that nobody had drawn.

The researcher switches project mid-conversation

Claude Desktop
you and how did that compare on cost? → bristlenose · search_quotes Note: Bristlenose is now showing a different study — “Nurses onboarding” (previously “IKEA discovery”). Everything below is from “Nurses onboarding”, which is not anonymised. In Nurses onboarding, cost comes up in 4 quotes…
The review's sharpest catch, and it isn't in the plan I wrote first. Clicking a row in Bristlenose's sidebar is not a “change my agent's subject” gesture in anyone's model — but under Option A it is exactly that. Without a marker the researcher gets a confident answer about the wrong study, in a scrollback that is otherwise all IKEA, and pastes it into a report.
The announcement never shipped — and the reason recorded at the time was the wrong one. It was dropped as unnecessary “because Option B pins the project”, but Option B is not what shipped. What actually removes the scenario is the allowlist: the only projects that can become the subject are ones the researcher explicitly turned Agent Access on for, so the catastrophic case above — the contractually-barred study exposed by an innocuous sidebar click — is prevented at the source rather than announced after the fact.

4 · How it works SHIPPED

BristlenoseServes a project on a kernel-assigned port. Writes mcp-handshake.json (port · token · project) into its own container at 0600. Deletes it on stop.
The extension~150 lines of Node inside the .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.
Claude DesktopManages the extension itself. Never sees a port or a token; has no config for the researcher to break.
The mechanism shipped exactly as drawn — handshake at 0600 in the container, deleted on stop, proxy re-reading it on every tool call, Claude Desktop never seeing a port or a token. The Node answer held: it ran on a machine with nothing installed. Two corrections to the boxes above.
“No prompt, no sticky denial” was measured from the wrong process. The shell test in Q7 was real and its conclusion didn't transfer: when the reader is Claude Desktop's Node, macOS attributes the container read to Claude and fires “Claude would like to access data from other apps.” Responsible-process attribution is the entire variable, and a shell doesn't have Claude's.

5 · The decision to take SUPERSEDED BY §5a

Option A — follow the fronted project

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
Option B — pin one project

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?
DECIDED: B. Not on UX grounds — A is friendlier — but because A has a failure mode no labelling can reach. A researcher may keep a study on local models precisely because the data is contractually barred from the network. Under A, clicking that project in the sidebar sends its quotes to a cloud vendor — and the switch announcement above cannot save it, because the notice rides on the tool result that already carries the quotes. The warning and the breach arrive in the same message.
B was decided and never built. §5a, immediately below, is what shipped. No user_config, no Claude Desktop directory picker, no “two studies = two installs”.

5a · Two surfaces, two jobs THIS IS WHAT SHIPPED

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”.

Settings ▸ MCP AgentsDo-once. Install the extension, see whether an agent has connected, get the command or the raw primitives for other clients.
Agent access, per projectA toggle. BN writes the handshake only for a project that is both turned on and currently serving — so one file, one meaning.
The agentSees whichever shared project is open. Never sees an unshared one. Unshare = delete the file = revoked.
This is Option A restricted to an explicit allowlist — which answers the objection that killed A (a project you never meant to expose gets exposed by a sidebar click) while keeping A's zero-friction feel for projects you did share.

The badge — three states, and two that were cut

Sidebar rows
StateMeaningTreatment
OffCannot be reached, everNo badge — absence is the information
On, project not openOpen it and it's reachablePale / outline
On, project openExposed nowSolid
Cut: “starting up”. The reason generalises beyond this badge — during those seconds the researcher is looking at Claude's window waiting for an answer, not at Bristlenose's sidebar. A badge can't fix a confusion the user is having in another app. That case is carried by the proxy's own sentence (“Bristlenose is starting — ask again in a moment”), which makes that message load-bearing rather than optional.

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.

right-click a project · exposed state
Analyse
Show in Finder
Rename…
Choose Icon
Move to
Turn Off Agent Access
Remove from Sidebar
Verb swap, not a checkmark — and Apple ships the proof. Strings pulled off shipping binaries on this Mac, not recalled:
And “Share” is the wrong word — an idiom collision, not a taste call.
Placement: its own group, below the housekeeping block. Not where 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.

“Turn on access when I have no agent?” — a permission, not a connection

what we know, and what follows
ConditionCan we know?What the menu does
Project not analysed / not locatableYesHide the item. Nothing to read; a context menu shows only relevant actions.
This build has no MCP supportYes — mcp.mountedHide the item. Genuinely impossible, permanently, for this build.
No agent installedNoShow it. Turning it on is not an error.
Hiding is the right Mac answer — for things we know are unavailable. “No agent” isn't one of them, on two counts.
The pattern, third time tonight: the boundary is one-way. We couldn't read their config to validate it, we can't tell whether the install took, and we can't tell whether an agent exists. We can offer; we can't observe. Every design that needs to know something about the other app is the wrong design — which is why the badge reads our own server, the messages are tool results rather than dialogs, and this toggle is a permission rather than a connection.

Where the control lives — decided: both SHIPPED AS ONE

A · Right-click the project
Show in Finder
Turn Off Agent Access
Rename…
Move to  ▸

A checkmark item, exactly where Show in Finder lives. One gesture, no window.

B · A list in Settings
Shared with agents
☑︎IKEA discovery
Nurses onboarding
Medical device study (local models only)

The whole posture on one screen — the answer to “what have I exposed?”

C · Both — the menu for speed, Settings for the overview

Right-click toggles one project. Settings ▸ MCP Agents lists every project with its state, alongside the install button and connection status.

C, and not reluctantly. These aren't duplicates of one control — they answer two different questions. The menu answers “expose this one” at the moment you think it. The list answers “what have I exposed?”, which is a governance question a researcher will be asked by someone else, and which no per-project menu can ever answer.
C was built, then cut back to A within a day. The Settings list shipped — every project a row, live checkbox, Anonymise on the open project's row — and was removed as the over-build. The right-click menu is the act; the sidebar antenna is the audit. Settings ▸ MCP Agents is setup only: header, Now-showing, one global Anonymise, install row, four client tabs.

6 · For the morning ALL ANSWERED

Answered the same morning, and the product shipped that night. Three went against the recommendation written here — worth reading in that light, because the questions were good and two of the recommendations weren't. And one thing nobody asked that turned out to matter most. Collapsing five manual steps into one button removed the friction that had been doing the disclosure work implicitly — so the AI consent sheet needed a new recipient class (an agent's vendor: a different company, under its own terms, at the moment the researcher asks rather than when the analysis runs). It also had to stop promising that names never leave the device, since Anonymise defaults to off. Removing friction can create a disclosure obligation even when it creates no new egress path.
1 · Scope modelA (follow the fronted project) or B (pin one)? Everything else in the plan is the same either way.
2 · Does the extension replace the Claude Desktop tab, or sit beside “paste it yourself”? Recommendation: replace. A second path we know is destructive isn't a fallback, it's a trap.
3 · Ship 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.
4 · Local install now, or submit to the directory too? Local drag/open install works today; the curated listing is discoverability, not capability.
5 · What happens to the parked project? The warm-sidecar pool keeps the previous project's server alive on its own port, with its own token. The handshake names only the fronted one — so under Option A the agent can still be reading the project you just switched away from. Review calls this a correctness bug, not a UI gap. Fronted-only + naming? Handshake carries both? Or drain the parked one while an agent is active?
6 · Where does this live? — five options Option B changed what the sheet can do. Two facts constrain it: Bristlenose cannot perform the Claude Desktop grant (the project is picked in Claude Desktop's own directory picker — we can only name the folder), and one install grants one project (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.
OptionVerdict
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.
7 · ANSWERED — the spike ran …and this half of the answer was wrong. See the correction in §4. The TCC gate that blocked everything is not a gate: 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.
The honest scorecard for this page. Twenty-odd decisions, drawn in one night, argued against real Apple strings and a real spike. Four didn't survive contact — and the pattern in the four is worth more than any of them individually: each one was decided from evidence gathered somewhere other than where the thing would run. A shell instead of Claude's Node process. A pin instead of the sidebar. A drawn sidebar row instead of a collapsed one. A reasonable-looking governance list instead of the audit surface already on screen.