User-Level vs Project-Level Codex Configuration: Understanding the Hierarchy in openai/codex-plugin-cc

User-level Codex configuration provides global defaults stored in ~/.codex, while project-level configuration resides in the repository and overrides those defaults for that specific workspace.

The openai/codex-plugin-cc repository implements a two-tier configuration system that balances machine-wide preferences with repository-specific requirements. Understanding how user-level and project-level Codex configuration interact is essential for managing default models, API endpoints, and security gates across different development environments.

Where Configuration Lives: Files and Paths

User-Level Configuration (CODEX_HOME)

The user-level layer lives in a dedicated home directory controlled by the CODEX_HOME environment variable, which defaults to ~/.codex. In plugins/codex/scripts/lib/codex.mjs, the helper function resolveCodexHome() (lines 54-55) resolves this path and confirms its existence before the CLI reads global defaults. This directory stores files such as external_agent_session_imports.json and other hidden configuration files that apply to every Codex session on the machine.

Project-Level Configuration (CWD)

Project-level settings reside inside the current working directory—typically in a JSON file that the Codex app-server discovers via a config/read request. When a command executes, the client sends client.request("config/read", { cwd }) to the server (implemented in codex.mjs lines 871-876), which locates and returns the repository-specific configuration. These settings override the global baseline only for the duration of the command within that directory.

How Configuration Layers Are Merged

The resolution process follows a strict hierarchy. First, the CLI invokes resolveCodexHome() to establish the user-level baseline from ~/.codex. Then, if a project configuration file exists in the workspace root, the app-server intercepts the config/read request (handled in app-server.mjs) and returns the project-specific JSON. Finally, the getConfig(workspaceRoot) utility from state.mjs merges these layers, with project values taking precedence over user defaults.

A concrete example is the review-gate flag. The stop-review-gate-hook.mjs hook (line 146) calls getConfig(workspaceRoot) to fetch the merged configuration. If the project-level config contains {"stopReviewGate": true}, the hook activates the gate for that repository only; otherwise, it falls back to the user-level default (typically false).

Practical Configuration Examples

Setting User-Level Configuration (Global)

Configure defaults that apply across all projects on your machine:


# Create the global config directory if it doesn't exist

mkdir -p "$(codex home)"            # Resolves to ~/.codex via resolveCodexHome()

# Write global defaults

cat > "$(codex home)/config.json" <<EOF
{
  "model_provider": "openai",
  "model": "gpt-4o-mini",
  "stopReviewGate": false
}
EOF

The codex home command relies on resolveCodexHome() defined in codex.mjs (lines 54-55).

Adding Project-Level Configuration (Per-Repository)

Create a configuration file in your repository root to override global settings:

// codex.project.json in the repository root
{
  "stopReviewGate": true,
  "model": "gpt-4o",
  "sandbox": "read-write"
}

When you run a Codex command inside this directory, the app-server executes the config/read request (lines 871-876 of codex.mjs) and merges these values with the global defaults.

Accessing Merged Configuration from a Plugin

Use the state.mjs module to retrieve the fully resolved configuration within hooks or plugins:

import { getConfig } from "./state.mjs";

function someHook(workspaceRoot) {
  const cfg = getConfig(workspaceRoot);   // Merges user- and project-level
  if (cfg.stopReviewGate) {
    // Enforce review gate for this repo only
    console.log("Review gate active: stopping for approval");
  }
}

This pattern is demonstrated in stop-review-gate-hook.mjs, which checks project-level flags before falling back to user defaults.

Key Implementation Files

Understanding these source files clarifies how the configuration hierarchy operates:

  • codex.mjs (plugins/codex/scripts/lib/codex.mjs): Contains resolveCodexHome() for locating the user directory and issues the config/read request for project files.
  • state.mjs (plugins/codex/scripts/lib/state.mjs): Provides getConfig(), which merges user-level and project-level settings for consumption by plugins.
  • app-server.mjs (plugins/codex/scripts/lib/app-server.mjs): Handles the underlying client.request("config/read", …) that pulls project-level JSON from the working directory.
  • stop-review-gate-hook.mjs (plugins/codex/scripts/stop-review-gate-hook.mjs): Example consumer that demonstrates project-level flag override behavior.
  • args.mjs (plugins/codex/scripts/lib/args.mjs): Demonstrates how CLI options seed from the merged configuration layer.

Summary

  • User-level configuration lives in ~/.codex (or CODEX_HOME) and provides global defaults via resolveCodexHome().
  • Project-level configuration resides in the repository and is fetched via the config/read request (lines 871-876 of codex.mjs).
  • The getConfig() utility in state.mjs merges layers, with project settings overriding user defaults.
  • Security features like stopReviewGate demonstrate this hierarchy: project settings activate per-repo gates, while user settings provide the machine-wide fallback.

Frequently Asked Questions

What takes precedence if both configurations define the same setting?

Project-level values always override user-level defaults. When getConfig(workspaceRoot) executes in state.mjs, it merges the project-specific JSON (returned by the config/read request) on top of the user-level baseline, ensuring repository-specific requirements take priority.

How does the Codex CLI locate the user-level configuration directory?

The CLI calls resolveCodexHome() defined in codex.mjs (lines 54-55), which checks the CODEX_HOME environment variable and falls back to ~/.codex. This function ensures the directory exists before the client attempts to read global configuration files.

Can project-level configuration affect security settings like review gates?

Yes. The stop-review-gate-hook.mjs (line 146) specifically checks the merged configuration for stopReviewGate. If the project config sets this to true, the gate activates for that repository only, regardless of the user-level default. This allows teams to enforce mandatory code reviews on a per-project basis.

Where should I store sensitive API keys in Codex configuration?

Store sensitive credentials in the user-level configuration within ~/.codex, not in project-level files that might be committed to version control. The codex.mjs utilities read authentication status from the user home directory, keeping secrets out of shared repositories while allowing the project config to reference provider aliases or endpoints.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →