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.tomlsetscli_auth_credentials_store = "keyring", Codex keeps the credential in the OS keyring andauth.jsonis absent. The keyring entry is keyed on a SHA-256 hash of the canonicalCODEX_HOMEpath, so you cannot copy it to another directory either.ephemeralstorage 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:
- Create the profile directory and a scratch directory inside it,
.oauth-capture. - Write
config.tomlwithcli_auth_credentials_store = "file"into the scratch directory before Codex starts. - Spawn
codex loginwithCODEX_HOMEset to the scratch directory, and show "Waiting for Codex login to complete...". - Poll every 500 ms for
auth.json. As soon as it appears, stop the login process, set the file to600, and store it in the profile (or in the system keyring with--credential-backend system-keyring). - 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.
| Classification | How you get it | Shared mode |
|---|---|---|
api_key | --api-key, --api-key-stdin or --from-env | Allowed |
personal_access_token | --from-live after codex login --with-access-token | Allowed |
chatgpt_managed_isolated | aisw add codex <name> with the login captured as above | Blocked |
chatgpt_managed_imported_bootstrap | --from-live while a ChatGPT login is active in ~/.codex | Blocked |
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.jsonand the mergedconfig.tomlinto the live directory:$CODEX_HOMEif it is set, otherwise~/.codex. That happens in both modes, with or without the shell hook. - Through the shell hook, it sets
CODEX_HOMEfor your shell. The hook runs the same command with--emit-envand 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.