Native proxy — what the decisions look like

The user-facing consequences of plan v2's decisions D1–D9 (docs/design-mcp-native-proxy.md §6.9), drawn at the shipped 660pt Settings width, so they can be judged on UX as well as engineering grounds. 29 Sep 2026. Strings are the shipped en keys unless a commentary block lists them as new.

Bristlenose, as it would ship another app (ChatGPT, Claude): a schematic from our screenshots, not their pixels commentary: not the product

Engineering read of D1–D9, before the pictures. Five decisions I'd take as written, two that need an amendment, and one new risk the plan doesn't name yet.

#VerdictWhy, and what it costs the user
D1TakeFresh ids cost nothing, and the user never sees them. The number of signer classes is a P0.2 measurement, not a design choice: the local Apple Distribution build and the Apple-re-signed TestFlight/App Store build can share an id and still hang the helper (§6.6). In practice only the maintainer's Mac holds both, so the hazard is ours, not users'.
D2TakeA few seconds of swiftc per Release build buys "the helper is always as fresh as the host". A Debug build has no native path, so a Cmd+R build can't exercise it. That makes the manual P6 pass on a Release build a requirement, not a nicety.
D3TakeOne gate over both copies is the right shape, with file-then-grep for the SIGPIPE trap. Invisible to users.
D4Take, with one questionFail closed is right. But ask the server too: when Agent Access goes off, does serve refuse that project's MCP calls whatever the handshake says? If it does, a stale group copy only points a proxy at a door that's shut. If it doesn't, D4 is the only guard, and the unlink must be verified. Not checked yet.
D5TakeWith no new diagnostics, the proxy's sentences have to carry the recovery on their own. Section 6 shows them. The native "can't reach" sentence is vaguer than today's Files & Folders one ("install again, or check for an update"), because it covers three different build defects, each of which D3 should stop from shipping.
D6TakeOne commit, one release, one tester, and rollback is a revert. The cost is section 7: in that release, every existing Claude user needs one reinstall.
D7Take; the refusal needs copyA marketplace inside the bundle is the only readable place. It is a .dmg-only case: the App Store never translocates. The refusal is drawn in state 2.6.
D8TakePer-host state keyed by our own label keeps "know the protocol, never the client" intact. It inherits today's honest blind spot: until the first question arrives, the button still says Install, because we can’t see ChatGPT’s or Claude’s state. Section 2 shows it.
D9TakeExternal TestFlight before the store costs one review cycle, and it is the only way a human reviewer sees the helper before the public does.
newAdd to P0.4Where does the runtime .mcpb live when Claude opens it? If it's written to our temporary directory, which sits inside our sandbox container, then Claude reading it is exactly the cross-team container read macOS 27 denies. The Install dialog could then fail in Claude with no message from us. P0.4 must test the real path, not an unzip we control. Fallbacks, in order: a location Claude can read (the bundle can't hold it, because Apple re-signs the helper after the zip is made), or an explicit save. Inferred, not measured.

0 · Kept exactly as shipped the half of the experience the plan must not touch

The plan changes how an agent reaches Bristlenose. It does not change how Bristlenose tells the researcher what's exposed and what's being read. Everything in this section ships today (0.23.0–0.31.5), is pinned by tests, and appears unchanged in every frame below.

Sidebar · exposure and activity, per project

Bristlenose
▤Onboarding study14 sessions
▥Beds & duvets3 sessions
◧Checkout interviews9 sessions
▦Pilot3 sessions

Project menu and sidebar context menu · the verb swaps

Agent Access is on for this project.

Shipped mechanismWhat it tells the researcherWhat drives itWhat the native plan must do
Antenna, solidAn agent could read this project right nowServeFleet.handshakeProjectPaths: the handshake writer's own setP1 writes the group copy from the same set, in the same call. D4's fail-closed rule stops the group copy outliving the solid antenna
Antenna, radiating, then two tapsAn agent is reading this project now, and then that it has finishedserve's per-project tool-call counter (/api/agent-activity)Nothing: the helper calls the same /mcp/ with the same bearer, so every call counts. P6 checks it: a question from ChatGPT and from Claude each radiates the right project
Projects register: Active / Available when opened, tick, Sessions, Last asked, roll-up, receipts, empty state, scope noteWhat's shared, what could be, what was asked and when; untick to revoke, re-tick to undoAgentProjectRegister + projectsSectionUntouched by every phase. The P5 switch-over edits the top half of the pane only; AgentProjectRegisterTests and MCPAgentsSettingsViewTests must pass unedited
Turn On / Turn Off Agent Access, the badge tooltip, the consent sheetPer-project permission, granted where the project livesAgentAccessPolicy, the menu verbs, consent v2 (already client-neutral)Untouched
AnonymiseParticipant names removed from everything an agent readsserve (mcp_server.py)Untouched: the helper is a pipe
Install row: version compare and "asked recently"Whether the agent's copy matches this buildX-Bristlenose-Proxy-Version on each call → agentProxyVersionThe helper sends the same header (the spike does, main.swift:256). D8 adds a host label beside it; it never replaces the per-project register

