Verified 2026-09-22

AGENTS.md as an Engineering Contract: Codex's Enforced Rules

OpenAI's Codex repo treats AGENTS.md as a partial engineering contract that holds only where a mechanical CI check backs each rule. A production read.

Contents

    Most repositories treat agent instruction files as documentation. openai/codex treats its AGENTS.md as something closer to a build contract: a set of rules where the enforceable subset is backed by a custom dylint shipped as a release artifact, generated-artifact drift checks, Bazel compile-time failures, and insta snapshot gates. The instructive part is not that the rules are strict. It is the asymmetry: rules with a mechanical checker behind them hold, and rules with only prose behind them decay.

    Sources and scope: This article is based solely on the published openai/codex AGENTS.md, the repository’s GitHub CI workflow definitions, its issue tracker, and official OpenAI Codex documentation. No hands-on evaluation, benchmark, or side-by-side run was performed; enforcement claims are derived from those primary sources.

    AGENTS.md as an engineering contract, not a prompt suggestion

    An engineering contract is a rule set where violation produces a failing check rather than a correction request. openai/codex approaches that standard in specific, nameable places: a custom argument-comment dylint, lockfile and schema drift verification, Bazel build data declarations, and snapshot acceptance. The remaining rules are guidance whose enforcement is entirely social.

    That distinction matters more than the rules themselves. The repository is a large Rust workspace, and the openai/codex AGENTS.md covers module size, change size, lint conventions, sandbox constants, generated schemas, model-visible context construction, and test authoring. Each of those areas sits at a different point on the enforceability spectrum. Reading the file as a contract means asking, for every clause, what fails when someone ignores it.

    AGENTS.md is also a cross-tool convention documented at agents.md, not a Codex-only format. That portability is a strength for adoption and a weakness for enforcement: a convention shared across tools cannot assume any particular tool’s checker exists.

    Module size limits: 500/800 LoC plus extraction invariants

    The AGENTS.md target is stated as “Target Rust modules under 500 LoC, excluding tests,” with a hard escalation: “If a file exceeds roughly 800 LoC, add new functionality in a new module instead of extending the existing file unless there is a strong documented reason not to.” The gap between those two numbers is deliberate: 500 is the target, 800 is the tripwire.

    The file names its high-touch offenders explicitly: codex-rs/tui/src/app.rs, codex-rs/tui/src/bottom_pane/chat_composer.rs, codex-rs/tui/src/bottom_pane/footer.rs, codex-rs/tui/src/chatwidget.rs, and codex-rs/tui/src/bottom_pane/mod.rs. Naming specific files turns an abstract rule into an instruction an agent can act on when it decides where to place new code.

    The extraction invariant is the more sophisticated clause: “When extracting code from a large module, move the related tests and module/type docs toward the new implementation so the invariants stay close to the code that owns them.” This anticipates the failure mode where a refactor improves file size while scattering the knowledge needed to change the code safely. Separately, the file instructs contributors to “resist adding code to codex-core,” preferring an existing crate or a new workspace crate before touching codex-rs/core.

    None of these clauses has a checker. There is no CI job that fails when a file crosses 800 lines. The rule is real, the high-touch list is concrete, and enforcement is review pressure.

    Change-size budgets: 800/500-line staged review gates

    Change size gets a parallel budget under AGENTS.md: “Unless the change is mechanical the total number of changed lines should not exceed 800 lines. For complex logic changes the size should be under 500 lines. If the change is larger, explore whether it can be split into reviewable stages and identify the smallest coherent stage to land first.”

    The escape hatch is the point. A mechanical change, meaning a rename or a generated-artifact refresh, is exempt. The rule targets comprehension cost in review, not aesthetics. The instruction to name “the smallest coherent stage to land first” converts an over-budget change into a sequencing problem rather than a rejection.

    This is the classic unenforceable clause. Diff size is trivially measurable, and yet a rule that fails only on human judgment cannot be a gate. It competes with module-size limits for the same scarce reviewer attention, and there is no mechanical signal that distinguishes an 850-line mechanical change from an 850-line logic change. The AGENTS.md author implicitly acknowledges this by making the rule conditional on a category the author must self-assess.

    The argument-comment lint: a custom dylint shipped from CI

    This is the strongest clause in the file, because it is the one rule with a purpose-built, distributed checker. The convention: use an exact /*param_name*/ comment before opaque literal arguments, meaning booleans, None, and numeric literals passed positionally. The comment text must exactly match the callee signature’s parameter name.

    There is one exemption: a method’s sole non-self argument when the method and parameter names match, such as .enabled(false) for fn enabled(&self, enabled: bool). The file also says not to add these comments for string or char literals unless the comment adds real clarity, which keeps the convention from metastasizing into noise.

    What makes this a contract rather than a style note is the distribution model. The argument-comment-lint release workflow builds prebuilt dylint binaries pinned to CARGO_DYLINT_VERSION 5.0.0 and DYLINT_LINK_VERSION 5.0.0, producing an artifact named libargument_comment_lint@nightly-2025-09-18-<target> for aarch64-apple-darwin (runner macos-15-xlarge) and x86_64-unknown-linux-gnu (runner ubuntu-24.04).

    That is the key architectural pattern: enforcement is decoupled from a contributor’s local toolchain. A contributor runs just argument-comment-lint, which the file notes is “powered by Bazel,” with incremental invocations expected under 15 seconds. But the file also states plainly that “CI checks all three platforms, which the local run does not.” Local convenience is a courtesy; the gate is remote and prebuilt. A rule whose checker must be compiled from source on each machine will be skipped on slow machines. A rule whose checker arrives as a pinned binary will not.

    Sandbox-aware test skipping: environment-variable invariants

    The sandbox clause is unusual because it instructs agents to do nothing. AGENTS.md states: “Never add or modify any code related to CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR or CODEX_SANDBOX_ENV_VAR.”

    The file explains why. CODEX_SANDBOX_NETWORK_DISABLED=1 is set whenever the shell tool runs, and CODEX_SANDBOX=seatbelt is set on children spawned under Seatbelt via /usr/bin/sandbox-exec. Tests that would be impossible under those conditions early-exit by checking those environment variables. An agent that finds an early-return and “fixes” it by removing the guard is not improving the test; it is breaking a deliberate escape hatch and creating failures in sandboxed runs.

    This is a negative rule, and negative rules are the hardest to enforce mechanically because correctness is the absence of a change. A lint could flag touched lines in those constants, but nothing in the published workflow set does. The clause works because it is short, absolute, and placed where an agent reading the file before editing will encounter it. It also illustrates a broader contract principle: some rules exist to prevent well-intentioned optimization.

    Schema and lockfile drift: generated artifacts as gates

    Generated artifacts give AGENTS.md its most directly machine-verifiable clauses, because drift checks need no judgment at all. The file requires: “If you change ConfigToml or nested config types, run just write-config-schema to update codex-rs/core/config.schema.json.” And: “If you change Rust dependencies (Cargo.toml or Cargo.lock), run just bazel-lock-update from the repo root to refresh MODULE.bazel.lock, and include that lockfile update in the same change. CI verifies lockfile drift.”

    App-server work follows the same shape: just write-app-server-schema plus the --experimental variant regenerate fixtures, validated with just test -p codex-app-server-protocol.

    The Bazel clause is the subtle one: “Bazel does not automatically make source-tree files available to compile-time Rust file access. If you add include_str!, include_bytes!, sqlx::migrate!, or similar build-time file or directory reads, update the crate’s BUILD.bazel (compile_data, build_script_data, or test data) or Bazel may fail even when Cargo passes.” Cargo and Bazel have divergent models of file visibility. A change that passes locally under Cargo can fail in CI under Bazel. That divergence is itself a mechanical gate, and it is exactly the kind of rule that cannot be trusted to prose: the failure appears in a build system the contributor may not have run.

    Model-visible context and test-authoring constraints

    Context construction gets numbered, reviewable constraints. AGENTS.md requires: “1. No history rewrite - the context must be built up incrementally. 2. Avoid frequent changes to context that cause cache misses. 3. No unbounded items - everything injected in the model context must have a bounded size and a hard cap. 4. No items larger than 10K tokens. 5. Highlight new individual items that can cross >1k tokens as P0. These need an additional manual review.” It adds that injected fragments must be defined as structs in core/context implementing the ContextualUserFragment trait.

    Several of these are mechanically checkable in principle. Rule 4 has a number. Rule 3 has a cap. Rule 1 is structurally discernible. Yet the enforcement mechanism described is a P0 flag and human review, which means the context rules live in the same tier as module size: precise, quantified, and dependent on reviewers noticing.

    Test authoring follows a similar pattern. Prefer integration tests over unit tests for agent changes, using core/suite and test_codex. “Features that change the agent logic MUST add an integration test.” Unit tests belong in a dedicated *_tests.rs file. UI-affecting changes MUST include insta snapshot coverage, via a workflow of just test -p codex-tui, then cargo insta pending-snapshots -p codex-tui, then reviewing *.snap.new, then cargo insta accept -p codex-tui. Use just test, never cargo test. Run just fmt after code changes without asking.

    The snapshot requirement is a genuine gate because unreviewed snapshots surface as pending files. The just wrapper requirement is closer to convention, enforceable only by CI configuration and review. The repository’s CI surface includes blocking-ci.yml, postmerge-ci.yml, repo-checks.yml, blob-size-policy.yml, codespell.yml, cargo-deny.yml, rust-ci.yml, rust-ci-full.yml, rust-ci-full-nextest-platform.yml, python-sdk-build.yml, and sdk.yml. Notably, blob-size-policy.yml runs openai/fence in audit mode and checks out with fetch-depth: 0, inspecting the proposed diff on pull requests and only the landed commit range on main pushes.

    Four tiers of enforcement and the 32 KB boundary

    Rules in AGENTS.md sort into four tiers by what fails when they are violated. Tier one produces a build or CI failure with no human in the loop: lockfile drift, schema drift, Bazel compile_data omissions, cargo-deny, codespell. Tier two produces a failing check that requires building or installing a tool: the argument-comment dylint, snapshot tests. Tier three is quantified but human-judged: the 500/800 module limits, the 800/500 change budgets, the 10K token cap. Tier four is pure convention: just fmt, just test over cargo test, the *_tests.rs placement rule.

    Rule declared in AGENTS.mdMechanical enforcementWhat happens when only prose backs it
    Module size under 500 LoC, extract past ~800 LoCNone; named high-touch files onlyLarge files keep growing at review discretion
    Change size under 800 lines, 500 for logicNoneOver-budget diffs land as single reviews
    /*param_name*/ comments on opaque literalsCustom dylint, prebuilt per-platform binaries in CIConvention erodes to inconsistent local linting
    ConfigToml schema and MODULE.bazel.lock freshnessjust write-config-schema, just bazel-lock-update, CI drift verificationStale generated artifacts merge silently
    Sandbox env-var constants untouchedNone; absolute negative ruleAgents “fix” early-exit guards and break sandboxed runs
    Bazel build data for include_str!/sqlx::migrate!Bazel compile failure even when Cargo passesCargo-local success diverges from CI failure
    Model context size caps and ContextualUserFragment structsP0 flag plus manual reviewUnbounded context growth and cache-miss churn

    The boundary condition is size. Codex CLI silently truncates AGENTS.md at roughly 32 KB, about 600 to 800 lines, with no warning, so instructions near the end are never sent to the model at all, per openai/codex issue #13386, open and filed 2026-03-03 against Codex CLI 0.108.0-alpha.3 with gpt-5.3-codex on macOS, and openai/codex issue #43075, open since 2026-09-05. The underlying knob is project_doc_max_bytes in config.toml.

    This inverts the usual instruction-file calculus. More rules make the file less enforceable, because the tail is discarded without notice. Related open issues show the surrounding surface is still settling: issue #37704 on /status reporting Agents.md: <none> after /init, issue #43309 on configurable reload policies including freeze, issue #44191 on scanning unsafe paths for AGENTS.md, and issue #41704 on no longer respecting AGENTS.md chains. The Codex AGENTS.md guide and the canonical AGENTS.md are the authoritative references for the format.

    The design lesson generalizes past this repository. When you write an instruction file for agents, the clauses that survive are the ones wired to a failure. Everything else is a preference that will be traded away under pressure, and a preference past the truncation boundary is not even read.

    FAQ

    Does the argument-comment lint run when a contributor cannot build it locally?

    Yes. Enforcement does not depend on a local build. The argument-comment-lint release workflow publishes prebuilt dylint binaries pinned to CARGO_DYLINT_VERSION 5.0.0 and DYLINT_LINK_VERSION 5.0.0 for aarch64-apple-darwin and x86_64-unknown-linux-gnu. The AGENTS.md text notes that just argument-comment-lint is Bazel-powered with incremental runs expected under 15 seconds, but also that CI checks all three platforms while the local run does not. Shipping the checker as a pinned artifact removes toolchain drift as an excuse.

    What happens if an AGENTS.md author writes past the project_doc_max_bytes limit?

    The file is silently truncated by Codex CLI at roughly 32 KB, about 600 to 800 lines, and instructions near the end are never sent to the model. There is no warning, per openai/codex issue #13386 and openai/codex issue #43075, both open. The relevant knob is project_doc_max_bytes in config.toml. The practical consequence is that rule ordering is a correctness concern: a clause past the cut behaves as if it were never written.

    Is AGENTS.md a Codex-only convention?

    No. AGENTS.md is a cross-tool convention documented at agents.md and adopted across multiple agent products, not a Codex-specific format. OpenAI’s own reference is the Codex AGENTS.md guide. The repository instance is openai/codex’s AGENTS.md (canonical). Portability helps adoption, but it also means no single tool’s checker can be assumed, which is why the enforceable clauses here are backed by repository-local CI rather than format-level guarantees.

    • ToolBrain — tool reviews, LLM comparisons, and AI workflow guides

    Cross-links automatically generated from CodeIntel Log.