Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ You can also use Codex with an API key, but this requires [additional setup](./d

### Model Context Protocol (MCP)

Codex CLI supports [MCP servers](./docs/advanced.md#model-context-protocol-mcp). Enable by adding an `mcp_servers` section to your `~/.codex/config.toml`.
Codex CLI supports [MCP servers](./docs/advanced.md#model-context-protocol-mcp). Enable by adding an `mcp_servers` section to your `$CODEX_HOME/config.toml` (see docs/config.md for the default location).


### Configuration

Codex CLI supports a rich set of configuration options, with preferences stored in `~/.codex/config.toml`. For full configuration options, see [Configuration](./docs/config.md).
Codex CLI supports a rich set of configuration options, with preferences stored in `$CODEX_HOME/config.toml`. For full configuration options and default path, see [Configuration](./docs/config.md).

---

Expand Down
2 changes: 1 addition & 1 deletion codex-rs/common/src/config_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use toml::Value;
#[derive(Parser, Debug, Default, Clone)]
pub struct CliConfigOverrides {
/// Override a configuration value that would otherwise be loaded from
/// `~/.codex/config.toml`. Use a dotted path (`foo.bar.baz`) to override
/// `$CODEX_HOME/config.toml`. Use a dotted path (`foo.bar.baz`) to override
/// nested values. The `value` portion is parsed as JSON. If it fails to
/// parse as JSON, the raw string is used as a literal.
///
Expand Down
225 changes: 215 additions & 10 deletions codex-rs/core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1149,22 +1149,52 @@ fn default_review_model() -> String {
/// - If `CODEX_HOME` is not set, this function does not verify that the
/// directory exists.
pub fn find_codex_home() -> std::io::Result<PathBuf> {
// Honor the `CODEX_HOME` environment variable when it is set to allow users
// (and tests) to override the default location.
// 1) Highest precedence: explicit override via CODEX_HOME
if let Ok(val) = std::env::var("CODEX_HOME")
&& !val.is_empty()
{
return PathBuf::from(val).canonicalize();
}

let mut p = home_dir().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not find home directory",
)
})?;
p.push(".codex");
Ok(p)
// 2) Backwards-compat: if legacy ~/.codex exists, keep using it
if let Some(home) = home_dir() {
let legacy = home.join(".codex");
if legacy.exists() {
return Ok(legacy);
}
}

// 3) Modern default: platform config directory /codex
// - Linux: $XDG_CONFIG_HOME/codex (or ~/.config/codex)
// - macOS: ~/Library/Application Support/codex
// - Windows: %AppData%\codex
// Prefer the OS-reported configuration directory.
if let Some(base) = dirs::config_dir() {
return Ok(base.join("codex"));
}

// Fallbacks for environments where config_dir() may be unavailable
// (e.g., stripped env on CI or limited shells). Keep these conservative
// and platform-aware to avoid surprising locations.
#[cfg(windows)]
{
use std::env;
if let Ok(appdata) = env::var("APPDATA") {
return Ok(PathBuf::from(appdata).join("codex"));
}
if let Ok(local) = env::var("LOCALAPPDATA") {
return Ok(PathBuf::from(local).join("codex"));
}
if let Some(home) = home_dir() {
// Default Windows roaming path under the user profile.
return Ok(home.join("AppData").join("Roaming").join("codex"));
}
}

Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not resolve platform config directory",
))
}

/// Returns the path to the folder where Codex logs are stored. Does not verify
Expand All @@ -1183,9 +1213,184 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;

use std::env;
use std::sync::Mutex;
use std::sync::OnceLock;
use std::time::Duration;
use tempfile::TempDir;

static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();

fn env_lock() -> &'static Mutex<()> {
ENV_LOCK.get_or_init(|| Mutex::new(()))
}

fn set_opt_var(key: &str, val: Option<&str>) {
unsafe {
match val {
Some(v) => env::set_var(key, v),
None => env::remove_var(key),
}
}
}

#[test]
fn find_codex_home_prefers_env() -> anyhow::Result<()> {
let _g = env_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);

let prev = env::var("CODEX_HOME").ok();
let tmp = TempDir::new()?;
unsafe {
env::set_var("CODEX_HOME", tmp.path());
}