Why this section exists. The first draft of this page drew the pane's top half in detail and collapsed the register into a one-line stub. That made it read as though the plan replaced the register. It doesn't, and it mustn't: the register and the antenna are the only places a researcher sees what agents can reach, and they took most of August to get right. Every Settings frame now carries the real register, in whichever state fits the story: typical, never asked, just asked, all closed, a receipt, empty.

1 · Today, on macOS 27 the two tabs the plan replaces

Before · Claude Desktop tab (0.31.5, macOS 27)

MCP Agents

Agents only see open projects

Anonymise
Remove participant names
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

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

❋
Bristlenose
Extension 0.31.5 (3906)

The first time you ask a question, macOS blocks Claude from reading Bristlenose, without asking. To allow it, open System Settings ▸ Privacy & Security ▸ Files & Folders, expand Claude and turn on Bristlenose — then ask again.

ProjectsOpen to agents: 2 projects · 14 sessions

Before · ChatGPT & Codex tab (0.31.5)

MCP Agents

Agents only see open projects

Anonymise
Remove participant names
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

In ChatGPT: Settings → MCP servers → Add server — or add to ~/.codex/config.toml (one entry covers ChatGPT desktop, Codex CLI, and the IDE extension):

[mcp_servers.bristlenose] url = "http://127.0.0.1:8154/mcp/" http_headers = { "Authorization" = "Bearer ••••••••••••" }

This address changes when Bristlenose restarts — re-copy the config after relaunching.

In ChatGPT, ask in Work mode and start with @Bristlenose — otherwise its tools aren’t used.

ProjectsOpen to agents: 2 projects · 14 sessions

What's wrong with these today. Claude: on macOS 27 a fresh install fails its first question by design, and the user fixes it in System Settings. ChatGPT: the pasted config carries a port and a token that change on every relaunch, so the user re-copies after every restart. The one-click ChatGPT plugin is ready, but it needs Node.

2 · ChatGPT & Codex tab, after D7 · D8 — seven states

2.1 · Ready — never asked (and just after installing)

MCP Agents
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

Adds Bristlenose to ChatGPT as a plugin. ChatGPT will open and ask you to install it.

❋
Bristlenose
Plugin 0.32.0 (3950)

In ChatGPT, ask in Work mode and start with @Bristlenose — otherwise its tools aren’t used.

Or connect by hand

2.2 · An agent has asked recently

MCP Agents
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

Adds Bristlenose to ChatGPT as a plugin. ChatGPT will open and ask you to install it.

❋
Bristlenose
An agent has asked about this project recently

In ChatGPT, ask in Work mode and start with @Bristlenose — otherwise its tools aren’t used.

Or connect by hand

2.3 · After a Bristlenose update, ChatGPT still runs the old plugin

MCP Agents
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

Adds Bristlenose to ChatGPT as a plugin. ChatGPT will open and ask you to install it.

❋
Bristlenose
Plugin 0.33.0 (4102)

Installed copy is older: 0.32.0 (3950). In ChatGPT, choose ⋯ ▸ Uninstall, then Install plugin.

In ChatGPT, ask in Work mode and start with @Bristlenose — otherwise its tools aren’t used.

Or connect by hand

2.4 · ChatGPT isn't installed

MCP Agents
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

Adds Bristlenose to ChatGPT as a plugin. ChatGPT will open and ask you to install it.

❋
Bristlenose
Plugin 0.32.0 (3950)
ChatGPT isn’t installed — download it from chatgpt.com

In ChatGPT, ask in Work mode and start with @Bristlenose — otherwise its tools aren’t used.

Or connect by hand

2.5 · ChatGPT too old to install by link

MCP Agents
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

Adds Bristlenose to ChatGPT as a plugin. ChatGPT will open and ask you to install it.

