Skip to content

feat: support project-level .codex config overrides #562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion codex-cli/src/cli.tsx
Original file line number Diff line number Diff line change
@@ -229,7 +229,12 @@ if (cli.flags.config) {
// ignore errors
}

const filePath = INSTRUCTIONS_FILEPATH;
// Open project instructions if present, else global
const cwd = process.cwd();
const projectInstructionsPath = path.join(cwd, ".codex", "instructions.md");
const filePath = fs.existsSync(projectInstructionsPath)
? projectInstructionsPath
: INSTRUCTIONS_FILEPATH;
const editor =
process.env["EDITOR"] || (process.platform === "win32" ? "notepad" : "vi");
spawnSync(editor, [filePath], { stdio: "inherit" });
43 changes: 37 additions & 6 deletions codex-cli/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -251,6 +251,8 @@ export const loadConfig = (
instructionsPath: string | undefined = INSTRUCTIONS_FILEPATH,
options: LoadConfigOptions = {},
): AppConfig => {
// Determine working directory for project‑level overrides
const cwd = options.cwd ?? process.cwd();
// Determine the actual path to load. If the provided path doesn't exist and
// the caller passed the default JSON path, automatically fall back to YAML
// variants.
@@ -280,9 +282,38 @@ export const loadConfig = (
storedConfig = {};
}
}
// Overlay project‑level config overrides from ./.codex if present
const projectConfigDir = join(cwd, ".codex");
const projectConfigPaths = [
join(projectConfigDir, "config.json"),
join(projectConfigDir, "config.yaml"),
join(projectConfigDir, "config.yml"),
];
for (const p of projectConfigPaths) {
if (existsSync(p)) {
try {
const raw = readFileSync(p, "utf-8");
const ext = extname(p).toLowerCase();
const projectStored =
ext === ".yaml" || ext === ".yml"
? (loadYaml(raw) as unknown as StoredConfig)
: JSON.parse(raw);
// Merge project overrides; shallow merge is sufficient
storedConfig = { ...storedConfig, ...projectStored };
} catch {
// ignore parsing errors in project config
}
break;
}
}

const instructionsFilePathResolved =
instructionsPath ?? INSTRUCTIONS_FILEPATH;
// Resolve instructions: prefer project/.codex/instructions.md over global
const projectInstructionsPath = join(cwd, ".codex", "instructions.md");
const instructionsFilePathResolved = instructionsPath
? instructionsPath
: existsSync(projectInstructionsPath)
? projectInstructionsPath
: INSTRUCTIONS_FILEPATH;
const userInstructions = existsSync(instructionsFilePathResolved)
? readFileSync(instructionsFilePathResolved, "utf-8")
: DEFAULT_INSTRUCTIONS;
@@ -366,13 +397,13 @@ export const loadConfig = (
}
}

// Always ensure the instructions file exists so users can edit it.
if (!existsSync(instructionsFilePathResolved)) {
const instrDir = dirname(instructionsFilePathResolved);
// Always ensure the global instructions file exists so users can edit it.
if (!existsSync(INSTRUCTIONS_FILEPATH)) {
const instrDir = dirname(INSTRUCTIONS_FILEPATH);
if (!existsSync(instrDir)) {
mkdirSync(instrDir, { recursive: true });
}
writeFileSync(instructionsFilePathResolved, userInstructions, "utf-8");
writeFileSync(INSTRUCTIONS_FILEPATH, userInstructions, "utf-8");
}
} catch {
// Silently ignore any errors – failure to persist the defaults shouldn't