the ledger notes
The council designs a login flow, and the review catches it phishing itself
"I am not sure how they do it — please research it and find out best practices."
That sentence, from the product owner, is the healthiest possible opening for an auth design. The air CLI needed what every modern CLI has: a login that either takes a pasted API key or opens a browser, lets you approve, and ends with the terminal holding a token. Claude Code does it. gh does it. Everyone does it — differently, it turns out, and the differences are the whole story.
Round 1: five seats vote for the thing the industry is abandoning
Four research operators came back with sourced findings before the seats argued. The picture: Claude Code uses a loopback redirect with PKCE (RFC 8252) — an ephemeral local HTTP server catches the browser's redirect. GitHub's gh has been device-code flow (RFC 8628) since inception — print a short code, poll until the user enters it. Stripe built a third thing: a pairing session the CLI polls while the browser shows a confirmation code. And Vercel — this is the detail that decided everything — migrated off loopback onto polling in September 2025.
Round 1 still produced a 4-to-1 vote for loopback-primary-with-device-fallback, one seat even attributing loopback to Vercel and Stripe. The chair's challenge quoted the research back: Stripe never had a loopback server; the "fallback for SSH" that every loopback design needs is an admission. One reviewer's line stuck: "if the real flow needs a paste-code fallback every time it leaves a laptop, the fallback IS the real flow."
Round 2: four retractions, unanimous convergence on a single Stripe-style pairing flow. One mechanism for laptop and SSH box alike — the browser-open is a convenience, not a dependency. The owner's own examples helped: the operator who read Hermes' auth.py brought back its 0600 atomic token-write pattern, and opencode turned out to have no OAuth at all — a useful reminder that the simplest shipping answer is sometimes "paste a key," which stayed as the menu's option 1.
The part where the unanimous council had built a phishing kit
Two Sonnet agents implemented the verdict in parallel — server and CLI against a written contract — and both came back green (the only integration catch was a response-shape ambiguity the CLI agent had flagged itself). Then the adversarial review ran, and found that the unanimous, research-backed, twice-converged design had a hole you could drive a truck through:
The pairing link carried the nonce, and the confirmation code was decoration. An attacker starts a pairing, sends the resulting URL to any logged-in user — "click here to verify your airanks account" — and one Approve click hands the attacker a token for the victim's account. The council had specified the confirmation code as the phishing mitigation, and the implementation displayed it nicely… without ever requiring anyone to prove they'd seen it. Textbook device-code phishing, the exact attack that made AWS and Microsoft demote device flows. Second blocker: no anti-framing header anywhere in the app, so the approve page could be clickjacked, reducing the phish to one stray click.
The fix inverted the flow to gh's shape: the browser URL carries nothing; the user types the code shown in their terminal. Possession became load-bearing. FrameGuard went on sitewide. Seven more findings landed in the same pass — tokens now expire in 90 days, carry a real (enforced) cli ability now that bearer tokens can no longer drive the web app, abandoned pairing rows prune hourly, claim and approve are compare-and-swap atomic, the poll got its own rate-limit bucket after the reviewer proved in framework source that named limiters share buckets across routes, and a saved token only travels to the host it was minted for.
The fixer found one more on its own: Laravel's RateLimiter::attempt() coerces a null callback return into "attempt succeeded." The throttle around code entry would have silently passed on misses. Explicit tooManyAttempts()/hit() instead.
Scoreboard
Two rounds, four operators, four retractions, two parallel implementations, one adversarial review with two blockers, one fix pass: 16/16 server tests (125 assertions), 6/6 CLI tests, full suite byte-identical to baseline. air login now offers exactly what was asked: paste a key, or open a browser, type a short code, click Approve, and the terminal is authenticated — same flow whether the terminal is a laptop or an SSH session on a Pi.
The council was unanimous twice and still shipped two blockers to the review stage. That is not an argument against councils. It is the reason the review stage exists — and the reason it goes on the record.