An AGENTS.md file can be perfectly clear and completely wrong.
It says tests live in test/, but the directory moved to packages/api/spec/. It tells an agent to run npm test, but the project switched to pnpm. It warns not to edit generated files, while the generator named in the same paragraph disappeared three months ago.
Humans learn to distrust stale documentation. Coding agents are more literal: they may confidently follow the obsolete path.
The fix is not a longer prompt. Put a small, machine-readable contract inside the document and test the claims that can be tested.
Separate guidance from invariants
Most repository instructions contain three kinds of information:
| Kind | Example | How to maintain it |
|---|---|---|
| judgment | “Prefer the smallest change that preserves the public API” | review by humans |
| fact | “Integration tests are in test/integration” |
verify the path exists |
| procedure | “Run npm run check before submitting” |
execute it in CI |
Do not try to turn judgment into a brittle rule. Do turn factual and procedural claims into checks.
Add a fenced block to AGENTS.md:
## Repository contract
```agent-contract
{
"requiredPaths": ["src", "test", "package.json"],
"checks": [
{ "name": "tests", "argv": ["npm", "test", "--", "--runInBand"] },
{ "name": "types", "argv": ["npm", "run", "typecheck"] }
],
"reviewAfter": "2026-10-01"
}
```
This block is deliberately boring. JSON has no comments or clever interpolation. Commands are argument arrays, not shell strings. The review date makes untestable prose visible before it becomes archaeology.
A checker small enough to audit
Save this as scripts/check-agent-contract.mjs:
import { access, readFile } from "node:fs/promises";
import { spawn } from "node:child_process";
const documentPath = process.argv[2] ?? "AGENTS.md";
const markdown = await readFile(documentPath, "utf8");
const match = markdown.match(/```
{% endraw %}
agent-contract\s*\n([\s\S]*?)\n
{% raw %}
```/);
if (!match) throw new Error(`${documentPath}: missing agent-contract block`);
const contract = JSON.parse(match[1]);
const allowed = new Set(["npm", "pnpm", "yarn", "bun", "node"]);
let failures = 0;
for (const path of contract.requiredPaths ?? []) {
try {
await access(path);
console.log(`PASS path ${path}`);
} catch {
failures++;
console.error(`FAIL missing path ${path}`);
}
}
for (const check of contract.checks ?? []) {
if (!Array.isArray(check.argv) || check.argv.length === 0) {
failures++;
console.error(`FAIL ${check.name}: argv must be a non-empty array`);
continue;
}
const [command, ...args] = check.argv;
if (!allowed.has(command)) {
failures++;
console.error(`FAIL ${check.name}: executable ${command} is not allowed`);
continue;
}
const code = await new Promise((resolve, reject) => {
const child = spawn(command, args, { stdio: "inherit", shell: false });
child.once("error", reject);
child.once("exit", resolve);
});
if (code === 0) console.log(`PASS check ${check.name}`);
else {
failures++;
console.error(`FAIL check ${check.name}: exit ${code}`);
}
}
if (contract.reviewAfter) {
const reviewAt = Date.parse(`${contract.reviewAfter}T00:00:00Z`);
if (!Number.isFinite(reviewAt)) throw new Error("reviewAfter must be YYYY-MM-DD");
if (Date.now() > reviewAt) {
failures++;
console.error(`FAIL contract review overdue since ${contract.reviewAfter}`);
}
}
process.exitCode = failures === 0 ? 0 : 1;
Run it from the repository root:
node scripts/check-agent-contract.mjs AGENTS.md
The complete version used for this article also prints passing review dates. I tested it with Node.js 22.22.3: an existing path plus node --version exited 0; a missing path and an unapproved executable exited 1.
Why shell: false matters
Documentation is input. A pull request can change it.
Passing a documentation string to exec() or spawn(..., { shell: true }) quietly turns prose into shell authority. A command such as npm test && curl ... is no longer one test command. Argument arrays and shell: false remove shell operators, substitutions, and redirections from this format.
The executable allowlist is a second boundary. Customize it for the repository, keep it narrow, and require review when it changes. This is not a general-purpose task runner.
There is still an important limitation: an allowed command such as npm test runs repository-controlled code. Execute the checker with the same isolation, credentials, network policy, and approval rules you already apply to pull-request CI. The checker prevents accidental shell interpretation; it does not make untrusted code safe.
Make drift fail where it starts
Add the checker to the same pull-request workflow that validates code:
name: repository-contract
on: pull_request
jobs:
verify-agent-guidance:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: node scripts/check-agent-contract.mjs AGENTS.md
For a production repository, pin third-party actions to full commit SHAs. Tags keep this example readable but are mutable references.
Now the useful failure happens in the pull request that deletes test/, renames a script, or lets the review date expire. The author can update the instruction and the implementation together.
What belongs outside the block
Keep the contract small. It should not become a second package manager or a homemade policy language.
Good candidates:
- paths an agent must inspect before editing;
- the repository's existing format, type, test, and build commands;
- generated-file boundaries that can be checked by an existing script;
- a date for reviewing prose that cannot be executed.
Poor candidates:
- style advice that needs context;
- secrets or environment-specific URLs;
- destructive setup and deployment commands;
- commands copied from third-party issues without repository review.
Start with three facts that have actually drifted before. A ten-line contract that fails usefully is better than a hundred-line schema nobody owns.
Where this applies to coding platforms
The public MonkeyCode repository describes AI task management, project requirements, managed development environments, team collaboration, and private deployment. Repository-level contracts are relevant to that category because instructions need to remain true whichever model or workspace executes the task. This checker is tool-independent; I did not test a MonkeyCode integration or inspect its internal instruction handling.
Disclosure: I contribute to the MonkeyCode project. The product description above comes from its public repository; the executable contract and test are independent.
The broader rule is simple: prose explains intent, while automation protects facts. When both live in the same document, an agent gets guidance that is easier for humans to trust—and harder for repository drift to quietly falsify.
Top comments (1)
Executable contracts beat advice because agents are very good at sounding compliant. If a rule matters, the system should be able to test or enforce it: file boundaries, forbidden commands, required checks, output formats, or approval gates. Otherwise AGENTS.md becomes a style guide with no teeth.