Multiple Codex CLI Accounts Without Copying auth.json: How CODEX_HOME Isolation Works

Copying ~/.codex/auth.json between accounts is safe for an API key and fragile for a ChatGPT login, because Codex refreshes ChatGPT auth in place and each copy goes stale on its own. The durable setup gives every account its own CODEX_HOME and signs in inside it, so the directory Codex refreshes is the one you keep. This post shows what aisw actually writes, what it prints, and where each check lives in its source.

If you only want the commands, the Codex CLI account switch guide has the add, use and verify loop, and managing multiple Codex CLI accounts covers listing, renaming and removing profiles. This page is about the files underneath. All output below is from aisw 0.3.10 in a throwaway HOME with fake credentials.

What ~/.codex/auth.json holds

Codex CLI reads its whole state from CODEX_HOME, which defaults to ~/.codex. The credential lives in auth.json there, and config.toml next to it decides whether Codex uses that file at all.

A ChatGPT login (plain codex login) writes a token set, not a single secret. These are the fields aisw looks for when it decides an auth.json is ChatGPT-managed (auth_bytes_look_chatgpt_managed in src/auth/codex.rs); any one of them is enough:

{
  "auth_mode": "chatgpt",
  "tokens": {
    "refresh_token": "…",
    "account_id": "…",
    …
  },
  "last_refresh": "2026-10-08T09:00:00Z"
}

The refresh token is the part that matters. Codex uses it to get new access tokens and writes the result back into the same file. An API key has no lifecycle like that. When you save one with aisw, the profile's auth.json is a single field:

$ aisw add codex work-api-key --api-key "$WORK_OPENAI_API_KEY" --credential-backend file
$ ls -A ~/.aisw/profiles/codex/work-api-key/
auth.json  config.toml
$ cat ~/.aisw/profiles/codex/work-api-key/auth.json
{"token":"sk-proj-..."}
$ cat ~/.aisw/profiles/codex/work-api-key/config.toml
cli_auth_credentials_store = "file"

Both files are created with mode 600. A third kind of credential, a personal access token from codex login --with-access-token, looks like neither: it has no refresh-token fields, so aisw classifies it separately.

Why copying auth.json between accounts breaks

The usual hand-rolled switcher keeps auth.json.work and auth.json.personal and copies one over ~/.codex/auth.json. Three things go wrong with ChatGPT logins.

  • The copies drift. Codex refreshes the live file, not your saved copy. The next time you copy the saved one back, you restore the token set from before the refresh. aisw's own error message puts it plainly: "Codex refreshes ChatGPT auth in place, and those refresh/session tokens are not safely shareable across multiple live owners."
  • There may be no file. If config.toml sets cli_auth_credentials_store = "keyring", Codex keeps the credential in the OS keyring and auth.json is absent. The keyring entry is keyed on a SHA-256 hash of the canonical CODEX_HOME path, so you cannot copy it to another directory either. ephemeral storage is process-local and cannot be saved at all.
  • Everything else stays shared. Swapping one file leaves config, history and session state in the same ~/.codex, so two accounts share them.

aisw handles the storage setting by forcing file-backed auth. Every Codex profile gets cli_auth_credentials_store = "file", and when it applies a profile to the live directory it merges that line into your existing config.toml rather than replacing it. Starting from a live config set to keyring:

$ cat ~/.codex/config.toml
model = "gpt-5.4"
cli_auth_credentials_store = "keyring"
$ aisw use codex work-api-key --state-mode shared
$ cat ~/.codex/config.toml
model = "gpt-5.4"
cli_auth_credentials_store = "file"

If the live config.toml is not valid TOML, aisw refuses to merge and asks you to fix or remove it, instead of writing a line-merged file that may still be broken.

How aisw captures a ChatGPT login inside its own CODEX_HOME

aisw add codex personal without --api-key runs Codex's own login, but never against ~/.codex. The steps, from add_oauth_with and run_oauth_flow in src/auth/codex.rs:

  1. Create the profile directory and a scratch directory inside it, .oauth-capture.
  2. Write config.toml with cli_auth_credentials_store = "file" into the scratch directory before Codex starts.
  3. Spawn codex login with CODEX_HOME set to the scratch directory, and show "Waiting for Codex login to complete...".
  4. Poll every 500 ms for auth.json. As soon as it appears, stop the login process, set the file to 600, and store it in the profile (or in the system keyring with --credential-backend system-keyring).
  5. Reject the profile if the same OpenAI identity is already saved under another name, then delete the scratch directory.

To see this without a real account, we put a stub codex on PATH that logs its environment and writes a fake ChatGPT-shaped auth.json. This is what the login process saw, and what aisw printed:

CODEX_HOME=~/.aisw/profiles/codex/personal/.oauth-capture
config.toml
cli_auth_credentials_store = "file"

$ aisw add codex personal
Added profile
─────────────

  Tool          : Codex CLI
  Profile       : personal
  Auth          : oauth
  Backend       : file
  Codex auth    : chatgpt_managed_isolated
  Activation    : stored

Effects
  Profile credentials stored in aisw.
  Codex login ran inside this profile-owned CODEX_HOME, so future refreshes stay tied to this profile.
  This is the durable ChatGPT-managed Codex path.

Afterwards the profile directory holds just auth.json and config.toml; the capture directory is gone. With a real Codex the login opens the browser or device flow as usual.

When the capture fails

If Codex exits without writing auth.json, which is what happens when your build stores the credential somewhere else, aisw removes the half-made profile and prints:

Error: Codex exited before aisw could capture credentials.
  Codex login completed without writing auth.json into CODEX_HOME.
  If your Codex build stores auth somewhere else, use an API key instead.

