Decision record
ADR-0011: The CLI signs in through the browser
ADR-0011: The CLI signs in through the browser and mints at exchange
Status: accepted · 2026-09-12
Context
Connecting an agent to a workspace was a settings page, a paste into a
config file whose shape differs per agent, and a token that reaches the
wrong workspace without anyone noticing — the endpoint is one URL for every
workspace, and the token decides. One session lost most of an hour to
exactly that, and nearly committed a token inside .mcp.json.
npx bladbase connect exists to make those mistakes impossible rather than
documented. It needs a credential, and it starts with none. Three ways to
get one were weighed.
- Ask for the token in the terminal. A person mints in settings and
pastes. This is the escape hatch (
--token), not the path: it keeps every step the command exists to remove. - Ask for a password in the terminal. Never. Firebase Auth is the identity provider; the CLI is not a place a password should be typed.
- A browser round trip with a loopback listener — what
gh, Vercel and Netlify do. The person signs in where they already sign in; the terminal gets a credential without seeing a password.
Within the third, what crosses the redirect matters. Sending the token itself puts a bearer credential in a browser URL, history and any proxy log. Sending a code that the terminal exchanges keeps the token off every surface but the one HTTPS response that delivers it.
Decision
The CLI opens /cli/authorize in the browser; the page issues a
one-time code; the CLI exchanges the code, and the token is minted at that
moment.
- The CLI listens on a random loopback port with a random
stateand openshttps://app.bladbase.com/cli/authorize?port&state&name&workspace. The page requires a session, lists the workspaces where the person holdstoken.own, and asks for a name, scopes and a lifetime — the same fields the settings page mints with. - Approving writes a
cliAuthorizationsrecord — sha256 of the code, user, workspace, name, scopes, lifetime, state, five-minute expiry — and redirects the browser tohttp://127.0.0.1:{port}/callback?state&code. The listener refuses a mismatchedstate. Denying redirects witherror=deniedand no code. POST /api/cli/exchange {code}claims the record in a transaction (once: a second exchange of the same code mints nothing, and an unknown, expired and used code all read the same) and mints throughcreateToken— the settings page’s own use case, with its membership check run again at that instant. No token plaintext is ever stored server-side.- The page refuses to render without a valid port and state: with no listener there is nowhere to send a code, so there is no form.
- Every agent config the CLI writes references an environment variable,
BLADBASE_TOKEN_<WORKSPACE>, never the token. The token lives in one file —~/Library/Application Support/bladbase/credentials.json,$XDG_CONFIG_HOME/bladbase/,%APPDATA%\bladbase\— mode 0600. - The command ends by calling the endpoint with the new token and printing the workspace name that answered. A wrong-workspace token is caught in the terminal before any agent starts.
Consequences
- One new authentication surface: a code exchangeable for a token, minted
for a browser session by a process the browser redirected to. Its
properties are the ones above — random, short, single-use, bound to a
statethe listener chose — and they are what make it safe to hold no other credential. - Config files the CLI writes are safe to commit, so it adds nothing to
.gitignore. Codex’s config is user-level, and the command says so. - The CLI has no runtime dependency: 27 KB, Node 20, built-ins only,
because
npxdownloads it every time. - The conventions snippet from
get_workspace_conventionsnow names the endpoint the request arrived at (forwarded headers on Cloud Run, Host locally) instead of a configured value that readlocalhostin production — the bug found on 2026-09-10. - Not done, on purpose: the macOS Keychain as a store (a later option, not the first one), service-member tokens (the settings page’s job, ADR-0005), agents beyond the four named (one file each behind one interface), and tokens in agent config files even when gitignored.