User-Level vs Project-Level Codex Config: Key Differences Explained

User-level Codex configuration lives in ~/.codex and provides machine-wide defaults, while project-level configuration resides in the current working directory and overrides those defaults for that specific repository only.

The openai/codex-plugin-cc repository implements a hierarchical configuration system that distinguishes between global user preferences and repository-specific settings. Understanding how these two layers interact is essential for managing defaults across development environments while maintaining per-project flexibility. The system resolves configurations through resolveCodexHome() for global settings and config/read requests for local overrides.

Understanding User-Level Codex Configuration

The user-level layer acts as the global baseline for every Codex session on your machine. According to the source code in plugins/codex/scripts/lib/codex.mjs, the CLI first determines where user settings live by calling resolveCodexHome() at lines 54-55, which defaults to ~/.codex unless overridden by the CODEX_HOME environment variable.

This directory stores machine-wide defaults including:

  • Default model provider and endpoint settings
  • API key locations and authentication status
  • Shared runtime configurations
  • Global feature flags like stopReviewGate

When you run any Codex command, these values load first and serve as fallbacks whenever a project does not explicitly define its own settings.

Understanding Project-Level Codex Configuration

The project-level layer lives inside your repository's working directory and fine-tunes behavior for that specific codebase. As implemented in plugins/codex/scripts/lib/codex.mjs at lines 871-876, the client sends a client.request("config/read", { cwd }) to the app-server, which locates and returns the project-specific JSON configuration.

Project-level settings typically include:

  • ** stopReviewGate**: Enforces mandatory code review gates for the repository
  • Custom provider aliases: Per-project API endpoints or model selections
  • Sandbox mode: Directory-specific read-write restrictions
  • Default models: Overriding the global model for this codebase only

These values take precedence over user-level defaults for the duration of the command execution.

How Configuration Precedence Works

The merging logic resides in plugins/codex/scripts/lib/state.mjs, specifically within the getConfig(workspaceRoot) function. When a plugin or hook requests configuration, the system first loads the user-level defaults from CODEX_HOME, then overlays any project-level values found via the config/read request.

A concrete example appears in plugins/codex/scripts/stop-review-gate-hook.mjs at line 146. The hook calls getConfig(workspaceRoot) and checks the stopReviewGate property:

  • If the project config contains {"stopReviewGate": true}, the gate activates for that repository.
  • If the project omits the flag, the hook falls back to the user-level default (typically false).

This overlay pattern ensures that project settings always win, while user defaults provide a safety net for unspecified values.

Practical Configuration Examples

Setting User-Level Global Defaults

Create persistent defaults that apply across all projects by writing to the Codex home directory:


# Ensure the directory exists (resolves to ~/.codex by default)

mkdir -p "$(codex home)"

# Write global configuration

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

The resolveCodexHome() utility ensures these settings are available for every Codex invocation on your machine.

Creating Project-Specific Overrides

Add a configuration file to your repository root to override global settings for team members working in that directory:

{
  "stopReviewGate": true,
  "model": "gpt-4o",
  "sandbox": "read-write"
}

When Codex executes within this directory, the app-server (handled in plugins/codex/scripts/lib/app-server.mjs) processes the config/read request and returns these values, which then override your ~/.codex defaults.

Accessing Merged Configuration in Plugins

Plugin authors can retrieve the fully merged configuration using the state utility:

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

function enforcePolicy(workspaceRoot) {
  // Returns merged user-level + project-level config
  const cfg = getConfig(workspaceRoot);
  
  if (cfg.stopReviewGate) {
    // Execute review gate logic specific to this repository
    console.log("Review gate enforced via project config");
  }
}

This pattern appears throughout the codebase, including in plugins/codex/scripts/lib/args.mjs, which seeds command-line options from the merged configuration object.

Summary

  • User-level config resides in ~/.codex (or CODEX_HOME) and provides global defaults read via resolveCodexHome().
  • Project-level config lives in the repository root, loaded via the config/read request handled by the app-server.
  • Precedence rules: Project settings override user defaults when merged through getConfig(workspaceRoot).
  • Implementation: The stop-review-gate-hook.mjs demonstrates real-world usage, checking the merged configuration to activate features only when enabled at the project level.

Frequently Asked Questions

Where is user-level Codex config stored?

By default, user-level configuration is stored in ~/.codex, though you can relocate it by setting the CODEX_HOME environment variable. The resolveCodexHome() function in plugins/codex/scripts/lib/codex.mjs handles this resolution at lines 54-55.

Can project-level settings override user-level defaults?

Yes. When getConfig(workspaceRoot) executes, it merges both layers and gives precedence to project-level values. If a setting exists in both the user home directory and the project configuration file, the project value takes effect for that repository only.

How does the Codex CLI read project-level configuration?

The client sends a JSON-RPC request client.request("config/read", { cwd }) to the app-server, as seen in plugins/codex/scripts/lib/codex.mjs at lines 871-876. The server locates the configuration file in the specified working directory and returns it for merging with global defaults.

What happens if a configuration key is missing from the project settings?

The system falls back to the user-level default. For example, if a project does not define stopReviewGate, the hook in stop-review-gate-hook.mjs uses the value from ~/.codex/config.json or the built-in default, ensuring continuous operation without requiring every project to explicitly define every setting.

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 →