Cupertino 1.8.0
Version 1.8.0 ·
Added
A fifth surface,
screen, brokering ScreenCaptureKit rather than an Apple app.apple_screen_list_targets,apple_screen_diagnosticsandapple_screen_capture_surface, the last behindallowCapture— off by default, and superseding the one-time-code gates Messages and Safari each carried.It has no npm package and could not have one. The Screen Recording grant lives in the app, so the server has to live there too.
surfaces.jsongains aruntimefield (node | swift), the generator enforces thebundleId/npmNamerules that each runtime implies, andServerHostanswers this surface's JSON-RPC directly throughScreenServerinstead of spawning a node child. Everything that loops over surfaces for a reason unrelated to packaging — the bridge, the closed table, the settings UI — still takes every surface unchanged; only the lists that actually mean "has a node package" read the newNODE_SURFACES.Identity was measured rather than assumed: an
LSUIElementDeveloper ID bundle holdskTCCServiceScreenCapture, its spawned children inherit it, and the grant survives both re-signing and the bundle moving.Settings separates apps from capabilities, and each has a real icon.
surfaces.jsoncarries an explicitkind(app | capability) rather than inferring one from a missingbundleId. An app takes its icon from LaunchServices as before; a capability names its owniconPath— Apple's own Settings extension, so it sits beside the app icons instead of reading as a different kind of row — with a mandatorysymbolfallback, because Apple's extension names under/Systemare not predictable (DisplaysExt.appexsits besideSound.appex).screenis the first capability. The sidebar splits into two titled sections so it no longer reads as an app nobody can find in their Applications folder, and the automation caption gains a case of its own: "not needed — there is no app to script", rather than the app-only "reads only" framing.Claude Code is a drop-in client instead of a copy-a-command one. The app writes
~/.claude.jsondirectly — top-levelmcpServersfor the client row,projects[<dir>].mcpServersfor local-scope folders — behind a stamp-and-retry guard that refuses a merge computed from bytes a concurrent session has since replaced, and a0600mode on any file this app creates.The previous refusal rested on two claims that do not hold. The file's mode survives the swap; and the concurrency risk is the same read-modify-write that
claude mcp addperforms from a second process, so handing over the command relocated the race rather than avoiding it — while giving up the one advantage this app has, which is that it reads back the file it wrote and can report a clobbered entry.
Fixed
The in-process server is gated by something. The screen surface shipped with no test of any kind while every node surface has a suite, and
scripts/verify-servers.shstructurally cannot cover it: it scans acli.jsfor bare imports and spawns it under the bundled runtime, and a surface the app serves itself has neither.make bundleruns only that script, so a broken in-process server would have reached a signature — which is the exact failure verify-servers.sh was written for, after three releases shipped servers that died on their first line.make screen-checkdrivesScreenServer.handledirectly: no socket, no app launch, no licence, no TCC grant, so CI runs it as a build gate. It pins the handshake, that a notification draws no reply, both states of the capture gate, the closed-table refusals, that the capture schema offers no surface without an app, and that an unreadable window list renders asnullrather than as an empty list — absent and EPERM being different findings. Each assertion was checked by reintroducing the bug it describes.make smoke-swifthandshakes app-served surfaces through the bridge, and is deliberately not a build gate: the socket is claimed by bundle identifier, so on any machine with Cupertino installed it would test the installed copy and pass while the artifact is broken."Install and Relaunch" worked, having read as broken. The relaunch was held until
Sessions.liveemptied, but a session is a connection held open for a client's lifetime rather than a tool call, so it was rarely empty and the held install never fired. Sparkle's postpone hook only fires for an explicit click in the first place — automatic updates are off — so this deferred nothing it was meant to. The EOF it was guarding against is still handled unconditionally a few milliseconds later, inupdaterWillRelaunchApplication, which SIGTERMs every server for an ordinary shutdown.The Capabilities card no longer fails with an error about a file it does not use.
SurfaceCatalogspawned a node server throughServerLocatorfor every surface, which for a swift-hosted one failed with "dev.json is unusable" — a real error about something with no bearing on a server that is not a node process. It drivesScreenServer.handledirectly for that runtime now, so the card still demonstrates the real tool list rather than describing it a second time.The screen surface's detail pane names the permission it actually needs. Access showed only "Automation — not needed" and no Screen Recording row at all, so the surface read as ready while every tool would refuse. That row is wired to
Permissions.screenRecording(), and its button opens the settings pane rather than re-callingCGRequestScreenCaptureAccess, which only ever prompts once. The Store card is hidden for a capability instead of asserting that everything goes through Apple Events, and the write-gate caption no longer describes a gate a surface with no mutating tools does not have."Include contents" says what it actually redacts. The toggle and its caption described only the argument half of
CallCapture's redaction — a mail body, a message, a note's text — and said nothing about the result half, which is withheld entirely on the grounds that every word of it was answered by the server. A reader would reasonably have expected return values in the log. It is relabelled from "Include message contents", since it governs nine surfaces and the app-wide log and "message" fit only two of them, and the caption now names «redacted», the literal string that appears.