This plan implements the reviewed cross-platform account switch truth specification. Every task cites its acceptance criteria. Work is split into four reviewable pull requests; each leaves the product internally consistent and independently testable.
Invariant summary
Storage observation, desired default, existing-session activation, and provider verification are different facts.
Only the canonical resolver answers active identity. Direct Keychain/file/DB inference is removed from callers.
Only a capability-certified mode may claim scoped or live switching.
An uncooperative store can produce a timestamped observation, never a committed activation.
A service may control a process only after exact ownership proof.
Credential mutation remains disabled whenever a legacy or otherwise unfenced writer cannot be excluded.
Every new state surface is secret-negative by construction.
Versioned UDS/named-pipe protocol, peer identity, nonce/generation validation and command allowlist.
jacked/service/spec.py
Canonical ServiceSpec, artifact ownership and generation contract delivered before supervisor-mediated control.
jacked/service/environment.py
Curated cross-platform environment construction and typed structured redaction.
jacked/service/launcher/
Minimal pre-Python OS launchers in immutable versioned paths; create an explicit clean environment and validate the runtime entry point.
jacked/service/supervisors/*.py
launchd, systemd-user, Task Scheduler and manual adapters rendered from ServiceSpec.
jacked/web/database.py
Additive schemas and atomic methods for credential switches, action nonces, observation inbox and session evidence.
jacked/api/routes/auth.py
Local mutation gateway, structured outcomes, active resolver and v1/v2 session APIs.
jacked/data/web/js/components/account-actions.js
Precise switch states, local-only guard, repair and restart guidance.
jacked/data/web/js/components/sessions.js
Evidence-qualified session labels and pending/conflict states.
Tasks
PR 1: Truthful credential activation
Task 1.1: Freeze the capability and outcome contracts
Traces to AC1, AC3, AC4, AC6, AC8, AC9.
Add failing tests for all capability modes and result axes before production code, including observed_target_unfenced, restart_required, and interactive_operation_in_progress.
Resolve executable identity through wrappers/symlinks to an exact build digest; bind config mode and registry provenance/version.
Ship a conservative registry: enumerate authority, required mirrors, optional metadata, every consumer, and writer-fence protocol. Scoped/cooperative flags remain false until isolated certification passes.
Implement the runtime mutation kill switch and hysteresis: any contract mismatch disables writes; re-enable only after a fresh capability resolution.
Define response enums and exact HTTP mapping from the spec, including storage, desired-default, committed-authority, session-activation and provider-verification axes. An observed-only result never updates the committed pointer.
Commit: feat(auth): define credential capability and outcome contracts
uv run python -m pytest tests/unit/test_credential_capabilities.py tests/unit/test_use_account.py -q
uv run ruff check jacked/credentials tests/unit/test_credential_capabilities.py
Task 1.2: Build safe store adapters and canonical resolution
Traces to AC1, AC2, AC3, AC6, AC8, AC9.
Implement file-store contract with strict JSON parsing, user-private staging, symlink/hardlink/reparse rejection, durable replace, readback and abandoned-stage cleanup under the switch lease.
Add matching Darwin-only pyobjc-framework-Security>=10,<13 and pyobjc-framework-LocalAuthentication>=10,<13 dependencies; test imports across supported macOS/Python versions; implement full-locator SecItemCopyMatching, SecItemUpdate, and foreground-only SecItemAdd.
Keep Keychain reads/writes off the FastAPI loop. Use a noninteractive LAContext for background work and map errSecInteractionNotAllowed; model a visible serialized operation when foreground mutation prompts.
Implement canonical exact-byte transcript digesting with a versioned strict parser. Reject duplicate/ambiguous JSON keys and unknown canonicalizer versions.
Implement the canonical resolver and replace Keychain-first/file-only/file-first drift in credential helpers, usage monitor, auth routes, launch, web auth and OAuth.
Atomically publish a per-scope, nonsecret, evidence-qualified resolver snapshot for the stdlib-only statusline. Missing, stale, or conflicting snapshots render desired/unknown; statusline never reads raw credential metadata.
Add a static contract test that fails when credential-store paths or Keychain APIs are read outside the approved adapters.
Commit: refactor(auth): centralize credential stores and active identity
uv run python -m pytest tests/unit/test_credential_sync.py tests/unit/test_active_account_cc_skip.py tests/unit/test_credential_resolver.py -q
uv run ruff check jacked/credentials jacked/api/credential_helpers.py jacked/api/usage_monitor.py jacked/web/auth.py jacked/web/oauth.py jacked/launch.py
Task 1.3: Implement the durable switch state machine
Traces to AC1, AC2, AC4, AC7, AC8, AC9.
Add credential_switches and machine-local recovery-key metadata migrations. Put the random recovery key outside roaming project state with mode 0600 or a current-user SID DACL.
Persist domain-separated before_hmac and target_hmac verifiers over the full transcript: machine/install ID, operation ID, state role, backend locator, capability epoch, canonicalizer, exact canonical digest and intended account/org. Never serialize raw credential snapshots.
Deliver the minimum writer-protocol fence in PR 1. Discover active old services/direct launchers conservatively, persist a writer epoch, and disable mutation whenever every possible writer cannot prove protocol v2 and the same capability epoch. Never infer absence from PID/session rows alone.
Implement lock order: writer-protocol fence, switch lease, certified upstream credential lock, SQLite writer. Hooks never enter this chain.
Commit pending with SQLite FULL durability before OS mutation. Interaction may occupy the serialized operation, but other callers receive interactive_operation_in_progress rather than starting a second writer.
Validate target structure/expiry without rotating tokens. Stage subordinate files, revalidate capability epoch, observe authority, write/read back authority, then publish mirrors.
Atomically finalize the active pointer, transaction outcome, swap audit and decision audit in one SQLite transaction.
Implement pending recovery: classify before/target/third-value HMACs; third value or stable-target/different-revision remains indeterminate while reporting observed stable identity separately.
Add explicit abandon only after foreground re-auth; expire requested targets and require manual retry.
uv run python -m pytest tests/unit/test_credential_transactions.py tests/unit/test_credential_recovery.py tests/unit/test_credential_writer_fence.py tests/unit/test_multi_org.py -q
uv run ruff check jacked/credentials jacked/web/database.py
Task 1.4: Wire manual, OAuth, launch and auto-swap behavior end to end
Traces to AC1, AC2, AC4, AC7, AC8, AC14.
Route Use Account, OAuth activation/re-auth and launch preparation through declared transaction contexts. Remove global-switch writes to per-account directories. In GLOBAL_UNCOOPERATIVE, update only desired intent and timestamped observation, never committed authority, committed audits, residency, or activation broadcasts.
Convert 429 recovery to DB-only/recommendation behavior. Permit auto-swap writes only in certified cooperative modes; otherwise emit a durable recommendation and one-click safe local launch action.
Gate credential mutation to loopback clients. Remote clients get a read-only explanation; do not add incomplete authentication.
Use a server-persisted action ID as both replay guard and idempotency key, bound to page session, exact request digest, action and expiry. Identical retries return persisted status/result; mismatched reuse fails. Send Cache-Control: no-store; add strict CSP and disable production API docs.
Add an operation-status endpoint for interactive/lost-response recovery. Do not launch a second Keychain writer until readback classifies the prior operation.
Move DB/audit/broadcast/residency side effects behind committed outcomes. Count committed_degraded as committed only if the failed surface is declared optional for every consumer.
Test two installed Claude builds with reversed store precedence, response loss before/after DB finalization, identical retry, and altered action-ID reuse.
Replace generic success toasts with exact storage/session axes and repair/restart actions.
Preserve compatibility wrappers until every caller/test patch is migrated; add a removal warning for the next major release.
Update README, troubleshooting, architecture notes and release notes in this PR for fail-closed, recommendation-only and observed-but-unfenced behavior.
Commit: fix(auth): make account activation verified and truthful
uv run python -m pytest tests/unit/test_use_account.py tests/unit/test_dual_token.py tests/unit/test_oauth_manual_flow.py tests/unit/test_web_js_swap_ui.py tests/unit/test_web_js_account_menu.py tests/unit/test_auth_action_nonce.py -q
uv run ruff check jacked/api/routes/auth.py jacked/web/auth.py jacked/web/oauth.py jacked/launch.py
PR 1 exit gate: fault-inject the exact incident (global file staging succeeds, Keychain returns ACL denial), plus an old writer between readback and DB finalization and an external overwrite immediately after an uncooperative readback. The first response is not success; the second is refused or indeterminate; the third is observed_target_unfenced, never committed. DB/audits remain truthful and no secret appears in logs/API/process arguments.
Add nullable scope, observed-at, evidence, state, credential revision, launch nonce and event idempotency fields to session spans.
Add a bounded/coalescing nonsecret session_observation_inbox. Service-offline behavior records pinned launch evidence but leaves global observation unknown.
Migrate old rows as legacy/unknown; never infer historical scope.
Preserve immutable historical spans. Earliest span is started-as; latest open span is observed configuration.
Commit: feat(sessions): store scoped identity evidence
uv run python -m pytest tests/unit/test_session_accounts.py tests/unit/test_active_sessions.py tests/unit/test_session_identity_migration.py -q
uv run ruff check jacked/web/database.py
Task 2.2: Make hooks and statusline scope-aware
Traces to AC4, AC5, AC6, AC9.
Explicitly accept SessionStart, Notification(auth_success), UserPromptSubmit, Stop and SessionEnd. Coalesce idempotently by session, event kind, credential revision and launch nonce; do not claim replay-proof ordering without an upstream event sequence.
Keep hook work to scope/revision/activity writes. Debounce store observation in the service and bypass caches for transaction-critical reads.
Supply nonsecret launch scope/nonce/revision env from jacked claude. Emit pinned only in certified scoped mode.
Require email plus organization for metadata fallback; conflict becomes unknown rather than a convenient match.
Make statusline consume only the resolver's atomic, secret-free per-scope snapshot. Validate freshness/evidence and show desired/unknown for missing, stale or conflicting state; never interpret .claude.json as active identity.
Commit: fix(sessions): observe credential scope on session activity
uv run python -m pytest tests/unit/test_session_start_hook.py tests/unit/test_launch.py tests/unit/test_statusline.py -q
uv run ruff check jacked/data/hooks/session_account_tracker.py jacked/statusline.py jacked/launch.py
Task 2.3: Ship truthful session API and UI
Traces to AC4, AC5, AC6, AC14, AC15.
Add /api/auth/session-states with started, observed, desired-global, pending, scope, evidence/freshness and nullable runtime-verified fields.
Preserve /api/auth/active-sessions for one release through a conservative adapter. Add no-store headers and never compress unknown into a confident active account.
Update account/session cards to render Started as, Observed configuration, Pending next activity, Pinned target, Credential conflict, and Runtime unverified.
Update watchers so observation state/revision changes repaint immediately.
Update README, UI copy, API/reference docs, troubleshooting, release notes and the v1-route deprecation timeline for the evidence-qualified session model. Remove claims that one click verifiably switches all running sessions.
Commit: fix(sessions): distinguish configuration from runtime identity
uv run python -m pytest tests/unit/test_active_sessions.py tests/unit/test_web_js_sessions.py tests/unit/test_websocket.py tests/unit/test_statusline.py -q
uv run ruff check jacked/api/routes/auth.py jacked/api/watchers.py
PR 2 exit gate: in a mixed global/scoped fixture, changing desired global account leaves pinned spans unchanged, marks global spans pending, and never populates runtime-verified identity. A metadata-target/Keychain-old conflict renders desired target plus runtime unknown in the UI and statusline. Legacy rows render unknown, not active.
PR 3: Provable service ownership
Task 3.1: Add the lease, manifest and inspection state machine
Traces to AC9, AC10, AC11, AC12, AC15.
Define the canonical ServiceSpec ownership/generation contract before any supervisor-mediated control. Hash/sign the expected artifact identity and reject name/label collisions.
Implement one per-OS-user API lifetime lease held until process exit. Treat each login-session tray as a replaceable client that discovers the same API and never owns a second API lease. Use POSIX advisory locking and a retained Windows file handle with SID ACL.
Atomically publish the private instance manifest after acquiring the lease; bind instance/machine/session nonce, process creation identity, UID/SID, executable/build/protocol/version/generation and bind plan.
Validate ownership, file type, parent, link/reparse state and process creation identity. Cleanup only when instance ID still matches.
Implement inspect states: managed healthy/degraded, verified unmanaged, stale manifest, legacy jacked, foreign listener, supervisor crash loop, quarantined and stopped.
Self-repair a missing/corrupt manifest only while the same in-memory instance still holds the lease.
Keep the legacy PID file inspect-only during compatibility; remove PID/port-only kill paths, including updater port-owner sweeps.
uv run python -m pytest tests/unit/service/test_instance.py tests/unit/service/test_process.py tests/unit/service/test_updater.py -q
uv run ruff check jacked/service/instance.py jacked/service/process.py jacked/service/updater.py
Task 3.2: Add private native control and discoverable quarantine
Traces to AC10, AC11, AC12, AC14.
Implement framed/versioned/nonced/time-bounded UDS control with peer UID on macOS/Linux and FILE_FLAG_FIRST_PIPE_INSTANCE named pipe with current-user/session SID DACL and remote-client rejection on Windows.
Expose only status, graceful shutdown and restart-handoff actions. Authenticate the complete request transcript with private control nonce, peer identity, action, instance ID, creation identity, generation, request nonce and expiry; reject replay.
Keep HTTP health public but non-sensitive. No bearer/control token, PID-control endpoint, or destructive action exists over HTTP.
When an ambiguous fixed-port listener exists, bind the owned service to a dynamic quarantine port, store it in the manifest and make CLI/tray/browser-launch/updater/status discover it. When a manifest exists or 8321 is ambiguous, clients never fall back to 8321.
Make supervisors treat one healthy quarantined instance as success, not a reason to spawn competitors.
Test IPC replay/manifest copy and enforce the no-fallback rule with a foreign listener on 8321 plus an owned quarantined instance.
Commit: feat(service): control owned instances over native IPC
uv run python -m pytest tests/unit/service/test_ipc.py tests/unit/service/test_quarantine.py tests/unit/service/test_bind.py tests/unit/service/test_cli.py -q
uv run ruff check jacked/service/ipc.py jacked/service/bind.py jacked/cli.py
Task 3.3: Make status/restart health-verifying
Traces to AC10, AC11, AC12.
Replace generic HTTP/port readiness with exact instance/build/protocol/generation and internal-watchdog health.
Route managed restart only through a verified ServiceSpec supervisor artifact and verified unmanaged shutdown through IPC. Linux forced fallback retains and signals through pidfd; Windows retains a process handle and rechecks creation time; macOS never force-signals an unmanaged process.
Fail closed on incomplete evidence and print structured remediation without raw process/supervisor output.
Add jacked service recover for crash-loop reset, quarantine discovery and guided legacy exit.
Commit: fix(service): verify identity across status and restart
uv run python -m pytest tests/unit/service/test_cli.py tests/unit/service/test_restart.py tests/unit/service/test_update_phases.py -q
uv run ruff check jacked/cli.py jacked/service
PR 3 exit gate: PID reuse between final inspection and signal, missing/altered manifest, wrong UID/SID, wrong executable, foreign supervisor artifact, foreign HTTP server and foreign port owner all result in zero signals or supervisor actions against the foreign process. The current orphan scenario becomes either verified recovery or quarantine with explicit guidance.
PR 4: Native supervision, environment hygiene and rollout
Task 4.1: Install a pre-interpreter sanitized launcher
Traces to AC9, AC11, AC13.
Implement the launcher concretely: POSIX supervisor artifacts invoke /usr/bin/env -i directly with fixed arguments; Windows uses a checked-in PowerShell -NoProfile -NonInteractive script whose .NET ProcessStartInfo clears and rebuilds the child environment before CreateProcessW. The boundary never reads, logs, or forwards non-allowlisted inherited values.
Package the Windows script and POSIX specification as deterministic package data; verify source hash/provenance, signer when releases are signed, OS ownership/DACL, architecture-neutral PowerShell behavior, and installed-wheel inclusion. Install to an immutable versioned path, atomically switch ServiceSpec, and retain the prior launcher through rollback.
Start Python in isolated mode and load only the validated runtime path. Test installed wheels on Windows x64/ARM64 and supported macOS/Linux architectures; fail closed on unsupported architecture or missing system launcher.
Derive home, user identity, PATH, temp and application directories from OS APIs or fixed validated values, not inherited variables. Allowlist locale and required Linux GUI/session-bus values; review CA/proxy values and reject proxy userinfo.
Resolve Keychain account identity from OS UID APIs, never optional USER variables.
Validate the candidate runtime import, build/protocol and entry point before supervisor activation; expose local repair state if invalid. Do not promise package rollback.
Apply the same pre-interpreter boundary/environment builder to manual starts, updater helpers, browser helpers and exec paths. Add structured redaction at logging/API boundaries.
Inject hostile PYTHONHOME, PYTHONPATH, sitecustomize, loader variables and secret canaries into the parent/supervisor. Prove they neither execute nor reach Python, its children, diagnostics, runtime process snapshots or APIs; the narrow launcher itself never emits inherited environment data.
Commit: security(service): sanitize every background process environment
uv run python -m pytest tests/unit/service/test_environment.py tests/unit/service/test_launcher.py tests/unit/service/test_updater.py -q
uv run ruff check jacked/service/environment.py jacked/service/updater.py jacked/cli.py
Task 4.2: Reconcile native supervisors on all three OSes
Traces to AC10, AC11, AC12, AC13, AC15.
Render every native artifact from the PR 3 ServiceSpec; reconcile schema/generation hash and foreign-artifact ownership without weakening it.
macOS: validate ownership, atomically replace plist, bootout/bootstrap/kickstart the exact per-user LaunchAgent, and verify the loaded generation.
Linux: install a user systemd unit only when the user bus works and no foreign unit exists; daemon-reload and enable-now; use bounded restart and watchdog only with implemented sd_notify. Retain manual mode for WSL/headless/non-systemd.
Windows: register one current-user API Task Scheduler task with IgnoreNew, bounded restart, unlimited execution and explicit battery settings; attach per-login-session tray clients separately. Validate task ownership before use.
Ensure the losing starter exits without triggering supervisor crash loops; persist terminal reasons and recovery command.
Make service status show disk spec, loaded supervisor, live runtime and drift independently.
uv run python -m pytest tests/unit/service/test_platform.py tests/unit/service/test_native_restart.py tests/unit/service/test_supervisors.py tests/unit/test_windows_update_batch.py -q
uv run ruff check jacked/service/supervisors jacked/service/platform.py
Task 4.3: Migrate safely and expand CI
Traces to AC11, AC12, AC13, AC15, AC16.
Preserve and extend the PR 1 writer-protocol fence throughout migration. An ambiguous legacy process keeps the new service quarantined and credential mutation disabled.
Preserve the old PID reader for two releases as diagnostic evidence; never control through it. Do not overwrite foreign launchd labels, systemd units or scheduled tasks.
Retire Windows VBS or another old launcher only after the new instance proves lease, manifest, IPC, build, protocol and generation, and survives at least one supervisor-driven restart/logon cycle. Keep the old artifact inert but recoverable until then.
Enforce a durable minimum-writer protocol epoch across managed service and manual entry points that re-exec through the launcher. Pre-v2 managed downgrade is unsupported until an explicit migration removes v2 journals/artifacts. Arbitrary direct execution of an old same-user binary cannot be prevented; detect its writes as unfenced and force subsequent state to indeterminate or mutation-disabled.
Add Ubuntu, macOS and Windows CI lanes with platform-specific unit selections and a common full suite. Real isolated Keychain, launchd, systemd-user and Task Scheduler smoke workflows may be manually dispatched, but a recorded pass is a release gate before enabling each platform migration flag.
Add token/environment canaries across journals, manifests, responses, logs, artifacts, diagnostics and subprocess argv.
Update README, troubleshooting/reference docs, architecture docs and release notes for supervisor/environment migration and rollback. Behavior-changing documentation was already updated in PRs 1 and 2.
Commit: test: verify account and service recovery on every OS
uv run python -m pytest -q
uv run ruff check .
uv run ruff format --check .
PR 4 exit gate: all three OS lanes pass and recorded real-OS smoke evidence exists before each platform flag is enabled; hostile startup and secret canaries remain absent; supervisor definitions reconcile idempotently; old launchers retire only after a verified handoff cycle; an invalid candidate runtime reports repair-required without port-based false health; docs make no unsupported live-switch or pinned-session claim.
Final verification
Run the full suite with uv run python -m pytest, then ruff check/format.
Run $dcr because credentials, local control, process termination, CSRF and cross-platform service installation are security-sensitive.
Run $cso; no high/critical finding may remain.
Run $qa against the local account/session UI. Cover committed, ACL denied, interactive pending, diverged, indeterminate, remote read-only, session pending, scoped and legacy unknown states.
Use disposable homes and processes for CLI/service end-to-end tests. Never use the maintainer's real Keychain, accounts, port 8321 listener or live Claude sessions.
Capture exact evidence for AC1-AC16 in the PR description: command, platform, result and artifact/log location.
After dependency and workflow changes, run $lockdown --workspace=/Users/jackneil/Github before release.
Required model-based failure points
Terminate or fault the credential engine before/after pending commit, staging, authority call, authority readback, mirror publication, DB finalization and broadcast. Combine each with concurrent switch request, same-account token rotation, two Claude builds with reversed precedence, capability/writer epoch change, old-writer race, recovery-key loss, SQLite busy/read-only, response loss, service restart and abandoned interactive operation. Also inject PID reuse after final inspection, foreign supervisor artifacts, IPC replay/manifest copy, 8321 plus quarantine, broken candidate launch, locked Windows launcher, reboot/logon, missing systemd user bus, managed pre-v2 downgrade, and direct execution of an old binary followed by a credential write. Every state must resolve to a documented outcome without automatic secret rewrite, foreign control, or secret exposure.
Rollout and rollback
Release PR 1 first. It prevents false success, centralizes identity, and includes the minimum writer-protocol fence. If a legacy/direct writer cannot be excluded, mutation is disabled.
Release PR 2 second. Keep the v1 active-sessions route for one release and mark it deprecated in response metadata.
Release PR 3 before enabling supervisor migration. New ownership proof must exist before any artifact tries to adopt or stop legacy processes.
Release PR 4 behind a migration feature flag. Enable per platform after its real-OS smoke lane passes. Keep manual service mode as the safe fallback.
Rollback behavior: schema changes are additive; old account/session reads continue. Managed service/manual entry points enforce the minimum writer epoch, so disabling transaction v2 fails closed and managed pre-v2 downgrade requires explicit migration. Direct old same-user binaries are outside that prevention boundary; detected writes make state indeterminate or disable mutation. If supervisor v2 is disabled, the owned new service can run manual/quarantined without deleting native artifacts. The previous versioned launcher is retained until candidate handoff proves healthy.
Live maintainer repair: only after the release is installed, explicitly inspect and repair the current Keychain/service state. That operation is not part of implementation testing.
Resolved decisions
Remote credential mutation is read-only until real remote authentication and TLS are designed.
Full runtime package slots and encrypted migration of existing DB token columns are separate projects.
Security.framework is the macOS backend; the shell security -X secret-in-argv path is removed.
HMAC recovery evidence covers the complete transaction transcript and is machine-local/internal.
Uncooperative shared stores never receive an atomic/live-switch claim.
The API service is one per OS user; tray presentation may be one per graphical login session.
Forced unmanaged termination is unavailable on macOS; ambiguity always fails closed.
Plan reviewed through three independent verification/adversarial rounds and a clean recursive design review. Generated from the jacked HTML artifact template.