What a context file for an agent is
A context file is a short file in a repository that a coding agent reads before every task. It says how to run the tests, which conventions the team keeps and what must not be touched.
Without it, the agent learns your project again on every task and guesses the rest. A good context file is short, true and safe, and a script can check all three.
By the end of this lesson
Note
I can write a short context file that tells a coding agent how to work in a repository.
Key words
| Word | What it means | Example |
|---|---|---|
| Context file | standing notes an agent reads before every task | AGENTS.md |
| Convention | a rule the team always follows | |
| Command | a line you type to run something | npm run test |
| Protected path | a file or folder the agent must not change | |
| Line limit | the most lines the file may have |
The cells students fill in are left blank in this preview.
A context file says what the code cannot
The code shows what the program does. It does not show how the team works. A context file holds four kinds of line: the commands to test and run, the conventions, the protected paths and where things live.
opencode, the agent in this lab, reads a file named AGENTS.md at the top of the project. Other agents read files with other names.
Worked example: lines to keep or cut
| Line | Keep or cut | Why |
|---|---|---|
| Run the tests with npm run test | Keep | A command the agent needs every time |
| Prices are whole pence, never decimals | Keep | A convention the code does not say |
| Do not touch: data/prices.json | Keep | A protected path |
| The shop began in 2019 | Cut | History the agent does not need |
| total adds up the basket | Cut | The code already says it |
| API_KEY=abc123 | Cut | A secret never goes in the repository |
The cells students fill in are left blank in this preview.
Short, true and safe
The agent reads every line on every task, so each line costs time and space. Set a line limit, such as 40 lines, and keep to it.
A line that is no longer true is worse than no line. A command that does not exist sends the agent the wrong way. A path that has moved protects nothing.
A common mistake
Warning
Many people think a longer context file gives better work. A rule on line 300 is easy to miss, for an agent and for a person. Keep the file short, and check that every command and path in it still exists.
Write the model helper, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the model helper, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the sample repository and checker, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const fs = require(\"fs\");' 'const { ask } = require(\"./model\");' 'const NL = String.fromCharCode(10);' 'fs.mkdirSync(\"ctx-demo/data\", { recursive: true });' 'fs.mkdirSync(\"ctx-demo/src\", { recursive: true });' 'const pkg = { name: \"shop\", scripts: { test: \"node --test\", lint: \"node lint.js\" } };' 'fs.writeFileSync(\"ctx-demo/package.json\", JSON.stringify(pkg, null, 2));' 'fs.writeFileSync(\"ctx-demo/data/prices.json\", JSON.stringify({ apple: 35, juice: 85 }));' 'fs.writeFileSync(\"ctx-demo/src/shop.js\", \"module.exports = {};\" + NL);' > context.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the sample repository and checker, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const draft = [\"# Notes for coding agents\", \"Run the tests with npm run test\",' ' \"Check the style with npm run lint\", \"Build the site with npm run build\",' ' \"Prices are whole pence, never decimals.\", \"Do not touch: data/prices.json\",' ' \"Do not touch: secrets/keys.txt\", \"API_KEY=abc123 for the test server\"];' 'fs.writeFileSync(\"ctx-demo/AGENTS.md\", draft.join(NL) + NL);' >> context.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the sample repository and checker, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const LIMIT = 40;' 'const lines = fs.readFileSync(\"ctx-demo/AGENTS.md\", \"utf8\").trim().split(NL);' 'const scripts = JSON.parse(fs.readFileSync(\"ctx-demo/package.json\", \"utf8\")).scripts;' 'const found = [];' 'if (lines.length > LIMIT) found.push(\"too long: \" + lines.length + \" lines\");' 'lines.forEach(function (line, at) {' ' const words = line.split(\" \");' ' const i = words.indexOf(\"run\");' ' const where = \"line \" + (at + 1) + \": \";' ' if (i > 0 && words[i - 1] === \"npm\" && scripts[words[i + 1]] === undefined) found.push(where + \"no script \" + words[i + 1]);' ' const path = line.slice(14);' ' if (line.indexOf(\"Do not touch: \") === 0 && fs.existsSync(\"ctx-demo/\" + path) === false) found.push(where + \"no such path \" + path);' ' if (line.indexOf(\"KEY=\") >= 0 || line.toLowerCase().indexOf(\"password\") >= 0) found.push(where + \"looks like a secret\");' '});' >> context.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the sample repository and checker, part 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' console.log(\"AGENTS.md: \" + lines.length + \" lines, limit \" + LIMIT);' ' found.forEach(function (one) { console.log(one); });' ' const q = \"In one short sentence, why should a context file for a coding agent stay short?\";' ' const sample = \"The agent reads every line on every task, so a long file buries the rules that matter.\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 60);' ' console.log(\"model said: \" + reply.text);' '}' 'main();' >> context.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the context checker
docker exec -w /home/node/project agent node context.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
What the Run prints
| Line | What it shows |
|---|---|
| AGENTS.md: 8 lines, limit 40 | The draft is short enough |
| line 4: no script build | package.json has no build script |
| line 7: no such path secrets/keys.txt | The protected path does not exist |
| line 8: looks like a secret | An invented key, which must come out |
| model said: ... | A sentence from the lab. A Recorded sample line if no model answers |
The cells students fill in are left blank in this preview.