❋
Bristlenose
Plugin 0.32.0 (3950)
This version of ChatGPT can’t add plugins from other apps — update ChatGPT

In ChatGPT, ask in Work mode and start with @Bristlenose — otherwise its tools aren’t used.

Or connect by hand

2.6 · .dmg app opened from Downloads (translocated), D7

MCP Agents
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

Adds Bristlenose to ChatGPT as a plugin. ChatGPT will open and ask you to install it.

❋
Bristlenose
Plugin 0.32.0 (3950)

Move Bristlenose to your Applications folder and open it from there to install the plugin. ChatGPT can’t install it from where Bristlenose is running now.

In ChatGPT, ask in Work mode and start with @Bristlenose — otherwise its tools aren’t used.

Or connect by hand

2.7 · "Or connect by hand" open

MCP Agents
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

Adds Bristlenose to ChatGPT as a plugin. ChatGPT will open and ask you to install it.

❋
Bristlenose
Plugin 0.32.0 (3950)

In ChatGPT, ask in Work mode and start with @Bristlenose — otherwise its tools aren’t used.

Or connect by hand

For Codex in a terminal or an IDE, add to ~/.codex/config.toml:

[mcp_servers.bristlenose] url = "http://127.0.0.1:8154/mcp/" http_headers = { "Authorization" = "Bearer ••••••••••••" }

This address changes when Bristlenose restarts — re-copy the config after relaunching.

Consequences the pictures show.

New strings (would go into 21 locales): chatgptHint, installPlugin / reinstallPlugin / updatePlugin, pluginBuild, downloadChatGPT, updateChatGPT, moveToApplications, connectByHand, and a shorter chatgptCodexHow for the TOML route. The ellipsis stays on "Install Plugin…" because the command finishes in another app (F9).

3 · Claude Desktop tab, after D6 · P4 · P5

After · any macOS, 15 to 27

MCP Agents

Agents only see open projects

Anonymise
Remove participant names
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

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

❋
Bristlenose
Extension 0.32.0 (3950)
ProjectsOpen to agents: 2 projects · 14 sessions

After · the first launch after the switch-over, for an existing user

MCP Agents

Agents only see open projects

Anonymise
Remove participant names
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

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

❋
Bristlenose
Extension 0.32.0 (3950)

Installed copy is older: 0.31.5

ProjectsOpen to agents: 2 projects · 14 sessions

4 · The whole journey, counted clicks from "I want this" to an answer

ChatGPT · today (Node installed; 0.31.5)

1Turn On Agent Access for a project
2Settings ▸ MCP Agents ▸ ChatGPT tab ▸ Copy Config
3Edit ~/.codex/config.toml by hand, or find ChatGPT's MCP settings and paste
4Restart ChatGPT, switch to Work
5"Always allow" once per tool, up to 5 times
6Any Bristlenose relaunch: back to step 2

ChatGPT · after (no Node, any macOS)

1Turn On Agent Access for a project
2Settings ▸ MCP Agents ▸ ChatGPT tab ▸ Install Plugin…
3ChatGPT opens on the plugin page ▸ Install plugin
4Toast: “Bristlenose plugin installed · Try now”
5Ask in Work mode. No tool approval: measured 29 Sep, five calls and no card

Claude Desktop · today (macOS 27, fresh install)

1Turn On Agent Access
2Install Extension…
3Claude: Install
4First question fails: "macOS has blocked Claude…"
5Open Files & Folders ▸ expand Claude ▸ switch on Bristlenose
6Ask again

Claude Desktop · after (any macOS)

1Turn On Agent Access
2Install Extension…
3Claude: Install
4Ask

Step 3 · ChatGPT's plugin page (schematic)

ChatGPT
❋
Bristlenose
Ask your Bristlenose study
Install plugin

Ask your Bristlenose study — quotes, themes, signals, frameworks — from your agent. Read-only.

MCP servers 1 · 0.32.0-3950 · Research

Step 4 · the toast, then a Work thread (schematic)

ChatGPT
Bristlenose plugin installed Try now
@BristlenoseWhat frustrated people most about onboarding?ChatWork

Measured vs inferred. The link, the plugin page, "Install plugin", the toast and "Try now" were all measured on 29 Sep with the spike, and so were Work mode and the per-tool approvals. What isn't measured: whether readOnlyHint removes the approval cards (work-list item 14, "not yet re-measured"), and the page's exact version label. Net: the ChatGPT path goes from 5+ steps with a hand-edited file and a re-copy after every relaunch, to 3 clicks. The Claude path on macOS 27 goes from 6 steps with a detour through System Settings to 3 clicks. Agent Access (step 1) is the same on both, and deliberately so.