If the login is still open after 120 seconds, aisw stops it and reports (quoted from run_oauth_flow):

Codex login timed out after 120s. If auth.json was not written, verify that config.toml has cli_auth_credentials_store = "file" (not "keyring").

The four Codex auth classifications

aisw labels every Codex profile, and the label decides which state modes it allows. You see it as Codex auth in aisw add, aisw use and aisw status, and as codex_auth_classification in JSON output.

ClassificationHow you get itShared mode
api_key--api-key, --api-key-stdin or --from-envAllowed
personal_access_token--from-live after codex login --with-access-tokenAllowed
chatgpt_managed_isolatedaisw add codex <name> with the login captured as aboveBlocked
chatgpt_managed_imported_bootstrap--from-live while a ChatGPT login is active in ~/.codexBlocked

The bootstrap label comes from a marker file, .aisw-chatgpt-bootstrap-from-live, written into the profile directory on import. Importing a fake ChatGPT login from the live directory:

$ aisw add codex work --from-live
  Codex auth    : chatgpt_managed_imported_bootstrap
  Activation    : active

Effects
  Profile credentials stored in aisw.
  Live tool configuration updated.
  Active profile updated.
  This Codex ChatGPT profile was imported from live state as a bootstrap session.
  Re-login directly inside this profile's isolated CODEX_HOME for the durable path.

A personal access token import prints "This Codex profile was imported from a personal access token session rather than a refresh-token-based ChatGPT login." instead, and on every switch to it, "it is not coupled to the ChatGPT refresh-token lifecycle that shared-mode switching blocks."

Why is the import only a bootstrap? The copied token set was minted for ~/.codex, and that directory can keep refreshing it. To turn it into the durable kind, remove the profile and sign in inside a fresh one:

aisw remove codex work --yes --force
aisw add codex work

--force is only needed if work is the active profile. The new login clears the marker, and the profile reports chatgpt_managed_isolated.

Isolated vs shared state

aisw use codex <profile> does two separate things, and the difference explains most "it still shows the old account" reports.

  • It writes the profile's auth.json and the merged config.toml into the live directory: $CODEX_HOME if it is set, otherwise ~/.codex. That happens in both modes, with or without the shell hook.
  • Through the shell hook, it sets CODEX_HOME for your shell. The hook runs the same command with --emit-env and evaluates the output.
$ aisw use codex work --state-mode isolated --emit-env
export CODEX_HOME='~/.aisw/profiles/codex/work'

In isolated mode, the default, that export points Codex at the profile directory itself. Codex reads, refreshes and rewrites the auth.json that aisw stores, so there is no second copy to fall behind, and config and history are per profile too. Without the hook in a given terminal, Codex reads the copy in ~/.codex instead, which is why aisw says isolation applies "when shell integration is active". The shell integration docs cover zsh, bash, fish and PowerShell.

In shared mode, aisw unsets CODEX_HOME and Codex keeps using ~/.codex; only the credential changes. An API key never changes, and before switching away from a shared access-token profile aisw copies the live auth.json back into it if the account identity still matches. For a ChatGPT login, shared mode would be the copy-auth.json pattern again, so aisw refuses it. For a bootstrap profile the refusal adds two lines that the switch guide does not show:

$ aisw use codex work --state-mode shared
Error: shared-mode ChatGPT switching is unsupported for Codex profile 'work'.
  Codex refreshes ChatGPT auth in place, and those refresh/session tokens are not safely shareable across multiple live owners.
  This is an expected upstream limitation, not an aisw corruption bug.
  Use isolated mode instead: aisw use codex work --state-mode isolated
  This profile was imported from live state, so treat it as a bootstrap session.
  Re-login directly inside its profile-owned CODEX_HOME for a durable setup, or use an API key for automation-style repeatability.

After a successful isolated switch to a login captured inside its profile, the last effect line confirms which kind you have: "This Codex ChatGPT login is durable because it is tied to this profile-owned CODEX_HOME."

What this does not change

A running codex process keeps the CODEX_HOME and credential it started with, so start a new one after switching. aisw does not sign you in on OpenAI's side, pool usage across accounts, or get around plan limits. For keeping a repository on the right account, see using a Codex account per repository; for the sign-in methods themselves, the Codex CLI login guide.

Stop moving auth.json by hand

Install aisw CLI for repeatable switching, or use the desktop app to inspect profiles visually.

Codex auth.json and CODEX_HOME questions

Direct answers about what auth.json holds, why copying it fails, and how aisw labels Codex logins.

Can I switch Codex CLI accounts by copying auth.json?

It works for an API key, which never changes. For a ChatGPT login it is fragile: Codex refreshes that auth in place, so a copied auth.json and the original drift apart, and if cli_auth_credentials_store is set to keyring there is no auth.json to copy at all. Give each account its own CODEX_HOME instead.

What is inside ~/.codex/auth.json?

For a ChatGPT login it holds a tokens object with a refresh token and account id, usually with auth_mode set to chatgpt and a last_refresh timestamp. aisw uses those fields to tell a ChatGPT-managed login from an API key or personal access token.

Why does aisw set cli_auth_credentials_store to file?

Codex keys its keyring entry on a hash of the CODEX_HOME path, which aisw cannot reconstruct portably. File-backed auth gives each profile directory its own auth.json that Codex reads and refreshes in place.

What does chatgpt_managed_imported_bootstrap mean?

The profile was copied from the live ~/.codex with aisw add codex <name> --from-live rather than signed in inside its own CODEX_HOME. It works, but aisw treats it as a starting point and recommends signing in again inside the profile.

Does switching Codex accounts bypass usage limits?

No. aisw manages local profiles and which credential Codex reads; it does not bypass authentication, quotas, usage limits, or provider terms.