Device-Flow Setup#
covibe-local setup can provision per-agent MCP tokens without the manual
create-token-then-export ritual. When no token is available it runs an
RFC-8628-style device flow: the CLI requests a grant, the developer approves it
once in the Co-Vibe console, and the server mints one typed token per requested
agent (claude-code, codex, cursor). Typed tokens let the server attribute
sessions and usage to the exact agent instead of trusting the self-reported
agent_type at session start.
Flow#
covibe-local setupfinds no--token, no usableCOVIBE_MCP_TOKEN, and at least one requested agent without a stored credential token for the base URL, so it POSTs{machine_name, agents[]}(the missing agents only) to/api/setup/device.- The server creates a
setup_device_grantsrow and returns a short-lived user code (8 characters, shown asXXXX-XXXX), a one-time device code, averification_uri,expires_in(600 seconds), and a pollinterval. - The CLI prints
Open <verification_uri> and confirm code XXXX-XXXX, best-effort opens the browser (openon macOS,xdg-openon Linux,starton Windows — never a hard failure), and polls/api/setup/device/pollwith the device code at the server interval, backing off on 429 via theretry-afterheader. - The developer signs in to the console at
/setup/authorize?code=XXXX-XXXX, reviews the machine name and requested agents, and approves or denies. The approval binds the developer, tenant, and workspace from the signed-in session. A signed-out visit round-trips through sign-in with a sanitizedreturnTo, so the code survives the hosted (WorkOS) sign-in redirect. - The next poll mints one typed token per requested agent, marks the grant
consumed, and returns the tokens exactly once as
{"tokens": {"claude-code": "cv_...", ...}}. A replayed poll gets{"status": "consumed"}and no tokens. - The CLI writes the tokens into the credentials file (merging with existing
agent tokens) and continues with the normal config writes. When stored
credentials already cover every requested agent, setup is fully zero-touch;
--dry-runalways skips the flow.
--agents claude-code,codex overrides agent auto-detection (Claude Code is
always included by default; Codex and Cursor are added when their home
directories are detected). The list scopes both token minting and the Codex
and Cursor config writes: an agent left out of --agents gets no config even
when its home directory is detected, and a requested agent whose home
directory is missing still gets its token but its config write is skipped
with an INFO line (Codex and Cursor cannot run without their home
directories). The repo-local Claude Code config is always written.
Endpoints#
POST /api/setup/device(unauthenticated, rate-limited): create a grant.POST /api/setup/device/poll(unauthenticated, rate-limited): poll by device code; terminal states areapproved(tokens, once),denied,expired, andconsumed.GET/POST /api/setup/device/grant(cookie-authenticated): look up a grant by user code for the approval page and approve or deny it.
Credentials File#
Tokens land in ~/.covibe/credentials.json (override with
COVIBE_CREDENTIALS_PATH; tests always set it to a temp path). The file is
written with mode 0600 and its directory with 0700:
{
"version": 1,
"servers": {
"http://localhost:3000": {
"tokens": { "claude-code": "cv_...", "codex": "cv_..." }
}
}
}The file is per-user and keyed by lowercase origin, so running setup in a second repo against the same server is zero-touch. Long-lived processes (the machine-level watch service) resolve the token from this file at runtime on every tick — including after an auth failure — so token rotation never requires a service reinstall and the service unit embeds no token.
COVIBE_AGENT Markers And Token Resolution#
Setup writes a non-secret COVIBE_AGENT marker into each agent config:
claude-code in .mcp.json, codex in the ~/.codex/config.toml env, and
cursor in .cursor/mcp.json. Configs stay secret-free — the raw-token
validators still fail on any literal cv_ value.
Everywhere a token is needed (the stdio MCP bridge, the Claude Code and Cursor hooks, companion snapshot/telemetry/watch), resolution order is:
- an explicit
--tokenflag (companion CLI only) - the
COVIBE_MCP_TOKENenvironment variable (legacy shared-token setups keep working unchanged) - the credentials file, looked up by
COVIBE_BASE_URLplusCOVIBE_AGENT
Empty strings and unexpanded ${...} placeholders — what agent runtimes pass
when the shell variable is unset — count as absent, so the credentials
fallback actually fires.
Hook commands carry no env of their own: when COVIBE_BASE_URL is unset, the
Claude Code and Cursor hooks read the Co-Vibe server env block in the
project's .mcp.json / .cursor/mcp.json to discover the origin before
defaulting to http://localhost:3000, so hosted (and non-:3000 local) setups
resolve credentials and post telemetry against the right server.
Doctor#
covibe-local doctor adds a credentials check. The expected agent list is
derived from what setup actually wired — claude-code when .mcp.json
carries the COVIBE_AGENT marker, codex when ~/.codex/config.toml has the
Co-Vibe block, cursor when .cursor/mcp.json has the Co-Vibe server —
falling back to auto-detection only before any config exists, so a deliberate
setup --agents claude-code never fails on a detected-but-unwired agent. The
check passes when the file exists with 0600 permissions and a token per
expected agent, fails on loose permissions or a missing agent token
(downgraded to info while the legacy env token still covers every agent), and
is info when no credentials exist yet. The
token check warns on legacy shared COVIBE_MCP_TOKEN setups and suggests
rerunning setup. A config written before this feature lacks the
COVIBE_AGENT marker; the MCP config check downgrades that single defect
to a warn — such setups still work via the legacy env token — and rerunning
covibe-local setup migrates them in place. Any other config defect (stale
base URL, raw token, wrong cwd) stays a hard fail.
Updates#
The machine service keeps itself current. Each watch tick the companion reports
its installed version on the session heartbeat; when the server advertises a
newer hosted tarball (see mcp-contract.md), a --service run silently
self-updates between ticks. Auto-update is intentionally narrow:
- Only the service updates. Service mode is detected by the
--serviceflag alone — foregroundwatch, pipes, CI, and test subprocesses never self-update. Setup adds--serviceto the units it installs. - Only npm-managed installs update. The package must be
co-vibedirectly under anode_modules/directory with a matchingpackage.jsonand no.git. Git checkouts get agit pullhint instead; npx caches are refused. - Disable it with the environment variable
COVIBE_AUTO_UPDATE=off, or the marker file~/.covibe/auto-update-off.covibe-local setup --no-auto-updatewrites that marker at setup time;covibe-local update --off/--ontoggle it afterward.
covibe-local update is the manual path: a one-off session-less heartbeat
fetches the version handshake, then the updater runs immediately (it ignores
the once-per-hour throttle and the service-mode gate, but still enforces the
sha256 and npm-root gates). On a git checkout it prints the git pull hint and
exits cleanly.
The update mechanism downloads the tarball from the server's /downloads,
verifies its sha256 against the handshake, and npm installs the .tgz into
the install prefix with --no-save --no-package-lock --ignore-scripts (local
installs) or npm install -g (global installs) so the host project's
package.json and lockfile stay byte-identical and host lifecycle scripts
never run. Because the sha256 gate already proved the bytes are exactly what the
server intended, a non-zero npm install exit is treated as environmental
(EACCES/ENOSPC, a held npm lock, an engine mismatch) — the attempt is recorded
for WARN spacing but the version is not blacklisted, so a healed machine
retries it automatically. The still-running old code then boot-checks the new
install (covibe-local --version); on failure it auto-reverts to the previous
tarball, records the failed version so it is not retried, and keeps running the
old code. Genuine version faults (a version mismatch or a failed boot-check) are
the only failures that blacklist a release. On success it logs INFO update: updated <old> → <new> and exits 0 so
launchd/systemd respawn the new code on the next supervised restart.
Rollout: machines whose service was installed before this release re-run
covibe-local setup once to gain --service. The write-only unit-drift
self-repair also rewrites a stale unit file at the next login or boot, so old
units pick up --service with no manual step at the next restart.
Automatic Repo Registration#
There is no per-repo registration command. Agent activity — the Claude Code
and Cursor hooks, the stdio MCP bridge, and the companion's own sync commands
— registers the enclosing git repo root in ~/.covibe/repos.json (override
with COVIBE_REGISTRY_PATH). Entries are keyed per server base URL, so a repo
used against two Co-Vibe servers gets one entry per server, and the machine
watcher only syncs repos registered for its own server. Registration is local
metadata only; nothing syncs at registration time, and the home directory
itself is never registered.
Setup resolves the new-repos policy up-front: on a TTY it asks once —
Sync git activity from new repos on this machine to <server>? [Y/n] — and
the --log-new-repos / --no-log-new-repos flags skip the prompt for
automation (non-TTY and --dry-run leave the stored policy untouched). Under
the opt-in policy, first activity registers a repo as pending: the agent
gets the one-line hint "new repo registered but not logged — run
covibe-local include inside it to opt in" (once per repo), the repo shows a
pending pill in Settings → Connected repos, and no data leaves the machine
until it is included.
Excluding Personal Repos#
Every repo a developer works in is logged to Co-Vibe unless excluded — setup
prints exactly this disclosure after wiring configs, and doctor repeats it as
the exclusions check. To keep a personal repo private:
npm exec -- covibe-local exclude # exclude the current repo
npm exec -- covibe-local exclude /path/to/repo
npm exec -- covibe-local exclude --list # show excluded repos and the policy
npm exec -- covibe-local exclude --remove # log the current repo againTo flip the machine to opt-in instead — new repos are NOT logged until you opt
them in — use the --new-repos policy:
npm exec -- covibe-local exclude --new-repos # stop logging new repos
npm exec -- covibe-local include # opt the current repo in
npm exec -- covibe-local exclude --new-repos --remove # log new repos againExclusions are machine-level, stored canonicalized in
~/.covibe/exclusions.json (override with COVIBE_EXCLUSIONS_PATH), and cover
the repo plus its subdirectories. Exclusion is sticky: covibe-local exclude
also deletes the repo's registry entries (every server), and no session
activity can re-register an explicitly excluded repo — only
covibe-local include inside it lifts the exclusion, re-registers it for the
CLI's server, and lets the machine watcher resume on its next tick. For an
excluded repo, the Claude Code and
Cursor hooks exit before any tool call or state write, the stdio MCP bridge
returns a terminal error without contacting the API, covibe-local snapshot,
covibe-local telemetry, and the trailing setup snapshot send nothing, and
the machine-level covibe-local watch filters the repo out of its per-tick
sync set (snapshots, telemetry inbox flushes, heartbeats) instead of exiting;
only the single-repo debug form, watch --repo <path> pointed at an excluded
repo, warns and exits. The Codex and Cursor usage sweeps are scoped to the
repo of the watch process's working directory (the machine service runs from
$HOME, so its sweeps carry no repo attribution), so an excluded repo's
threads never sync from another repo's watcher either. The server never sees
the repo.
The repo an agent works in is resolved deterministically from the OS/runtime,
never from agent self-report: repo-local configs make the bridge's
process.cwd() correct for Claude Code and Cursor, and the global Codex
config forwards the shell-set PWD via env_vars so the bridge checks the
repo Codex was actually launched in (see codex-integration.md).
Security Notes#
- The server stores only the SHA-256 hash of the device code; the raw code is returned once at grant creation and never logged.
- Raw
cv_tokens are delivered exactly once by the poll endpoint, never appear in work-event payloads or CLI logs, and are never written into agent configs. - Grants expire after 600 seconds; approved-but-unconsumed grants expire one TTL after approval, and pending grants cannot be approved past expiry.
- Both unauthenticated endpoints and the user-code lookup are rate limited.
Verification#
npx vitest run tests/unit/credentials-file.test.ts tests/unit/device-flow.test.ts \
tests/unit/setup-device-command.test.ts tests/unit/doctor-credentials.test.ts