Cross-platform account switch truth and service recovery

Status: reviewedDate: 2026-09-03Branch: master @ eabbc76Type: spec

User-facing goal

When a user chooses a Claude account, jacked must report exactly what changed on that operating system: the desired default, the credential store last observed by Claude, and the activation state of existing sessions. It must never claim a successful switch after a required store fails, never label historical session data as verified runtime identity, and never kill an unproven process while recovering its background service.

Incident contract

The 2026-09-03 incident produced three independent failures:

  1. The Udifi account was written to SQLite, ~/.claude/.credentials.json, and ~/.claude.json, while macOS rejected the Keychain update. The API still returned active.
  2. Two running sessions remained grouped under Raider because session identity was only detected at session start; response hooks refreshed liveness without re-observing identity.
  3. An untracked jacked process owned port 8321 while launchd repeatedly tried and failed to start its registered replacement. PID-file absence made status report stopped.

The same inspection exposed a secret-bearing environment inherited by the launch agent. Supervisor diagnostics must never emit full environments, and service processes must be created with a curated environment.

Acceptance criteria

Architecture

Capability-driven credential topology

A versioned registry binds an exact resolved Claude executable build and configuration mode to a backend contract. Each record names the authority, required mirrors, optional metadata, every declared consumer, the writer-fence protocol, registry provenance, and capability epoch. Read-only local checks may narrow the contract but never certify write, locking, or reload behavior. Unknown or remotely changed behavior trips a runtime kill switch.

ModeAllowed behaviorExisting sessionsAuto-swap
SCOPED_COOPERATIVEProcess-scoped credential activation under the certified contract.Only the launched scope is affected.Allowed only through the transaction engine.
GLOBAL_COOPERATIVEShared authority with certified upstream lock and reload behavior.Reload expected, still not provider-verified.Allowed only through the transaction engine.
GLOBAL_UNCOOPERATIVEForeground observed-target operation only. It may update desired intent and last observation, but never the committed authority pointer.Restart required; runtime unverified.Recommendation only.
UNSUPPORTEDNo credential mutation.Unchanged.Disabled with remediation.
flowchart TD
  U[Local user chooses account] --> G{Capability mode}
  G -->|unsupported| X[Return unsupported]
  G -->|shared uncooperative| Q[Foreground observed-target operation]
  G -->|cooperative| T[Verified transaction]
  Q --> UO[observed_target_unfenced; never committed]
  T --> P[Commit pending SQLite row]
  P --> W[Write and read back authority]
  W -->|matches transcript| C[Atomic DB finalization]
  W -->|old authority unchanged| F[failed_preserved]
  W -->|unknown or concurrent value| I[indeterminate]
  C --> S[Report storage and session axes separately]
flowchart TD
  U[Local user chooses account] --> G{Capability mode}
  G -->|unsupported| X[Return unsupported]
  G -->|shared uncooperative| Q[Foreground observed-target operation]
  G -->|cooperative| T[Verified transaction]
  Q --> UO[observed_target_unfenced; never committed]
  T --> P[Commit pending SQLite row]
  P --> W[Write and read back authority]
  W -->|matches transcript| C[Atomic DB finalization]
  W -->|old authority unchanged| F[failed_preserved]
  W -->|unknown or concurrent value| I[indeterminate]
  C --> S[Report storage and session axes separately]

Durable switch state

credential_switches is the sole durable journal. A pending row is committed before any OS store changes. It contains symbolic phases plus domain-separated before_hmac and target_hmac verifiers. Each HMAC covers the full transcript: machine-install ID, operation ID, exact backend locator, capability epoch, canonicalizer version, intended account/org identity, state role, and exact canonical credential digest. The install key is stored in machine-local user-private state. HMAC material is never exposed outside recovery code.

Raw credential snapshots remain memory-only. If the process crashes after the authority write, recovery compares the authority with the before and intended transcript verifiers. A third value, a missing key, a different machine, or an unrecognized canonicalizer produces indeterminate. Stable account identity matching the target may be reported separately, but it never authorizes token repair.

A minimum writer-protocol fence ships with the transaction engine. Mutation is disabled if an active legacy service, direct launcher, or other possible writer cannot prove protocol v2 and the same capability epoch. An arbitrary same-user copy of old code cannot be prevented from executing outside managed entry points; if it is seen or its write changes the authority, the system records an unfenced writer and subsequent state becomes indeterminate or mutation-disabled. Because arbitrary writers cannot be excluded for a shared uncooperative backend, that mode can never finalize a committed authority pointer.

Session truth model

FactMeaningEvidence
Started asEarliest recorded configuration span.Launch binding or first store observation.
Latest observedMost recent credential configuration visible to jacked.Scope, credential revision, store resolver, and timestamp.
Desired globalThe user-requested or committed default.Switch transaction state, not session state.
Runtime verifiedThe identity proven to have served a session request.Nullable until Claude exposes provider/process evidence.

Hooks remain fast and noninteractive. They append/coalesce nonsecret observation requests and liveness events. The service resolves stores off the event loop and atomically publishes a secret-free, evidence-qualified snapshot for stdlib-only statusline readers. The statusline never reads raw credential metadata or infers an active identity; a missing, stale, or conflicting snapshot renders desired/unknown. A global switch marks global sessions pending; only subsequent observations open a new span. Pinned launch evidence wins only in certified scoped mode.

