Part of #6.
The page asks a visitor to grant Full Disk Access and let an agent run unattended on their Mac, and it does not tell them the two things that decide whether that is acceptable: approvals are off by default, and the text plus whatever files the agent opens go to Anthropic through the CLI. Both facts are already in the code. A reader who installs on the strength of the one security line and finds out afterwards has been surprised by the part of the product that most needed saying up front.
What is there today
- The whole security story is two lines in one bento cell,
resources/views/index.stx#L308-L309: "Nobody else can use it" plus "Only one-to-one chats with handles you allow. Group chats are ignored, and only Uplink.app gets Full Disk Access, not your terminal." - One FAQ restates the same thing at
resources/views/index.stx#L73-L74, and its answer ("Anything Claude Code can: search the web, read and edit files, run your tests") describes the capability without saying it is exercised with no prompts. app/Uplink/config.ts#L67defaultspermissionModetobypassPermissions. The value is passed straight to the CLI as--permission-modeatapp/Uplink/engine.ts#L71-L72, from both entry points: the from-source service atapp/Commands/Uplink.ts#L25and the downloadable app atapp/Uplink/desktop-agent.ts#L178.UPLINK_PERMISSION_MODEis the only override, and it appears nowhere in theUPLINK_*block of.env.example#L141-L160, so a from-source user cannot discover it from the file they are told to copy.- The page's only data-flow statement is
resources/views/index.stx#L245: "No server in between. Your Mac reads the message, does the work and answers through Messages." That is true of Uplink, and a reader will take it to mean nothing leaves the Mac. - The allowlist facts worth stating are all in
app/Uplink/filter.ts, which names itself the security boundary of the app atapp/Uplink/filter.ts#L7-L9. Every branch ofclassifyrejects rather than admits: group chat atapp/Uplink/filter.ts#L37-L38, a chat not on the allowlist at#L40-L41, an outgoing message in someone else's chat at#L43-L48, a sender that does not match the chat at#L49-L55, tapbacks, edit markers and system items at#L57-L58, empty text at#L60-L62, and Uplink's own reply coming back around at#L64-L68. - An empty allowlist means this Mac's own Messages handles, not everyone:
app/Uplink/uplink.ts#L202. When neither a configured list nor an own handle is available, every text is ignored and the log says so,app/Uplink/uplink.ts#L134. catchUpMsis 30 minutes atapp/Uplink/config.ts#L57, so a Mac waking up does not replay a day of texts. The reply prefix atapp/Uplink/config.ts#L58is half of the loop guard.- Local state is genuinely local. The downloadable app keeps one SQLite file in Application Support,
app/Uplink/sqlite-store.ts#L7-L8andapp/Uplink/settings.ts#L16. The token lives in the Keychain and not in that file,app/Uplink/settings.ts#L9-L11,#L19-L20and#L74-L78. The dashboard refuses to render outside a local environment,resources/views/dashboard.stx#L12-L17. The menubar popover is served on loopback with cross-site requests refused,app/Uplink/desktop-agent.ts#L277-L286. No file underapp/Uplink/makes an outbound HTTP request. - Full Disk Access goes to the app rather than to
bun, because TCC charges a child process's access to the app that launched it,app/Uplink/launcher.ts#L5-L10. The exception is running the watcher in a terminal, which does need the grant for that terminal,README.md#L104. - The page itself is the one thing that does phone home.
resources/views/index.stx#L104-L106preconnects tofonts.googleapis.comandfonts.gstatic.comand loads Geist and Geist Mono from Google, andresources/views/404.stx#L16-L18loads Geist the same way. There is nopublic/fonts/directory, so every visitor's IP reaches Google before the first paint.
Proposal
- Add one section to
resources/views/index.stxbetween the bento section's closing tag atindex.stx#L314and the download section atindex.stx#L316, with anidandscroll-mt-16matching the other anchored sections atindex.stx#L240andindex.stx#L316, and a nav link beside the two atindex.stx#L181-L182. No new route and no new view. - State the permission model plainly in that section: the agent runs with
bypassPermissions, which means no approval prompts, so a text that is accepted gets unattended read, write and execute on the Mac. NameUPLINK_PERMISSION_MODEas the way to change it. Say that an unlocked or stolen iPhone with access to the allowed thread is therefore a compromise of the Mac. - State the limits that make that acceptable, each of which is true today: the allowlist is fail closed and rejects by default, group chats never reach it, tapbacks and system items never reach it, an empty allowlist means this Mac's own handles, a 30 minute catch-up window means a waking Mac does not replay old texts, and the reply prefix keeps Uplink from answering itself.
- State where the data goes, in the same section: the text and the files the agent opens go to Anthropic through the CLI on the user's own plan, no stacksjs server is involved, runs are recorded in a local SQLite database shown only by the local dashboard, the token is in the Keychain, and there is no telemetry and no account.
- Rewrite
index.stx#L245so it says what it means. "No server in between" becomes a statement about Uplink adding no server of its own, with the trip to the model provider named rather than left to inference, and it links to the new section. - Reword the bento cell at
index.stx#L308-L309so it is no longer the whole story, and point it at the new section. - Self-host the fonts. Add Geist and Geist Mono woff2 files under
public/fonts/, replaceindex.stx#L104-L106and404.stx#L16-L18with@font-facerules, and drop both preconnects. - Add
UPLINK_PERMISSION_MODEto theUPLINK_*block in.env.example, with a one line comment, so the override the new copy names is documented where a from-source user will look for it.
Requirements
- Do not write that nothing leaves the Mac. The new copy replaces a vague impression with a specific claim, so a sentence like "it all stays on your Mac" would be a regression even though the current page never says it outright. The prompt and the file contents the agent reads go to Anthropic, and the section has to say so.
- Name the exact strings.
bypassPermissionsandUPLINK_PERMISSION_MODE, spelled as they appear atapp/Uplink/config.ts#L67. A reader who greps the repo for what the page told them has to find it. - Write the data-flow sentence so a second engine does not invalidate it. Attribute the trip to the CLI that runs and its provider rather than hardcoding one vendor name into the mechanism sentence, so this section does not need rewriting in the engine track.
- Keep the Full Disk Access sentence true for both install paths. The claim that only Uplink.app gets the grant holds for the downloadable app and for
buddy uplink:install. It does not hold forbuddy uplink:watchin a terminal, perREADME.md#L104. - Self-host the same weight axes that are requested today.
index.stx#L106asks for Geist 400..700 and Geist Mono 400..600, and404.stx#L18asks for Geist 400..700. Keepfont-display: swapand keep the existing fallback stacks atindex.stx#L132-L133. - Use the tokens and utility classes already defined.
u-line,u-raised,u-reveal,u-monoandu-mutedatindex.stx#L132-L138, and the reveal animation atindex.stx#L162-L169. No new colour, no new radius, no client script. - No em-dash and no en-dash in the new copy. This is a house rule in
AGENTS.md, and neitherindex.stxnor404.stxcontains one today.
Acceptance
- The page renders a section between the bento and the download sections, reachable from the nav, and the download section still starts where it did.
- A reader who has never opened the repo can find on the page: that approvals are off, the literal mode name, the environment variable that changes it, that an unlocked phone with access to the allowed thread is a compromise of the Mac, and that the text and the files the agent opens reach Anthropic.
- Every factual sentence in the new section maps to a line in the code that says it. A reviewer can walk the list under "What is there today" against the copy and find no claim that is not in
app/Uplink/. - A grep for
fonts.googleapisandfonts.gstaticacrossresources/views/returns nothing, both font families load frompublic/fonts/, and the browser network panel shows no third-party origin on either page. - A grep for the em-dash and en-dash characters across
resources/views/index.stxandresources/views/404.stxreturns nothing. UPLINK_PERMISSION_MODEis present in.env.example../buddy lintand./buddy typecheckpass.