5 · After a Bristlenose update measured 29 Sep: ChatGPT outcome B · Claude outcome A

Outcome A · the host updates in place (1 click there)

1Pane shows Update Plugin… + “Installed copy is older”
2ChatGPT's plugin page offers Update (or Claude's install dialog replaces the old copy)
3Ask

Outcome B · the host keeps the old copy

1Pane shows Update Plugin…
2ChatGPT shows the page as installed, with no update. (For the Node .mcpb today, Claude only previewed a same-named extension.)
3We have to say: “Remove Bristlenose in ChatGPT ▸ Plugins, then click Install Plugin… again”
4Install, then ask

6 · When it goes wrong what the person reads, in the host's own chat

Bristlenose isn't running (both proxies, unchanged)

Claude · ChatGPT Work
Which participants struggled with the export?

▸ bristlenose · search_quotes

Bristlenose isn’t open, so I can’t see your study right now. Open Bristlenose and select the project, then ask me again.

Can't read the handshake · Node today, macOS 27

Claude
Which participants struggled with the export?

▸ bristlenose · search_quotes

macOS is blocking Claude from reading Bristlenose, and on this version it doesn’t ask. Open System Settings ▸ Privacy & Security ▸ Files & Folders, expand Claude, turn on Bristlenose, then ask again.

Can't read the handshake · native, after

Claude · ChatGPT Work
Which participants struggled with the export?

▸ bristlenose · search_quotes

This Bristlenose extension can’t reach Bristlenose’s data. In Bristlenose, go to Settings ▸ MCP Agents and install it again, or check for a Bristlenose update, then ask me again.

Contract changed · outdated plugin (both)

Claude · ChatGPT Work
Which participants struggled with the export?

▸ bristlenose · search_quotes

Your Bristlenose extension is older than the Bristlenose app and can’t read it any more. In Bristlenose, open Settings ▸ MCP Agents and install it again — it takes a moment and keeps your settings.

7 · D6: one release switches everything before and after, per surface

Surface0.31.x (today)The switch-over releaseWho notices
Claude Desktop tabInstall Extension… + Files & Folders note and button on macOS 27; the "Allow" note on 26Install / Reinstall / Update Extension… onlyEveryone on 27 (a shorter, calmer pane)
Existing Claude usersNode .mcpb; works on 26 with the grant, and on 27 once Files & Folders is onTheir old extension keeps working where it already works, until they click Update/Install. The pane says so on 26; on 27 it just says InstallAll of them: one reinstall each
ChatGPT & Codex tabTOML + Copy Config + Work noteInstall Plugin… row + Work note; the TOML route folds under "Or connect by hand"Every ChatGPT user; anyone on a pasted config still needs to re-copy after restarts until they switch
Register, antenna, Agent Access verbs, AnonymiseAs shippedIdentical (section 0)Nobody, by design
Files & Folders copyOn the pane (27), in the proxy, in the help pageHelp page and the Node proxy onlyNobody new
Node dependencyChatGPT plugin impossible without NodeNoneChatGPT users without Node: most of them
Rollback—Revert one commit, ship a patch; installed helpers keep answering, because the handshake format doesn't changeMaintainer

The cost of no flag is that the first time anyone except the maintainer sees the native path is the release itself. D9's external TestFlight is the mitigation: it puts a human reviewer and any external tester on the build before the store does. The benefit is that no user ever sees a half-state (a native Claude tab next to a TOML ChatGPT tab, say), and there is no Diagnostics toggle to explain.

8 · Benchmark against the best match, exceed, or fall short

Five exemplars, read from their own pages on 29 Sep 2026: Apple Xcode ("Giving external agents access to Xcode"), DEVONthink 4.3, ACE Studio, Figma and Notion. None of the five documents a one-click route to both Claude Desktop and ChatGPT. Where a page is silent, it counts as silent, not as a failure.