let got = find_codex_home()?;
assert_eq!(got, tmp.path().canonicalize()?);

set_opt_var("CODEX_HOME", prev.as_deref());
Ok(())
}

#[cfg(unix)]
#[test]
fn find_codex_home_legacy_unix() -> anyhow::Result<()> {
let _g = env_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);

let prev_code = env::var("CODEX_HOME").ok();
let prev_home = env::var("HOME").ok();
let prev_xdg = env::var("XDG_CONFIG_HOME").ok();

unsafe {
env::remove_var("CODEX_HOME");
}

let tmp = TempDir::new()?;
let legacy = tmp.path().join(".codex");
std::fs::create_dir_all(&legacy)?;
unsafe {
env::set_var("HOME", tmp.path());
env::set_var("XDG_CONFIG_HOME", tmp.path().join("xdg"));
}

let got = find_codex_home()?;
assert_eq!(got, legacy);

set_opt_var("CODEX_HOME", prev_code.as_deref());
set_opt_var("HOME", prev_home.as_deref());
set_opt_var("XDG_CONFIG_HOME", prev_xdg.as_deref());
Ok(())
}

#[cfg(all(unix, not(target_os = "macos")))]
#[test]
fn find_codex_home_defaults_to_config_dir_on_unix() -> anyhow::Result<()> {
let _g = env_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);

let prev_code = env::var("CODEX_HOME").ok();
let prev_home = env::var("HOME").ok();
let prev_xdg = env::var("XDG_CONFIG_HOME").ok();

unsafe {
env::remove_var("CODEX_HOME");
}

// Use a temporary HOME without ~/.codex
let tmp = TempDir::new()?;
unsafe {
env::set_var("HOME", tmp.path());
}

// Force XDG config dir to a temp location
let xdg = tmp.path().join("xdg-config");
std::fs::create_dir_all(&xdg)?;
unsafe {
env::set_var("XDG_CONFIG_HOME", &xdg);
}

let got = find_codex_home()?;
let expected_base = dirs::config_dir().expect("config_dir must exist");
assert_eq!(got, expected_base.join("codex"));

set_opt_var("CODEX_HOME", prev_code.as_deref());
set_opt_var("HOME", prev_home.as_deref());
set_opt_var("XDG_CONFIG_HOME", prev_xdg.as_deref());
Ok(())
}

#[cfg(windows)]
#[test]
fn find_codex_home_defaults_to_appdata_on_windows() -> anyhow::Result<()> {
use std::path::PathBuf;
let _g = env_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);

let prev_code = env::var("CODEX_HOME").ok();
let prev_app = env::var("APPDATA").ok();
let prev_user = env::var("USERPROFILE").ok();

unsafe {
env::remove_var("CODEX_HOME");
}

// Create an isolated APPDATA and USERPROFILE
let tmp = TempDir::new()?;
let appdata = tmp.path().join("appdata");
std::fs::create_dir_all(&appdata)?;
unsafe {
env::set_var("APPDATA", &appdata);
env::set_var("USERPROFILE", tmp.path());
}

// Ensure legacy %USERPROFILE%\.codex does not exist
let legacy = PathBuf::from(env::var("USERPROFILE")?).join(".codex");
if legacy.exists() {
std::fs::remove_dir_all(&legacy)?;
}

let got = find_codex_home()?;
assert_eq!(got, PathBuf::from(env::var("APPDATA")?).join("codex"));

set_opt_var("CODEX_HOME", prev_code.as_deref());
set_opt_var("APPDATA", prev_app.as_deref());
set_opt_var("USERPROFILE", prev_user.as_deref());
Ok(())
}

#[cfg(target_os = "macos")]
#[test]
fn find_codex_home_defaults_to_config_dir_on_macos() -> anyhow::Result<()> {
let _g = env_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);

let prev_code = env::var("CODEX_HOME").ok();
let prev_home = env::var("HOME").ok();

unsafe {
env::remove_var("CODEX_HOME");
}

let tmp = TempDir::new()?;
unsafe {
env::set_var("HOME", tmp.path());
}

