Live Bidirectional Auth Sync
Status: Open — design proposal
Problem
Section titled “Problem”jackin’s sync auth mode (today) reads the host login once per
container launch, copies the token into the role-state directory,
and bind-mounts it. That covers the first-launch convenience case
but not three operationally important scenarios:
- Host token rotates while a container is already running.
gh auth login/claude /login/codex loginon the host updates the host’s token store, but every running jackin container keeps using the snapshot it got at launch time. The operator has tojackin eject <role> && jackin load <role>for each container — which means tearing down the agent’s session. - In-container login does not flow back to the host. When an
agent inside the container runs
gh auth refresh,claude /login, orcodex login, the new token persists in that container’s writable layer (and survives via the role-state bind-mount) but is invisible to the host and to every other running container. - Parallel containers running the same agent. Two containers running the same agent today each have their own copy of the token. If one of them refreshes (OAuth refresh-token rotation), the other’s token can be invalidated server-side without warning — surfacing as a sudden 401 mid-session.
This roadmap item covers the long-term answer: a single source of truth for each auth axis, kept in sync across the host and every running jackin container in real time, by an opt-in jackin daemon.
The item is deliberately scoped beyond a single auth axis. It must cover, with one architecture:
GitHub CLI(gh)Claude Code(file + macOS Keychain)Codex(~/.codex/auth.json)Amp(forthcoming)- any future authentication axis jackin’ adds (the credential source pattern evolution should plug in here too)
Naming reconsideration
Section titled “Naming reconsideration”The current mode name sync describes “copy the host’s current
state into the container at launch time.” That is not what
“sync” means in any other context — sync conventionally implies
continuous, bidirectional reconciliation. Today’s behaviour is
closer to snapshot, forward, or mirror-on-launch.
Options for resolving the naming clash before this feature ships:
| Option | Today’s mode renamed to | New live mode named |
|---|---|---|
| A | forward | sync (reclaim the word for the live axis) |
| B | snapshot | sync |
| C | mirror | live-sync |
| D | (keep sync) | sync-live (but sync keeps misleading name) |
Option A or B is preferable: they make the existing mode’s actual
behaviour visible from the name and reserve the unmodified sync
for what operators expect. Pre-release breaking-change policy
(see AGENTS.md → “Project status: pre-release”)
makes a rename across [github] / [claude] / [codex]
configurations cheap. The implementation PR for this roadmap item
should rename in lockstep with the daemon’s first ship; not
beforehand (the rename without the live behaviour would be churn
without payoff).
A decision belongs to the design pass that picks the daemon shape; this item flags the question rather than answering it.
What “live sync” must do
Section titled “What “live sync” must do”Per auth axis, when the live mode is active:
- Host change → all running containers, near real time. Within
a few seconds of a host-side
gh auth login/claude /login/codex login, every running jackin container that opted in sees the new token. No restart required. - In-container change → host AND other running containers. When an agent inside a container refreshes the token, that new token reaches the host’s authoritative store and any sibling running containers within seconds.
- Single refresh-token at a time, OAuth-safe. When an OAuth
refresh-token rotation happens, only one party (the agent that
triggered the refresh) holds the live grant; everyone else must
read the new token before issuing the next API call. The
architecture must avoid the parallel-agent invalidation cascade
that bit operators on the launch-time
syncbehaviour today. - Transparent storage abstraction. macOS host stores
gh/claudetokens in the system Keychain; Linux stores them in plaintext under~/.config/. The daemon must bridge those formats — read viagh auth token/security find-generic-password, write viasecurity add-generic-password -Uon macOS or atomic file write on Linux — without leaking keychain-only tokens as plaintext on disk. - Operator-visible. The launch summary already names the auth
source for each axis. The daemon must surface its state too —
a status line or
jackin auth statussubcommand (proposed — does not exist today) showing which containers are subscribed, when each axis last reconciled, any pending conflicts. The Jackin Desktop Agent Hub consumes the same daemon state for Claude/Codex/Amp account cards instead of scraping provider files directly from the macOS app. - Revocable and contained. Same opt-in / opt-out semantics
the existing modes have. Operators must be able to switch a
workspace or role to non-live
forward/ignoreand have the live channel torn down for that container.
Architecture sketch
Section titled “Architecture sketch”┌────────────── host ──────────────┐ ┌─── running container ────┐│ host token store │ │ agent-side store ││ gh: Keychain or hosts.yml │ │ gh: hosts.yml ││ claude: Keychain or .creds │ │ claude: .creds ││ codex: auth.json │ │ codex: auth.json ││ ↕ │ │ ↕ ││ live-auth-sync adapter │ ←──→ │ jackin-auth-watcher ││ (hosted by the jackin daemon) │ │ (in-container watcher) ││ - polls gh auth token │ │ - inotify per axis ││ - inotify per source file │ │ - small static binary ││ - polling, with platform │ │ baked into construct ││ change-event hooks where │ │ ││ available │ │ ││ - reconciles shared store │ │ │└──────────────────────────────────┘ └──────────────────────────┘ ↕ ↕ └─── shared store, flock-protected ─────┘ ~/.jackin/auth-shared/<axis>/<file> - atomic tmp+rename writes - mtime + checksum for conflict detection - bind-mounted into containersComponents to build
Section titled “Components to build”- Shared store at
~/.jackin/auth-shared/<axis>/. Single on-disk source of truth per axis. Atomic tmp+rename writes; flock to serialize concurrent writers. - live-auth-sync adapter — hosted by the
jackin daemon. Not a
separate process. The daemon owns the long-running lifecycle;
this item adds one adapter against the daemon’s plug-in
surface that watches host sources (poll + inotify on Linux;
polling, with macOS Keychain change-event hooks where a stable
public API exists —
SecKeychainAddCallbackis deprecated, so the macOS path is polling-only until a successor surfaces). Reconciles host → shared store, then shared store → host. Surfaces status to the CLI / TUI through the daemon’s control socket. - Container-side watcher — small static binary baked into the construct image. Inotify on the agent-store paths inside the container. On change, atomically push to the shared store bind-mount.
- Mount strategy update. Today’s
syncbind-mounts a per-containerhosts.yml. Live mode bind-mounts the shared store path directly so all parties see the same file. The per-container path stays forforward/snapshot/ignoremodes that don’t opt into live. - macOS Keychain bridge. Adapter writes tokens back to
Keychain via
security add-generic-password -Uso the host user’s normalgh/claudeworkflow stays consistent. Never leak keychain-only tokens to plaintext files except inside the shared store + container. - Per-axis adapter trait. Each auth axis has its own source-
store and on-disk shape. The daemon’s plug-in surface defines
one trait per axis so adding
Amplater is one adapter, not a rewrite.
Conflict resolution
Section titled “Conflict resolution”- Last-writer-wins by
(mtime, checksum). - Agent-induced rotation always wins over a stale host poll. When the adapter’s poll cycle and an in-container refresh land in the same window, the in-container value is the source of truth (it’s the OAuth-grant that the server now considers current).
- Detected conflict surfaces in
jackin auth status(proposed subcommand — does not exist today) so the operator can see drift instead of having one side silently overwrite the other.
Why this depends on the daemon
Section titled “Why this depends on the daemon”This is one of the first features that genuinely needs a long-running host-side jackin process. The shape of that process — lifecycle, install method, control socket, security posture, log redaction — is not decided here. It belongs to the umbrella jackin daemon roadmap item, which exists so each reactive feature plugs into one common daemon rather than each inventing its own.
This live-sync item depends on jackin daemon Phase 1 (empty daemon — lifecycle and control socket) shipping first. Live sync then adds a per-axis adapter against the daemon’s plug-in surface: one adapter for gh, one for Claude, one for Codex, future ones for Amp and any new auth axis. The adapter trait shape is described under the daemon roadmap item. Desktop should treat these adapters as its account-status source whenever they exist; direct CLI/file checks are fallback diagnostics only.
Host-side effects
Section titled “Host-side effects”Per AGENTS.md “Never mutate the host machine
silently”: the live-sync adapter writes to host-side state. Each
write is opt-in via the workspace’s chosen mode (live /
sync / forward / ignore) and surfaces in the daemon’s
status output and the launch summary.
- macOS Keychain writes. When an in-container refresh
produces a new token, the adapter writes it back to the host
Keychain via
security add-generic-password -Uso the host user’s normalgh/claudeworkflow keeps working. The operator authorizes the write by enabling live mode for the axis; the daemon’s status output names every host write as it happens. - Plaintext shared-store writes. The adapter writes the
current axis token to
~/.jackin/auth-shared/<axis>/<file>(atomic tmp+rename, flock-protected). Same opt-in contract: live mode is the only mode that produces these files; other modes leave the path empty. - No host repository / dotfile mutations. The adapter never
touches
~/.gitconfig,~/.ssh/,~/.config/gh/hosts.ymloutside the operator’s ownghwrites, or any user repo’s.git/config. Read-only access to host source files (e.g.gh auth token) is fine; writes target only the Keychain entry the operator’s host already owns and the jackin-private shared store. - Opt-out path. Switching the workspace or role to a non- live mode tears down the adapter for that axis and leaves host state at whatever the most-recent live-mode reconcile produced; no further writes occur.
Out of scope for this item
Section titled “Out of scope for this item”- SSH-key forwarding stays excluded. Authentication documents why.
- Cross-host sync (different operator machines sharing one store). The daemon is per-operator-user, per-host.
- Distributed conflict resolution beyond last-writer-wins.
How this changes the per-axis pages
Section titled “How this changes the per-axis pages”When this item ships, each per-axis auth doc and the auth overview must be updated together so operators understand the new mode’s semantics and the rename. The implementing PR series should land docs updates in lockstep with the daemon.
Related work
Section titled “Related work”- jackin daemon — umbrella item that introduces the long-running jackin process this feature depends on. Lifecycle, install, control socket, security posture, log redaction are decided there.
- GitHub CLI authentication strategy
— current
syncmode lives here; “Bidirectional shared-store sync” was previously listed as a follow-up under that item. This dedicated roadmap item replaces that follow-up. - Reliable Claude authentication strategy — Claude axis design notes; live sync becomes the long-term answer to the concurrent-session token-drift concerns.
- Credential source pattern — future unified credential resolver. The daemon’s per-axis adapter trait should be expressible inside the credential source pattern.
- Jackin Desktop Agent Hub — native account/status surface that reads live auth and quota state from the daemon first.