Service ownership and control

flowchart LR
  SUP[launchd / systemd / Task Scheduler] --> CLEAN[Pre-interpreter clean environment]
  CLEAN --> BOOT[Versioned minimal launcher]
  BOOT --> LEASE[Kernel lifetime lease]
  LEASE --> MAN[Private instance manifest]
  MAN --> APP[Jacked tray and API]
  CLI[Local CLI or tray] --> IPC[Unix socket / named pipe]
  IPC --> APP
  WEB[Browser] --> HTTP[Dashboard HTTP]
  HTTP --> APP
  APP --> HEALTH[Public non-sensitive health]
flowchart LR
  SUP[launchd / systemd / Task Scheduler] --> CLEAN[Pre-interpreter clean environment]
  CLEAN --> BOOT[Versioned minimal launcher]
  BOOT --> LEASE[Kernel lifetime lease]
  LEASE --> MAN[Private instance manifest]
  MAN --> APP[Jacked tray and API]
  CLI[Local CLI or tray] --> IPC[Unix socket / named pipe]
  IPC --> APP
  WEB[Browser] --> HTTP[Dashboard HTTP]
  HTTP --> APP
  APP --> HEALTH[Public non-sensitive health]

The API singleton is per OS user. Login-session tray processes are replaceable clients and never create a second API lease. The instance manifest records protocol/build/generation, per-user identity, active login-session attachments, process creation identity, resolved runtime path, bind plan, quarantine port when used, and a private control nonce. POSIX uses mode 0600 and ownership checks; Windows uses a current-user SID DACL and reparse-point protections.

Lifecycle control uses a framed, versioned native IPC protocol with peer identity, transcript authentication, request nonces, expiry, timeouts, and an action allowlist. Browser HTTP never exposes lifecycle control. Account mutation remains a local same-origin endpoint protected by Host/Origin checks, CSP, no-store responses, and a server-persisted action ID that is both the atomic replay guard and idempotency key. Identical retries return the stored operation state/result; mismatched reuse is rejected.

Forced termination is platform-specific and race-free: Linux retains a pidfd; Windows retains a process handle and verifies creation time; macOS sends no forced signal to an unmanaged process. Managed processes stop only through a supervisor artifact whose ownership and exact ServiceSpec generation are proven.

The supervisor or a minimal non-Python launcher constructs an explicit allowlisted environment before the Python interpreter starts. POSIX supervisors invoke /usr/bin/env -i directly with fixed validated arguments. Windows invokes a versioned, ACL- and hash-verified PowerShell -NoProfile -NonInteractive script that clears a .NET ProcessStartInfo environment and starts Python through CreateProcessW. The launcher is a narrow no-log trust boundary: it may receive the supervisor environment where the OS offers no cleaner primitive, but never reads, logs, or forwards non-allowlisted values. Python runs in isolated mode from a validated runtime. Launcher artifacts are checked-in package data, installed to immutable versioned paths, verified against ServiceSpec, and retained through handoff.

Truth model and API outcomes

OutcomeHTTPDB active pointerUser message
committed200Updated atomically with audits.Default storage verified; existing-session state shown separately.
committed_degraded200Updated; counts as committed for residency only when the failed surface is declared optional for every consumer.Authority verified; named optional metadata needs repair.
observed_target_unfenced202Committed pointer unchanged; desired intent and timestamped observation may update.Target was observed, but concurrent writers cannot be excluded. Restart required; sessions unverified.
interactive_required428Unchanged.Foreground Keychain authorization is required.
interactive_operation_in_progress / busy / concurrent_write / diverged409Unchanged.Show persisted operation status, observed conflict, or manual retry action.
restart_required409Unchanged unless accompanied by an observed-only desired update.Explain why live activation is unproven and provide a safe relaunch action.
unusable / unsupported422Unchanged.Explain credential or capability requirement.
failed_preserved503Unchanged.Previous authority was read back unchanged.
indeterminate503Unchanged; pending retained.Do not retry automatically; require recovery or re-auth.

Non-goals

Files and interfaces involved

AreaInterfaces
Credential corejacked/credentials/*, compatibility wrappers in jacked/api/credential_helpers.py
Writers/readersjacked/api/routes/auth.py, jacked/api/usage_monitor.py, jacked/web/auth.py, jacked/web/oauth.py, jacked/launch.py
Persistencejacked/web/database.py: credential switches, session evidence, action nonces, machine-local identity
Sessions/UIsession_account_tracker.py, statusline, session/account components, WebSocket watchers
Servicejacked/service/instance.py, native IPC, bootstrap, supervisor adapters, CLI/updater/tray integration
Security boundaryLocal-only credential mutation, CSP/CSRF/action-nonce controls, secret-negative logging and environment contract
VerificationUnit/state-machine tests, three-OS CI, and recorded live isolated Keychain/supervisor smoke evidence required before each platform flag is enabled

Reviewed through three swarm verification/adversarial rounds and a clean recursive design review. Generated from the jacked HTML artifact template.