feat(installer): support project-local Codex installs - #1551
Conversation
) Codex CLI has a first-class project config layer — `.codex/config.toml` is layer 4 of the loader stack, above the user config at layer 6 (`codex-rs/config/src/loader/README.md` in openai/codex), and it landed in openai/codex#8354 on 2025-12-22. The CodexTarget's "Codex has no project-local config concept" note was therefore never accurate, and `supportsLocation('local') === false` made Codex the one agent that forces a machine-wide MCP install. `mcp_servers` is not on the project layer's denylist (which strips base URLs, model providers, `notify`, profiles and otel — settings repo contents shouldn't choose), so a project-scoped `[mcp_servers.codegraph]` is honored. - Path helpers take a `Location`: global keeps `~/.codex/config.toml` + `~/.codex/AGENTS.md`; local writes `<cwd>/.codex/config.toml` and the project-root `<cwd>/AGENTS.md` — the same split the gemini and opencode targets already use for their local layout. - Drops the five `loc !== 'global'` early returns from detect, install, uninstall, printConfig and describePaths. - Local install returns a note that Codex only applies a project layer in a project marked trusted; untrusted projects load the layer but leave it disabled, so a silent success would be misleading. - Refreshes the two doc comments that used Codex as the example of a global-only target (now the Copilot CLI). Tests: two new cases covering the local write layout, the trust note, global config staying untouched, and local uninstall leaving the global entry intact. Both fail against the previous implementation. The generic per-target contract suite now also exercises codex at location=local.
There was a problem hiding this comment.
CodeGraph review
Overall risk: 🟡 Low — Codex CLI now supports project-local install/uninstall; new tests directly cover file creation, reversal, and global-config isolation.
Verify in the running product (3 checks)
- cli — In a fresh project directory, run
codegraph install --location=localand select only Codex CLI. Confirm it creates ./.codex/config.toml with [mcp_servers.codegraph] and adds a CodeGraph block to the project's AGENTS.md, and that it prints a note that the config only activates once the project is trusted in Codex. - cli — After the local install above, run
codegraph uninstall --location=local. Confirm the local .codex/config.toml entry and AGENTS.md block are removed, while a separately-run global install (~/.codex/config.toml) is left untouched. - cli — Run the interactive
codegraph installand select only Codex CLI as the target. Confirm the 'All projects vs just this project' location prompt now appears.
Worth double-checking
- Upstream Codex trust-gating claim
- Installer contract idempotency/recoverable coverage for codex-local
- CHANGELOG entry placement
What to look for in each
- Upstream Codex trust-gating claim — The header comment in codex.ts asserts Codex's project config layer (openai/codex#8354), the denylist that excludes mcp_servers, and the 'disabled until trusted' behavior. This is unverifiable from this repo's source — confirm it against the actual Codex CLI version being targeted, since the whole local-install feature's correctness rests on it.
- Installer contract idempotency/recoverable coverage for codex-local — The two new tests (#1531) explicitly assert reversibility and that global config stays untouched (sibling-preserving), but don't directly assert that a second local install is byte-identical to the first (idempotent) or that install recovers correctly from a partial prior local state — confirm the shared parameterized suite exercises codex+local for those two invariants too.
- CHANGELOG entry placement — The diff hunk doesn't show the enclosing '### New Features' / '## [Unreleased]' headers directly — confirm the new Codex entry actually landed under those, per the changelog rule.
Business rules — 3 honored · 3 not applicable
| Status | Rule | Note |
|---|---|---|
| ✔ Honored | Surfaces | The change extends the documented installer install/uninstall CLI surface for Codex without altering or breaking any other listed surface. |
| ✔ Honored | Changelog and release contract | The new CHANGELOG entry uses user-facing prose, real commands, and the agent name (Codex CLI), and no version number is pre-staged. |
| ✔ Honored | Installer contract | Two new parameterized tests (#1531) directly cover local install creation, the trust note, local uninstall reversing install, and global config being left untouched. |
| — Not applicable | Test selection (affected tests) | src/bin/codegraph.ts (the affected-test-selection command) is only listed as a downstream dependent, not modified by this diff, so test-selection logic is unaffected. |
| — Not applicable | Source strings must exclude interpolated template literals | The src/installer/index.ts changes in this diff are comment-text edits only and do not touch extractSourceStrings or any template-literal indexing logic. |
| — Not applicable | Runtime compatibility and releases | This change doesn't alter runtime/Node version requirements, dependency versions, or the release/bundling process. |
Full assessment
This PR extends the Codex CLI installer target to support --location=local (previously global-only), writing ./.codex/config.toml and a project-root AGENTS.md block, with an explicit note that Codex only applies project config in a trusted project. The AgentTarget interface and orchestrator comments are updated for consistency, and CHANGELOG.md gets a user-facing entry. Two new parameterized tests exercise local install and local uninstall end-to-end, matching the installer contract's reversible and sibling-preserving invariants.
Blast radius: 13 files affected beyond the diff · 157 symbols · 1 test file selected
Tests to run:
__tests__/installer-targets.test.ts
Closes #1531.
What this changes
CodexTargetwas the one target that forced a machine-wide install:supportsLocation('local')returnedfalse, so picking--location=localskipped Codex with a note saying it has no project-local config. This makes both locations work.--location=global(unchanged)~/.codex/config.toml~/.codex/AGENTS.md--location=local(new)<cwd>/.codex/config.toml<cwd>/AGENTS.mdWhy the old assumption was wrong
The file carried this comment:
Checked against
openai/codexrather than assumed:.codex/config.tomlis a first-class layer.codex-rs/config/src/loader/README.mddocuments the stack, precedence top-over-bottom:loader/mod.rslists three discovery routes for it —${PWD}/config.toml, a walk up parent directories for./.codex/config.toml, and$(git rev-parse --show-toplevel)/.codex/config.toml— implemented inload_project_layers()/discover_project_layers().mcp_serversis honored from that layer. The project layer is sanitized against a denylist (PROJECT_LOCAL_CONFIG_DENYLISTinloader/mod.rs) covering settings repo contents shouldn't get to choose — base URLs,model_provider/model_providers,notify,profile/profiles, otel.mcp_serversis not on it, so a project-scoped[mcp_servers.codegraph]survives into the merged config thatcore/src/mcp.rs(runtime_config(&Config)→configured_mcp_servers/effective_mcp_servers) consumes.It is not a recent addition. In-repo
.codex/config.tomllanded in feat: support in-repo .codex/config.toml entries as sources of config info openai/codex#8354 on 2025-12-22 — before the comment was written. So this is a correction, not a catch-up: the commit message and the new file comment both say "was never accurate" rather than "has changed."The one caveat, surfaced instead of hidden
All three project routes are documented as "loaded but disabled when untrusted." A local install can therefore be written perfectly and still do nothing until the user trusts the project. Rather than report a silent success,
install('local', …)returns a note:Why
AGENTS.mdsits at the project root, not under.codex/That's the file Codex reads for repo instructions, and it matches the local layout already used by the two other dual-location targets:
opencodewrites<cwd>/AGENTS.md, andgeminiwrites<cwd>/.gemini/settings.jsonplus a project-root<cwd>/GEMINI.md. Same asymmetry, same reasoning.What I deliberately did not do
The issue suggested optional version-gating (
supportsLocation('local')true only for Codex builds that ship the project layer, falling back to global otherwise). I left it out: the layer has existed since 2025-12, and an older Codex simply doesn't read the extra file — nothing breaks. Gating would mean shelling out tocodex --versionand parsing it, i.e. error handling for a case that no longer occurs. Happy to add it if you'd rather have the belt and braces.Tests
Two new cases in
__tests__/installer-targets.test.ts, both red against the previous implementation (expected false to be true) and green with this change:codex: local install writes ./.codex/config.toml and the project-root ./AGENTS.md block (#1531)— asserts the write layout,[mcp_servers.codegraph]in the local TOML, the CodeGraph block in the project-rootAGENTS.md, the trust note, and that~/.codex/config.tomlis not created.codex: local uninstall reverses the local install and leaves the global entry alone (#1531)— installs both locations, uninstalls local, asserts the global entry survives.No existing assertions were changed. The generic per-target contract suite (install / idempotent re-install / sibling preservation / uninstall / non-writing
printConfig) now picks upcodexatlocation=localautomatically, because it iteratessupportsLocation.Also updated
Two doc comments that used Codex as the example of a global-only target (
targets/types.ts,installer/index.ts) now name the Copilot CLI instead, so the stale claim doesn't survive in prose. Plus a user-facingCHANGELOG.mdentry under[Unreleased] → New Features, per thesrc/installer/rule in CLAUDE.md.