MeasureBest out thereBristlenose, after the planVerdict
Steps to a first answerFigma: a preferred route of about 4 actions per client (Codex: Plugins ▸ + ▸ Install ▸ Allow). Xcode: toggle + 1 Terminal command. ACE: 4–5 for ChatGPT, about 12 for Claude Desktop (hand-edited JSON + restart)3 clicks per client after Agent Access: Install… in Bristlenose, Install in the host, ask. No file, no terminal, no restartExceed
Runtime on the user's MacXcode, DEVONthink and ACE ship a helper inside the app bundle. Notion falls back to npx mcp-remoteA signed, sandboxed helper inside the bundle. No NodeMatch
Where you turn it onDEVONthink: Settings ▸ AI ▸ MCP, a switch per provider. ACE: a per-client button that shows that client's steps. Xcode: one toggleSettings ▸ MCP Agents, a tab per client, plus per-project Agent Access from the Project menu and the sidebarMatch
Preferred vs manual routeFigma labels one "preferred" route per client, with a manual fallback and deeplinksInstall… is the preferred route; "Or connect by hand" is the manual one. Both installs are links (codex://, .mcpb)Match
Is it connected? Is it working?Xcode: "alerts you when the external agent connects … and when it's active". Notion: every agent app listed as Connected / Not Connected, disconnect in-app. Figma: "enabled and running" confirmationActive: the sidebar antenna (shipped). It is solid while a project is exposed, radiates while an agent is reading that project, and signs off with two taps when the agent has finished. It's per project, and it fires for every host. The register's Last asked column records it afterwards. Not covered: "connected" before the first question, which host asked, and the Settings install row, which still says Install after a successful installExceed on active
… and before the first questionXcode's alert fires on connect, before any workNothing until the first question, and nothing that names the hostShort, narrowly
Scope controlDEVONthink: exclude one item or a whole database; encrypted databases excluded automatically; "Redact sensitive content". Notion: full user permissionsPer project, off by default; only open projects; Anonymise; read-only toolsMatch DEVONthink per database; short of its per-item exclusion; exceed Notion
Errors and recoveryNotion and Figma: a troubleshooting page mapping each failure to one fix. None of the five speaks inside the chatEvery failure is one sentence naming one fix, spoken by the agent where the user is looking (section 6). The help page's troubleshooting table is still to be writtenExceed in context
ChatGPT Work vs ChatACE: "Standard Chat mode does not support local MCP", with screenshots of the mode selector and the Work toggleOne line in the pane. "Try now" lands in a Codex thread, which works. The help page has no screenshot yetMatch in the pane; short in the docs
After an app updateNone of the five says anything (ACE: "may change between updates")A version comparison says Update… and names the installed build. Outcome depends on P0.5Exceed
macOS permissionsNone of the five mentions them. DEVONthink's one reported failure (config never written) looks like a sandbox problem and has no official answerNone needed on 15–27, which is measured on 27 and still P0.1 for 15/26Exceed, pending P0.1

Closing the narrow gap: connected, and which host

Proposal · after the host has reached Bristlenose

MCP Agents
Claude Desktop
Claude Code
ChatGPT & Codex
Generic MCP

Adds Bristlenose to ChatGPT as a plugin. ChatGPT will open and ask you to install it.

❋
Bristlenose
Plugin 0.32.0 (3950) · ChatGPT connected today at 14:02

In ChatGPT, ask in Work mode and start with @Bristlenose — otherwise its tools aren’t used.

Or connect by hand

Already shipped · the sidebar antenna, per project

Bristlenose
Onboarding study
14 sessions
Checkout interviews
9 sessions
Pilot
3 sessions

The antenna already answers "is it working?" (right-hand frame, drawn loosely: the real one is the SF Symbol antenna.radiowaves.left.and.right, animated through its three variable-value frames). It is solid for an exposed project, radiates while an agent reads it, and taps twice when the agent has finished. That beats Xcode's alert on two counts: it's per project, and it says when the agent has finished. Nothing in this plan should duplicate it, so the "is asking now" subtitle I first drew here is withdrawn.

What's left is narrow. The antenna can't show "connected" before the first question or which host asked, and the install row still says Install after a successful install. Two small changes close it, both facts about our own endpoint, not about the client:

Worth it? It's optional. For a researcher, the first question plus the antenna already proves it works. The check-in mostly fixes the Settings row, which would otherwise read as if the install hadn't taken.

The other shortfalls, and what I'd do. ChatGPT Work vs Chat in the docs: when the help page (work-list item 2) ships, give it ACE's treatment, with a screenshot of the Chat | Work toggle and one sentence on why Chat can't reach a local app. Per-item exclusion (DEVONthink): this is a product question, not part of this plan. A per-session "exclude from agents" would be the Bristlenose version. Park it.