Problem
The API has a reference with a working playground and two credentials, tokens and keys
(RFC 0016). What it does not have is a tool on the developer's own machine. Every
platform developers live in ships one: gh, stripe, fly, gcloud, supabase.
They sign you in once through the browser, remember who you are, let you switch
between work and personal accounts, and do the repetitive parts for you.
Ours has a specific first job. The API a caller may reach is not the same for every caller: the OpenAPI document the gateway serves depends on who asks (RFC 0020). An SDK generated from the public document offers operations a given account cannot call, and misses the ones its plan adds. The command line is where a developer signs in as the account they build for, and where the SDK for that account is generated.
The code is public, under the SDK repository's Apache 2.0 licence, and it is Rust. It
will be read by people who know Rust well. It has to hold up to that reading: small
dependency set, no unwrap on a user's input, errors that say what to do, generics
where they remove a class of bug and nowhere else.
Proposal
What it is
A single binary, iohr, for Linux (x86-64, arm64), macOS (Apple silicon, Intel) and
Windows (x86-64, arm64), all three in the first release. It is installed from our own
APT repository, our Homebrew tap, winget, or a one-line installer for each platform
(RFC 0021). The name is the platform's short name, already used on the wire (the
iohr-api audience, the iohr.<service>.v1 packages), short to type, and free on
every registry it ships to (checked 2026-10-02: crates.io, Homebrew, Debian, Ubuntu,
winget, Scoop, npm and PyPI). It talks only to the public API and the public sign-in
pages, the same two hosts the SDKs talk to, and sends no telemetry.
Commands, first release
iohr login sign in (browser, or a device code); add a profile
iohr login --with-token read an API token from stdin (CI, scripts)
iohr logout [--profile P] forget a profile and revoke what it holds
iohr profile list|use|show the profiles on this machine; the default one
iohr whoami who the active profile is, its account, plan, scopes
iohr accounts list the accounts the signed-in person belongs to
iohr token create|list|revoke API tokens for an account (RFC 0016)
iohr api <METHOD> <PATH> one authenticated call, JSON out (like `gh api`)
iohr openapi pull the OpenAPI document this profile sees, to a file
iohr sdk generate an SDK cut to one or more profiles (RFC 0020)
iohr sdk check fail when the generated SDK no longer matches
iohr completion <shell> shell completions
Every command takes --profile, reads IOHR_PROFILE, and prints JSON with
--json. Exit codes are documented and stable: 0 success, 1 a failed call, 2 a usage
error, 3 not signed in, 4 forbidden by scope or plan.
Signing in
iohr login offers both standard grants for a command line, and both are first class:
- In a browser on : the authorization code grant with PKCE on a loopback redirect (RFC 8252). The command opens the system browser, the developer signs in or is already signed in, and the terminal receives the tokens on a local port it listens on for that one request.
- With a device code: the OAuth 2.0 device authorization grant (RFC 8628). The terminal prints a short code and a link, the developer approves in any browser on any device, and the terminal receives the tokens. It works over SSH, in a container and on a headless server, where a loopback redirect cannot.
The command picks for the developer: the browser when it can open one on this machine,
the device code otherwise (an SSH session, no display on Linux, a container, a CI
runner). --web and --device force either. Our identity provider supports both. The
device grant needs one new page in our sign-in app, where the code is typed and
approved. It runs on the identity provider's current open-source release. A later
release makes a device code strictly single use even when two requests redeem it at
the same moment
( changelog);
it is not yet published as open source, and we upgrade when it is.
A diagram is drawn here in the RFCs product; this page does not show diagrams yet.
--with-token takes an API token instead, for CI and for people who prefer to create
one in the console. A key (client id and secret) is never stored by the command line:
servers use keys through the SDK, not through a developer's keychain.
Profiles and several accounts
A profile is a name for one way of calling the API: who signs in, and which account the calls count against. A person can belong to their personal account and to several teams, and can hold API tokens for any of them, holds several profiles:
# the config file in the platform's config directory
# (XDG on Linux, Application Support on macOS, %APPDATA% on Windows)
default = "personal"
[profiles.personal]
kind = "person" # signed in with `iohr login`
account = "acc_…"
[profiles.acme-ci]
kind = "token" # an API token for the Acme team
account = "acc_…"
The file holds names, account ids and settings, never a secret. Secrets go to the
operating system's credential store (macOS Keychain, Windows Credential Manager, the
GNOME Keyring or KWallet on Linux) through the
keyring crate, one entry per
profile keyed by profile and account. gh learned the hard way that an entry keyed by
service alone returns an arbitrary token once two accounts share a host
(cli/cli#12885). A machine without a
credential store (a CI runner) uses IOHR_TOKEN and nothing is written to disk.
The active profile is chosen per invocation (--profile, then IOHR_PROFILE, then
default). There is a profile use to change the default, but nothing depends on a
global switch: two terminals building for two accounts at the same time must not race
each other.
A diagram is drawn here in the RFCs product; this page does not show diagrams yet.
How the code is laid out
A Cargo workspace in the SDK repository, beside the SDKs, so the command line uses the Rust SDK for its own calls and every conformance case the SDKs pass covers it too:
| Crate | Kind | Holds |
|---|---|---|
iohr |
binary | argument parsing (clap derive), output, exit codes; no logic of its own; cargo install iohr |
iohr-auth |
library | the grants, the credential store, profiles |
iohr-openapi |
library | fetching a document, normalising it, the generator's model of it |
iohr-codegen |
library | one target per language, rendering that model to files |
inorbithr |
library | the Rust SDK (already reserved on crates.io, matching the @inorbithr npm scope) |
The libraries return typed errors (thiserror); only the binary turns them into a
message and an exit code. This is the split the Rust CLI
book and the
API guidelines describe, and the one
cargo, rustup and uv follow: thin binary crate, tested libraries.
Generics where they remove a bug:
- A credential is a trait.
trait Credential { async fn bearer(&self) -> Result<Bearer, AuthError>; }with three implementations (a person's refreshing session, a pasted token, a key's exchange). Every command is written once againstimpl Credential; a test swaps in a fixed one. - A sign-in is a typestate. A device authorization is
Pendinguntil approved andGrantedafter; onlyGrantedcan be turned into a stored profile, so a half finished sign-in cannot be saved. - A target is a trait.
trait Target { type Options: clap::Args; fn render(&self, api: &Api, opts: &Self::Options) -> Result<Files, RenderError>; }; adding a language is one implementation, with its own flags, and nothing else changes. - A secret is a type.
Redacted<T>prints<redacted>, is zeroed on drop (zeroize) and is neverSerialize; the SDK's rule that secrets never print holds in the command line by construction.
What it does not do: no async-trait macro (native async traits since Rust 1.75), no
trait objects where a generic is static, no builder macros, no global state, no
unsafe. Edition 2024. MSRV is the SDK's (Rust 1.94), declared with rust-version
and tested in CI; the MSRV-aware resolver
keeps a dependency from raising it silently. Clippy pedantic with warnings denied,
cargo-deny for licences and advisories, cargo-semver-checks on the libraries. The
dependency list is short and every entry has a line in the README saying why: clap,
tokio (current-thread), reqwest with rustls, serde, toml, thiserror, keyring,
zeroize, minijinja (templates), and typify (Rust types from JSON Schema).
Tests
Unit tests in each library; the SDK repository's conformance cases (token refresh,
retries, error mapping) run against the command line's client too; the device grant
and the token commands run against a local copy of the identity provider in CI; sdk generate has snapshot tests per target and a compile test that builds what it
generated. CI runs every test on Linux, macOS and Windows, including the credential
store on each. A release is never cut from a red main.
Alternatives considered
inorbitas the name. Another company uses the InOrbit name for robotics software and owns the@inorbitnpm scope (the SDK's ADR 0006 already moved npm to@inorbithr).iohravoids the clash and matches the names already on the wire.- Go, like gh, flyctl and the stripe CLI. A fine choice, and the platform already has Go in its SDK set. Rust wins here because the generator shares its model and its Rust target with the Rust SDK, the platform itself is Rust, and the user asked for it.
- One grant only. A loopback redirect alone fails over SSH and in containers; a device code alone makes the common case, a laptop with a browser, a step longer. Offering both, chosen automatically, costs one extra code path.
- Tokens in a file, like
ghonce did. The keychain is the right home on a ; a file stays an option only behind an explicit flag, mode 0600, and is what CI never needs because it uses an environment variable. - Generating the SDK in the browser (console download). Fine as a second door later.
The command line comes first because the SDK is regenerated in the developer's
repository and checked in CI (
sdk check), which a download cannot do. - Putting the command line in the platform repository. It is public code and belongs with the SDKs, their licence, their conformance cases and their release process.
Decision
Decided 2026-10-02:
- The binary is
iohr; the crates areiohr,iohr-auth,iohr-openapiandiohr-codegen, the Homebrew formula and the Debian packageiohr, the environment variablesIOHR_TOKENandIOHR_PROFILE. Every one of these names was free on 2026-10-02. The Rust SDK keeps the crate nameinorbithr. - Both sign-in grants ship in the first release: the browser when one can be opened
here, the device code otherwise,
--weband--deviceto force either. - Windows is in the first release, beside Linux and macOS.
Publication
When it ships: the docs gain a "Command line" guide and the quickstart offers brew install / apt install next to "Create a test token"; the console's tokens page shows
iohr login --with-token beside a new token; this RFC's status log records the
release.
Status log
- 2026-10-02: proposed with RFC 0020 (SDK generation) and RFC 0021 (packages).
- 2026-10-02: decided with the user's answers: the name
iohr, both sign-in grants chosen automatically, Windows in the first release. - 2026-10-02: the device grant ships on the identity provider's current open-source release instead of waiting for the next one. The difference is narrow: a code redeemed twice in sequence is already refused; only two redemptions at the same instant could both succeed, and that needs the device's own secret code, which never leaves the terminal that started the sign-in (the person only types the short code). We upgrade when the open-source release with strictly single-use codes is published. The approval page is never skipped, warns against codes sent by someone else, and code entry is rate limited.
- 2026-10-02: phase one built, in the open-source SDK repository, not yet packaged.
iohrsigns a person in, in a browser on or with a device code, keeps several accounts as profiles with secrets in the operating system's credential store, refreshes and revokes its session, manages API tokens, and calls the API. Checked against the platform: both sign-ins, a token made, used, refused outside its scopes and revoked within two seconds, and a session revoked on sign-out. Packages follow RFC 0021. - 2026-10-02: first pre-release, 0.1.0-alpha.2, installable with Homebrew, APT and the
installers (RFC 0021); the docs gain a Command line guide and the console shows
iohr login --with-tokenbeside a new token. - 2026-10-07: Checked:
iohrships from inorbithr/sdk; nothing since the last line changes the decision. Status unchanged.