Pair the companion on this Mac
The service and CLI provide a private local pairing handoff for the Mac companion. Same-Mac route selection follows ADR 0204. The native Mac app requests its first connection automatically, without a code or CLI pre-step, through the service's owner-private Unix issuer. This API does not install or launch the app.
The service publishes companion-issuer.json after its primary listener binds.
The state root must be canonical, owned by the current UID and not writable
by others; existing mode 0755 roots work. The companion directory is 0700,
the descriptor and handoff files are 0600, and the Unix socket is 0600.
CLANKIE_STATE overrides the default ~/.clankie root. The native app rejects
symlinks, foreign ownership, hard links and unsafe modes, validates the exact
socket path beneath that private directory, and checks its connected peer UID.
POST /v1/pairing/local/handoff exists only on this private Unix socket. It uses
the service's configured owner identity to mint a single-use offer and atomically
write companion-offer.json, replying only { "ready": true }. No operator
bearer crosses this request and no TCP route exposes it. The existing
clankie pair --local-companion --json command remains available for older app
versions: it sends its operator bearer only to the validated private Unix issuer
and prints only handoffPath, never the offer. Do not run either owner handoff as
root or place its state in a shared folder. Same-user processes share the macOS
account boundary for this private issuer; the session itself goes only to a
trusted app (below). Loopback reachability or headers alone grant no authority.
The companion reads the same-UID file with no symlink following, verifies its
ownership, mode, size and expiry, then posts { "offerSecret": "…" } to
POST /v1/pairing/local/redeem at its controlPlaneUrl.
Only an app the owner trusts can redeem
(ADR 0204, VUH-2604).
The service finds the process holding the client end of the redeem's loopback
connection. It mints a session only when that process's running code satisfies
one of the owner's trusted code requirements. Trust an app with
clankie companion trust /Applications/Clankie.app.
- A Developer ID build is trusted by team and bundle id.
- An ad-hoc or development build is trusted only by its exact code hash, so it is trusted again after each rebuild.
The list starts empty, and an empty list refuses every app. Any other app gets
403 companion_app_untrusted with a reason (untrusted, not_signed or
peer_unknown), and the offer stays valid for the trusted app. Each refusal is
recorded as pairing.local_companion.refused with the app's pid, identifier,
team, code hash and path. An accepted redeem records the same identity on its
device.activated or device.session.refreshed event. Proof builds for other
work pair through clankie pair --purpose proof; they are not trusted
companions. Use
@clankie/protocol/local-companion for the file, request and session schemas.
Remove the file after redemption. Keep the returned device token in the app's
credential store; restore and refresh through the existing device APIs.
A lost response must be reconciled with the app's stored session; a consumed
offer cannot be replayed. A newly requested private handoff can recover the same device. The Mac app
automatically renews lost or invalid private-local sessions once per restoration.
Explicit revocation asks once to connect again and creates a new identity;
remote sessions, host outages and rejected encrypted routes do not switch local.
POST /v1/pairing/local/offer requires the operator bearer; it returns
version, offerSecret, and expiresAt. These offers expire after five
minutes and die with the service. Minting another invalidates the previous
local offer. Redeeming succeeds once; replay is consumed (409), and an unknown,
expired or superseded offer is expired (410). The session response uses the
existing device identity, grants and signer, plus the host name and explicit
loopback control/relay endpoints. First redemption activates Take Control;
later handoffs preserve the active companion's device ID and grants, including
after a service restart. Revocation stays effective: fresh authorized pairing
creates a new identity and never revives the revoked token.
Restoring with GET /v1/devices/self and refreshing with
POST /v1/devices/self/session/refresh over the primary native loopback listener
keep the service's loopback control/relay endpoints, even when the configured
direct route advertises a LAN or tailnet address for other devices. Headers do
not establish same-Mac provenance; forwarded gateway requests retain their
configured route. A successful private handoff and device reuse prove the
service boundary, not a fresh signed-app install.
Local pairing exists only on a self-hosted Mac. The primary listener checks the
actual local and remote socket addresses, numeric loopback Host, native JSON
request, and browser/forwarding headers. Origin, Referer, browser Fetch Site/Dest
and forwarding headers are refused. Node's native fetch adds only Fetch Mode
cors, which is allowed. A browser page cannot read the private offer, mint one,
or redeem it. Another local UID cannot read the handoff or obtain the owner
bearer. Loopback reachability alone grants nothing. Local offers are absent from
the generic offer store, public gateway and LAN device doorway, and carry no
short code, deep link or gateway publication. Pairing responses are no-store;
secrets never enter events or logs.
Read /v1/captain/readiness for setup state. The companion and console share
credentials and model selection; neither keeps a separate setup flag. Device
subscription sign-in and the first-run key-entry limit are documented in
model keys.
Joining another computer
Companion pairing makes a device portal on this Mac. clankie join registers a
machine where Clankie can act, with an outbound gateway connection and no SSH
route or inbound listener. The joining host generates a 256-bit approval code
locally and advertises only its hash; an existing owner
device approves its access level and a subset of its advertised directories.
The registry mints a separate join-UUID and scoped capability. These are not
device or operator credentials, and the client cannot approve itself. Approval
travels through the existing encrypted owner surface. The lease and channel
are independently authenticated and encrypted; the gateway cannot read or
forge commands, results or capabilities. Keep approval codes out of evidence.
Use join --gateway URL --host HOST_ID --directory PATH, join approve CODE --access LEVEL --directory PATH, and join resume|status|leave; see
the CLI guide. Keep the joining terminal running. Leaving or
machines remove join-UUID revokes the capability; reconnecting needs fresh
approval. The receiver intersects current policy with its original approved
ceiling and canonical directory grants; raising the ceiling needs fresh approval.
Native worker/screen adapters and hosted gateway routing in clankie-ops are
named follow-ups; real Mac/Windows gateway captures are not yet verified.