let got = find_codex_home()?;
let expected_base = dirs::config_dir().expect("config_dir must exist");
assert_eq!(got, expected_base.join("codex"));

set_opt_var("CODEX_HOME", prev_code.as_deref());
set_opt_var("HOME", prev_home.as_deref());
Ok(())
}

#[test]
fn test_toml_parsing() {
let history_with_persistence = r#"
Expand Down
6 changes: 3 additions & 3 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ Notes:

Because Codex is written in Rust, it honors the `RUST_LOG` environment variable to configure its logging behavior.

The TUI defaults to `RUST_LOG=codex_core=info,codex_tui=info` and log messages are written to `~/.codex/log/codex-tui.log`, so you can leave the following running in a separate terminal to monitor log messages as they are written:
The TUI defaults to `RUST_LOG=codex_core=info,codex_tui=info` and log messages are written to `$CODEX_HOME/log/codex-tui.log`, so you can leave the following running in a separate terminal to monitor log messages as they are written:

```
tail -F ~/.codex/log/codex-tui.log
tail -F "$CODEX_HOME/log/codex-tui.log"
```

By comparison, the non-interactive mode (`codex exec`) defaults to `RUST_LOG=error`, but messages are printed inline, so there is no need to monitor a separate file.
Expand All @@ -62,7 +62,7 @@ See the Rust documentation on [`RUST_LOG`](https://docs.rs/env_logger/latest/env

## Model Context Protocol (MCP)

The Codex CLI can be configured to leverage MCP servers by defining an [`mcp_servers`](./config.md#mcp_servers) section in `~/.codex/config.toml`. It is intended to mirror how tools such as Claude and Cursor define `mcpServers` in their respective JSON config files, though the Codex format is slightly different since it uses TOML rather than JSON, e.g.:
The Codex CLI can be configured to leverage MCP servers by defining an [`mcp_servers`](./config.md#mcp_servers) section in `$CODEX_HOME/config.toml`. It is intended to mirror how tools such as Claude and Cursor define `mcpServers` in their respective JSON config files, though the Codex format is slightly different since it uses TOML rather than JSON, e.g.:

```toml
# IMPORTANT: the top-level key is `mcp_servers` rather than `mcpServers`.
Expand Down
12 changes: 6 additions & 6 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This key must, at minimum, have write access to the Responses API.
If you've used the Codex CLI before with usage-based billing via an API key and want to switch to using your ChatGPT plan, follow these steps:

1. Update the CLI and ensure `codex --version` is `0.20.0` or later
2. Delete `~/.codex/auth.json` (on Windows: `C:\\Users\\USERNAME\\.codex\\auth.json`)
2. Delete `$CODEX_HOME/auth.json` (see docs/config.md for the default location on your OS)
3. Run `codex login` again

## Connecting on a "Headless" Machine
Expand All @@ -24,28 +24,28 @@ Today, the login process entails running a server on `localhost:1455`. If you ar

### Authenticate locally and copy your credentials to the "headless" machine

The easiest solution is likely to run through the `codex login` process on your local machine such that `localhost:1455` _is_ accessible in your web browser. When you complete the authentication process, an `auth.json` file should be available at `$CODEX_HOME/auth.json` (on Mac/Linux, `$CODEX_HOME` defaults to `~/.codex` whereas on Windows, it defaults to `%USERPROFILE%\\.codex`).
The easiest solution is likely to run through the `codex login` process on your local machine such that `localhost:1455` _is_ accessible in your web browser. When you complete the authentication process, an `auth.json` file should be available at `$CODEX_HOME/auth.json`. See [Configuration](./config.md) for the default location per platform.

Because the `auth.json` file is not tied to a specific host, once you complete the authentication flow locally, you can copy the `$CODEX_HOME/auth.json` file to the headless machine and then `codex` should "just work" on that machine. Note to copy a file to a Docker container, you can do:

```shell
# substitute MY_CONTAINER with the name or id of your Docker container:
CONTAINER_HOME=$(docker exec MY_CONTAINER printenv HOME)
docker exec MY_CONTAINER mkdir -p "$CONTAINER_HOME/.codex"
docker exec MY_CONTAINER mkdir -p "$CONTAINER_HOME/.codex" # or set CODEX_HOME and use it instead
docker cp auth.json MY_CONTAINER:"$CONTAINER_HOME/.codex/auth.json"
```

whereas if you are `ssh`'d into a remote machine, you likely want to use [`scp`](https://en.wikipedia.org/wiki/Secure_copy_protocol):

```shell
ssh user@remote 'mkdir -p ~/.codex'
scp ~/.codex/auth.json user@remote:~/.codex/auth.json
ssh user@remote 'mkdir -p ${CODEX_HOME:-~/.codex}'
scp "$CODEX_HOME/auth.json" user@remote:"${CODEX_HOME:-~/.codex}/auth.json"
```

or try this one-liner:

```shell
ssh user@remote 'mkdir -p ~/.codex && cat > ~/.codex/auth.json' < ~/.codex/auth.json
ssh user@remote 'mkdir -p ${CODEX_HOME:-~/.codex} && cat > ${CODEX_HOME:-~/.codex}/auth.json' < "$CODEX_HOME/auth.json"
```

### Connecting through VPS or remote
Expand Down
6 changes: 5 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ Codex supports several mechanisms for setting config values:
- If `value` cannot be parsed as a valid TOML value, it is treated as a string value. This means that `-c model='"o3"'` and `-c model=o3` are equivalent.
- In the first case, the value is the TOML string `"o3"`, while in the second the value is `o3`, which is not valid TOML and therefore treated as the TOML string `"o3"`.
- Because quotes are interpreted by one's shell, `-c key="true"` will be correctly interpreted in TOML as `key = true` (a boolean) and not `key = "true"` (a string). If for some reason you needed the string `"true"`, you would need to use `-c key='"true"'` (note the two sets of quotes).
- The `$CODEX_HOME/config.toml` configuration file where the `CODEX_HOME` environment value defaults to `~/.codex`. (Note `CODEX_HOME` will also be where logs and other Codex-related information are stored.)
- The `$CODEX_HOME/config.toml` configuration file. If `CODEX_HOME` is not set, Codex now defaults to your platform's config directory, typically:
- Linux: `$XDG_CONFIG_HOME/codex` (or `~/.config/codex`)
- macOS: `~/Library/Application Support/codex`
- Windows: `%AppData%\codex`
If a legacy `~/.codex` directory already exists, Codex will continue to use it for backward compatibility. (`CODEX_HOME` is still the authoritative override.)

Both the `--config` flag and the `config.toml` file support the following options:

Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Key flags: `--model/-m`, `--ask-for-approval/-a`.

- Run `codex resume` to display the session picker UI
- Resume most recent: `codex resume --last`
- Resume by id: `codex resume <SESSION_ID>` (You can get session ids from /status or `~/.codex/sessions/`)
- Resume by id: `codex resume <SESSION_ID>` (You can get session ids from /status or `$CODEX_HOME/sessions/`)

Examples:

Expand Down Expand Up @@ -63,7 +63,7 @@ Below are a few bite-size examples you can copy-paste. Replace the text in quote

You can give Codex extra instructions and guidance using `AGENTS.md` files. Codex looks for `AGENTS.md` files in the following places, and merges them top-down:

1. `~/.codex/AGENTS.md` - personal global guidance
1. `$CODEX_HOME/AGENTS.md` - personal global guidance (see docs/config.md for default location)
2. `AGENTS.md` at repo root - shared project notes
3. `AGENTS.md` in the current working directory - sub-folder/feature specifics

Expand Down
2 changes: 1 addition & 1 deletion docs/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Save frequently used prompts as Markdown files and reuse them quickly from the slash menu.

- Location: Put files in `$CODEX_HOME/prompts/` (defaults to `~/.codex/prompts/`).
- Location: Put files in `$CODEX_HOME/prompts/`.
- File type: Only Markdown files with the `.md` extension are recognized.
- Name: The filename without the `.md` extension becomes the slash entry. For a file named `my-prompt.md`, type `/my-prompt`.
- Content: The file contents are sent as your message when you select the item in the slash popup and press Enter.
Expand Down
Loading