You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today the CLI's authentication is implicitly anchored to a single environment per invocation: production by default, sandbox only when --sandbox is passed. Even though credentials.yml can store credentials for both hosts simultaneously, there is no notion of an active selection — every command that targets sandbox needs the flag re-typed, and a user with multiple accounts in the same environment cannot keep them authenticated at the same time.
We want a model where the user can be authenticated with multiple distinct credentials (e.g. production-personal, production-work, sandbox-test) and select one active context at a time. Subsequent commands should target the active context without requiring --sandbox or --account on every invocation. At the same time, agents and parallel shells must be able to bypass the active context entirely with per-invocation overrides.
Why this matters
Switching environments mid-session is high-friction: --sandbox must be re-typed for every command.
A user with access to more than one account in a single environment cannot keep both authenticated at once today; logging into a second account replaces the first.
The mental model people bring from gh, kubectl, and aws --profile is "named context, active one at a time". The CLI doesn't match.
Agents and automation running multiple invocations in parallel need to address specific stored credentials without mutating shared state.
The CLI supports two operating modes that compose cleanly: stateful (use the active context) and stateless (per-invocation overrides).
Stateful mode (interactive default)
With no overrides, every command uses active_context. The user manages contexts with auth login, auth list, auth switch, auth status, auth logout. This is the path for an interactive shell session.
Stateless mode (per-invocation overrides)
For agents, automation, and parallel shells where the active context cannot be assumed or mutated, three overrides are available, in increasing rawness:
--context <name> — picks a stored context by name for this invocation only. Does not change active_context. This is the agent-friendly handle: "give me a pointer to a stored credential".
--sandbox — host override. Tells the CLI to talk to the sandbox API for this invocation. Composes with the other overrides. When passed standalone and multiple sandbox contexts exist in storage, the CLI errors with the list of candidates and asks the user to disambiguate via --context.
--token / --account — raw credential overrides. Highest precedence. Used when the agent has credentials from outside credentials.yml (env vars, secret manager, vault).
Resolution chain
For each of (token, host, account):
1. Explicit raw flag (--token / --sandbox / --account)
2. Environment variable (DNSIMPLE_TOKEN / DNSIMPLE_ACCOUNT)
3. --context <name> (loads named context from credentials.yml)
4. Active context (credentials.yml.active_context)
5. Error: not authenticated
--sandbox and --context are independent and compose. You can use either, both, or neither.
Command surface
auth login [--sandbox] [--name <name>]
Authenticates and creates a new context. --name is optional; auto-derived if omitted.
The newly created (or refreshed) context becomes active.
The local --sandbox flag here means "this login is for the sandbox host".
auth logout [--name <name>]
Removes a context. Defaults to the active one. If the active context is removed, no context is active until the user picks one.
auth list
Lists all stored contexts, highlighting the active one. Read-only; does not require network or a valid token.
auth switch [<name-or-account-id>]
With an argument: switches the active context to the matching one (by name or by account ID; errors if ambiguous).
Without an argument: opens an interactive picker.
Switching is a local operation only — no API call. auth switch no longer touches accounts; changing the account inside a context is done by re-running auth login.
There is no separate auth use — auth switch subsumes that role.
Acceptance Criteria
credentials.yml uses the new contexts: + active_context: schema; the legacy hosts: map is migrated automatically on first load.
The pre-migration file is preserved as credentials.yml.bak so users have a manual rollback path.
auth login creates a new context, auto-deriving its name when --name is omitted (per the algorithm in Notes); when --name is provided, the name is honored or rejected per the same rules.
Re-logging in with a token that already exists in storage mutates the existing context in place rather than creating a duplicate.
auth list lists all stored contexts, highlights the active one, and works with no network and no valid token.
auth switch [<name-or-account-id>] updates active_context locally with no API call; without arguments it opens an interactive picker.
auth logout [--name] removes a context and updates the active selection if needed.
Global flag --context <name> is added and integrates with the resolution chain; passing it does not modify active_context.
--sandbox continues to work as a per-invocation host override and composes with --context, --token, and --account.
--sandbox standalone with multiple sandbox contexts stored returns an actionable error listing the candidates and pointing to --context.
--token and --account continue to behave as raw overrides at the highest precedence and bypass credentials.yml entirely when both are supplied.
The four concurrent-usage patterns in Notes all work end-to-end without interfering with each other.
README and dnsimple ai document both the stateful and stateless modes, with the same patterns shown in Notes.
Existing unit and integration tests are updated; new tests cover login naming, switching, listing, the picker, the resolution chain, the migration, and the multi-context error path for --sandbox.
The two-mode model is designed to support both interactive single-user shells and concurrent automation in the same CLI binary. The patterns below all run side by side without interfering with each other, because the only shared state (active_context) is touched exclusively by auth switch.
Pattern 1 — Interactive user, occasional one-shot to a different context
A developer working primarily in production occasionally needs to peek at sandbox without losing their place.
$ dnsimple auth status
Context personal
Account alice@example.com (981)
$ dnsimple zones list # uses active context (personal / production)
$ dnsimple --context sandbox zones list # one-shot sandbox; active context untouched
$ dnsimple zones list # back to active (still personal / production)
Pattern 2 — Multiple terminals each pinned to a different context
Two tmux panes (or two terminal windows) work in parallel, each effectively "logged in" to a different context for the duration of the session, without ever calling auth switch.
# Pane A
$ alias d-prod='dnsimple --context personal'
$ d-prod zones list
$ d-prod records list example.com
# Pane B (concurrent, independent)
$ alias d-sbx='dnsimple --context sandbox'
$ d-sbx zones list
active_context is never mutated by either pane, so neither pane's auth status is affected by the other's work.
Pattern 3 — Agent fanning out across stored contexts
An automation agent runs the same command across N stored contexts in parallel.
$ for ctx in personal work sandbox; do
dnsimple --context "$ctx" zones list --json &
done
$ wait
Each invocation reads its own context from credentials.yml. None of them touch active_context. The user's interactive session keeps whatever context was active before and after.
Pattern 4 — Agent with raw credentials from a secret manager
An agent holds tokens for accounts that aren't (and shouldn't be) stored in credentials.yml — typically because they come from a vault or environment.
$ dnsimple --token "$PROD_TOKEN" --account 981 zones list
$ dnsimple --sandbox --token "$SBX_TOKEN" --account 24 zones list
credentials.yml is never read. The CLI is fully stateless for this invocation.
Combining overrides
The override flags compose field-by-field, in the precedence order documented above. A few useful combinations:
# Use the 'work' context's token+host, but target a different account the token can see
$ dnsimple --context work --account 12345 zones list
# Use a stored context but inject a token from elsewhere (e.g. just-rotated, not yet saved)
$ dnsimple --context work --token "$NEW_TOKEN" zones list
--context provides the baseline; raw overrides win where present.
Interactive picker
Example shape of auth switch with no argument:
$ dnsimple auth switch
[1] personal production alice@example.com (981)
[2] work production bob@example.com (550)
[3] sandbox sandbox carol@example.com (24)
Select: _
Naming algorithm
Auto-derived names when --name is omitted, evaluated in order:
Re-login detection. If any existing context has the same (host, token) as the new login, mutate it in place. The context name is whatever it already is.
Bare environment name. Try production or sandbox. If free, use it.
Account-suffixed. Try <env>-<account_id> (e.g. production-981). If free, use it.
Numeric suffix. Try <env>-<account_id>-2, then -3, ... Used only when two distinct tokens grant access to the same account in the same environment.
When --name foo is passed explicitly:
Free → create.
Exists with the same (host, token) → mutate (refresh metadata).
Exists with a different (host, token) → reject: context "foo" already exists; pick a different name or run 'dnsimple auth logout --name foo' first. We never silently overwrite a token under a user-supplied name.
Note: account IDs in DNSimple are not unique across environments — account 24 in sandbox is unrelated to account 24 in production. The <env>- prefix is therefore mandatory in the auto-derived form.
Migration
Existing credentials.yml files using the hosts: map need a one-time, in-place migration on first read:
Each host entry becomes a context named after the environment (production, sandbox).
If both exist, production is set as active_context to preserve current default behavior.
Old hosts: key is removed after a successful write.
The pre-migration file is preserved as credentials.yml.bak for one release cycle so users have a manual rollback path.
The migration is silent on success and idempotent.
Description
Today the CLI's authentication is implicitly anchored to a single environment per invocation: production by default, sandbox only when
--sandboxis passed. Even thoughcredentials.ymlcan store credentials for both hosts simultaneously, there is no notion of an active selection — every command that targets sandbox needs the flag re-typed, and a user with multiple accounts in the same environment cannot keep them authenticated at the same time.We want a model where the user can be authenticated with multiple distinct credentials (e.g. production-personal, production-work, sandbox-test) and select one active context at a time. Subsequent commands should target the active context without requiring
--sandboxor--accounton every invocation. At the same time, agents and parallel shells must be able to bypass the active context entirely with per-invocation overrides.Why this matters
--sandboxmust be re-typed for every command.gh,kubectl, andaws --profileis "named context, active one at a time". The CLI doesn't match.Current state
credentials.ymlis keyed by host:Limitations:
Config.Sandbox.--sandboxis the only way to target sandbox, on every command.auth switch <id>only changes the default account for whichever host the per-invocation--sandboxflag selects.Expected Behavior or Outcome
Rework
credentials.ymlinto a list of named contexts, each carrying its own host, token, and default account:The CLI supports two operating modes that compose cleanly: stateful (use the active context) and stateless (per-invocation overrides).
Stateful mode (interactive default)
With no overrides, every command uses
active_context. The user manages contexts withauth login,auth list,auth switch,auth status,auth logout. This is the path for an interactive shell session.Stateless mode (per-invocation overrides)
For agents, automation, and parallel shells where the active context cannot be assumed or mutated, three overrides are available, in increasing rawness:
--context <name>— picks a stored context by name for this invocation only. Does not changeactive_context. This is the agent-friendly handle: "give me a pointer to a stored credential".--sandbox— host override. Tells the CLI to talk to the sandbox API for this invocation. Composes with the other overrides. When passed standalone and multiple sandbox contexts exist in storage, the CLI errors with the list of candidates and asks the user to disambiguate via--context.--token/--account— raw credential overrides. Highest precedence. Used when the agent has credentials from outsidecredentials.yml(env vars, secret manager, vault).Resolution chain
For each of
(token, host, account):--sandboxand--contextare independent and compose. You can use either, both, or neither.Command surface
auth login [--sandbox] [--name <name>]--nameis optional; auto-derived if omitted.--sandboxflag here means "this login is for the sandbox host".auth logout [--name <name>]auth listauth switch [<name-or-account-id>]auth switchno longer touches accounts; changing the account inside a context is done by re-runningauth login.auth statusWarningrow added inauth switchsilently accepts invalid accounts and auth status ignores stored default #25 for stale state.There is no separate
auth use—auth switchsubsumes that role.Acceptance Criteria
credentials.ymluses the newcontexts:+active_context:schema; the legacyhosts:map is migrated automatically on first load.credentials.yml.bakso users have a manual rollback path.auth logincreates a new context, auto-deriving its name when--nameis omitted (per the algorithm in Notes); when--nameis provided, the name is honored or rejected per the same rules.auth listlists all stored contexts, highlights the active one, and works with no network and no valid token.auth switch [<name-or-account-id>]updatesactive_contextlocally with no API call; without arguments it opens an interactive picker.auth logout [--name]removes a context and updates the active selection if needed.auth statusreports the active context's name, host/environment, and account, including theWarningrow fromauth switchsilently accepts invalid accounts and auth status ignores stored default #25.--context <name>is added and integrates with the resolution chain; passing it does not modifyactive_context.--sandboxcontinues to work as a per-invocation host override and composes with--context,--token, and--account.--sandboxstandalone with multiple sandbox contexts stored returns an actionable error listing the candidates and pointing to--context.--tokenand--accountcontinue to behave as raw overrides at the highest precedence and bypasscredentials.ymlentirely when both are supplied.dnsimple aidocument both the stateful and stateless modes, with the same patterns shown in Notes.--sandbox.Resources/References
auth switchsilently accepts invalid accounts and auth status ignores stored default #25 — the auth switch/status bug fix that surfaced this design gapauth statusoutput)auth switchsilently accepts invalid accounts and auth status ignores stored default #25credentials.ymlinto the OS keyring (blocked on this ticket)Notes
Concurrent and multi-context usage
The two-mode model is designed to support both interactive single-user shells and concurrent automation in the same CLI binary. The patterns below all run side by side without interfering with each other, because the only shared state (
active_context) is touched exclusively byauth switch.Pattern 1 — Interactive user, occasional one-shot to a different context
A developer working primarily in production occasionally needs to peek at sandbox without losing their place.
Pattern 2 — Multiple terminals each pinned to a different context
Two tmux panes (or two terminal windows) work in parallel, each effectively "logged in" to a different context for the duration of the session, without ever calling
auth switch.active_contextis never mutated by either pane, so neither pane'sauth statusis affected by the other's work.Pattern 3 — Agent fanning out across stored contexts
An automation agent runs the same command across N stored contexts in parallel.
Each invocation reads its own context from
credentials.yml. None of them touchactive_context. The user's interactive session keeps whatever context was active before and after.Pattern 4 — Agent with raw credentials from a secret manager
An agent holds tokens for accounts that aren't (and shouldn't be) stored in
credentials.yml— typically because they come from a vault or environment.credentials.ymlis never read. The CLI is fully stateless for this invocation.Combining overrides
The override flags compose field-by-field, in the precedence order documented above. A few useful combinations:
--contextprovides the baseline; raw overrides win where present.Interactive picker
Example shape of
auth switchwith no argument:Naming algorithm
Auto-derived names when
--nameis omitted, evaluated in order:(host, token)as the new login, mutate it in place. The context name is whatever it already is.productionorsandbox. If free, use it.<env>-<account_id>(e.g.production-981). If free, use it.<env>-<account_id>-2, then-3, ... Used only when two distinct tokens grant access to the same account in the same environment.When
--name foois passed explicitly:(host, token)→ mutate (refresh metadata).(host, token)→ reject:context "foo" already exists; pick a different name or run 'dnsimple auth logout --name foo' first. We never silently overwrite a token under a user-supplied name.Note: account IDs in DNSimple are not unique across environments — account 24 in sandbox is unrelated to account 24 in production. The
<env>-prefix is therefore mandatory in the auto-derived form.Migration
Existing
credentials.ymlfiles using thehosts:map need a one-time, in-place migration on first read:production,sandbox).productionis set asactive_contextto preserve current default behavior.hosts:key is removed after a successful write.credentials.yml.bakfor one release cycle so users have a manual rollback path.The migration is silent on success and idempotent.