
Put Your Opinions to Work with AGENTS.md and Keep Them Safe
I’ve seen enough social media debates to understand that software engineering is the most opinionated profession in the world. Tailwind CSS either ruins your markup or saves your team. Agile died a decade ago, or it never actually started. Microservices are dead or completely necessary to scale. To be honest, I have always found it all absurd. Don’t people get tired of having the same arguments every other week?
While I try to avoid the fights, I am no exception when it comes to having strong engineering opinions. I believe static type systems should make invalid states impossible to express, so a boolean never replaces a state machine and magic strings never replace real types. I believe type safety should extend to configuration, which is why I always use Pkl rather than bury myself in endless, error-prone YAML files. I believe property-based testing finds the bugs your manual test scenarios were never going to find. I believe Effect-Oriented Programming is essential to write code that is easier to reason about, which is why our TypeScript code uses Effect and our Kotlin code gets very close to true effects with context parameters.
Whatever the opinions of you and your team when it comes to building great software, the challenge has always been enforcing them. It was a combination of static analysis and code review. Both are problematic. It takes time to find good static analysis tools and customize them to your specific standards, so it’s common to take a “good enough” approach. Code review is a bottleneck because reviewers are busy doing their real jobs, so reviews are typically rubber stamps that allow all sorts of violations of your coding standards.
AI changes that. Specifically, the AGENTS.md file changes that. Write an opinion down once, and every coding agent applies it everywhere. Well, almost.
What is AGENTS.md?
AGENTS.md is a Markdown file checked into your repository that customizes how AI coding agents behave. Agents load it right after the system prompt, and it sits at the top of the conversation on every single
request, which makes it a configuration layer between the model’s base instructions and your actual code.
On December 9, 2025, the Linux Foundation announced the formation of the Agentic AI Foundation, and OpenAI donated AGENTS.md to the AAIF alongside Anthropic’s Model Context Protocol and Block’s goose.
I wrote about why that matters when AAIF named me an inaugural Ambassador. The adoption numbers are clear. More than 60,000 repositories, almost 24,000 GitHub stars, and
countless tools use AGENTS.md. The people (and their coding agents) have spoken.
AGENTS.md is where you put your opinions, or the consensus opinions of your team at work, to teach coding agents the ground rules for the kind of code base you want. But there are a few gotchas you
should be aware of.
AGENTS.md Discoverability
There is no unified approach to using AGENTS.md. Zed puts it seventh(!) in the preference hierarchy behind other non-standard options like .rules and .cursorrules, so a stray .cursorrules a teammate committed two years ago silently wins. Codex caps combined
instruction bytes at 32 KiB and truncates the rest. Claude Code reads CLAUDE.md and ignores AGENTS.md entirely, which surprises a lot of teams at first.
The fix in all these situations is trivial if a bit annoying. Make AGENTS.md the source of truth for coding patterns and practices and then use a specific agent’s syntax to import AGENTS.md into whichever other files rank higher in the agent’s
hierarchy. For example, you can instruct Claude Code to use your AGENTS.md with their @ import syntax:
# CLAUDE.md
@AGENTS.md
# Claude-specific instructions
Claude Code expands the @ import at session start and loads the shared file as if it were inline. A symlink does the same job:
ln -s AGENTS.md CLAUDE.md
Balancing the Instruction Budget
AGENTS.md loads on every request rather than on demand. On one level, this is good because your opinions should apply to all the code you write. The problem is that not every
task demands every opinion. My Kotlin code does not need to know that my TypeScript code uses Effect, so coding agents waste tokens understanding my TypeScript
opinions when they need to update a Spring Boot repository.
Matt Pocock has a term for this: the instruction budget. According to Matt, frontier AI models follow roughly 150 to 200
instructions with consistency. Smaller models follow fewer. The specific numbers don’t
matter, and they vary and evolve across the matrix of vendors and models anyway. The key is to understand that there is an art to writing a good AGENTS.md that is compact but thorough. Unfortunately,
a study of 2,303 real agent context files across 1,925 repositories found that they read like
complex configuration code rather than documentation, growing through frequent small additions that nobody ever prunes. The same
study found that developers pile in build commands, implementation details, and architecture while security surfaces in a mere
14.5% of files. People clearly went off on their tech opinions in these files unrestrained by the character limits of social media.
AGENTS.md demands a balance. It should be long enough to capture your preferences but short enough to influence coding agents meaningfully. First, take advantage of model training. Coding agents already
know how to compile Kotlin and typecheck TypeScript. Don’t bother adding commands straight out of a README on GitHub to your AGENTS.md. Add only unique commands. For example, I have
AI agents written in Kotlin, but my AGENTS.md doesn’t have the standard Gradle build command that agents already know. It does have the Gradle command that runs the custom
build task that composes other Gradle tasks to spin up Docker Compose to run the whole agent application locally with databases, cloud mocks, and all the other infrastructure. That enforces
my opinions and creates a setup specific to my product. That’s perfect for AGENTS.md.
In general, handcraft your AGENTS.md like an Etsy product rather than let an agent generate it because agents are naturally verbose unless you configure them to be concise.
Give it a short project description, the “pillars” of the code base representing your foundational patterns and practices at a high level,
and bespoke commands. Resist the urge to enumerate file structures, which change constantly, creating a maintenance headache and wasting tokens on wild goose chases.
Progressive Disclosure
The opinionated depth you trim from AGENTS.md is still important and belongs somewhere. You need a documentation tree the agent pulls in on demand only when necessary, which is the
developer-level “culture of context” that agents need to thrive. Create context files representing the various concerns across your product like UI, testing, and
observability. Then link to them from AGENTS.md so that it functions like an index. For example, you could link to an OBSERVABILITY.md from AGENTS.md.
Then when you give your coding agent an observability task, it will load AGENTS.md first and recognize that it needs to load OBSERVABILITY.md for detailed context while
ignoring FRONTEND.md, TESTING.md, and whatever other context files you have expressing your opinions. This focuses the agent on the context it needs while
limiting bloat on topics irrelevant to the task at hand.
Here is a sample AGENTS.md file that balances the instruction budget while expressing key opinions including what not to do.
# AGENTS.md
## Project
- Package manager: `npm`
- Full build: only when requested
- Before changing code, inspect relevant existing patterns and docs.
## Rules
### Required
- Never hardcode credentials, hostnames, or URLs. Runtime configuration comes through Pkl.
- Model invalid states out of existence with types. Do not use booleans as state machines or nullable fields to represent meaningful absence.
- Use typed error values (`Either`, `Result`, or Effect's error channel). Do not throw across module boundaries.
- Keep the functional core pure: no clock, randomness, or I/O. Inject these dependencies at the boundary.
- Add behavior tests before implementation for new behavior. For refactors, type-only changes, and trivial changes, use judgment.
### Conventions
Follow these docs when relevant:
- TypeScript / Effect: `docs/TYPESCRIPT.md`
- Testing / Kotest / fast-check: `docs/TESTING.md`
- Pkl configuration: `docs/CONFIG.md`
## Permissions
### No approval needed
- Read files
- Run tests
- Run `npm typecheck`
- Lint touched files
### Ask first
- Add or update dependencies
- Change CI
- Change infrastructure
- Rename or remove a public export
- Make changes outside the task's scope
### Never
- Commit secrets
- Force-push
- Rewrite prose in documents I authored
This file has just 50 lines, and every one of them expresses core values that are important to me in this code base like Effect conventions, property-based testing patterns, and Pkl configuration.
Details on specific concerns like TESTING.md live elsewhere, and they cost nothing until the agent needs them for a testing task.
Notice also the split among what an agent must always follow, what it should follow when the topic comes up, and what it must never do on its own. These categories help agents “think” like a senior engineer focused on higher-priority concerns first.
You also have the option of implicit progressive disclosure through multiple AGENTS.md files across a monorepo or subprojects so that each AGENTS.md file is focused on its particular subtree.
Consider something like this:
repo/
├── AGENTS.md
├── docs/
│ ├── TYPESCRIPT.md
│ ├── TESTING.md
│ └── CONFIG.md
├── apps/
│ ├── web/
│ │ └── AGENTS.md # web-specific rules
│ └── api/
│ └── AGENTS.md # API-specific rules
└── packages/
├── core/
│ └── AGENTS.md # core-specific architecture
└── config/
└── AGENTS.md
In this case each lower-level AGENTS.md adds the rules its own subtree needs, and the file closest to the code an agent edits takes precedence. The advantage is that you do not have to create explicit
links to bespoke Markdown files because coding agents understand what AGENTS.md is, which is the beauty of the standard. There are two key disadvantages. You can take things too far
and create a maintenance nightmare across folders, and you multiply the compatibility concerns across agents because of the different approaches they take to consuming AGENTS.md.
I personally prefer keeping things at the top level and using lower level AGENTS.md files sparingly. That’s your call.
Speaking of maintenance, I said earlier that there is an art to crafting a good AGENTS.md. Any art takes time, and you never get it right the first time. OpenAI’s own guidance for Codex says that when an agent
repeats a mistake, ask it for a retrospective and have it update AGENTS.md. When your coding agents fail you, take the time to work with them to curate AGENTS.md so it (or they) gets better and better
over time.
Then Add Skills
AGENTS.md is always resident to express your opinions and high-level patterns for your code base. An agent skill loads only when its description triggers to apply your opinions to specific, repeatable tasks. They complement each other.
For example, the sample AGENTS.md articulates my opinions that errors are values, which is an important principle in Effect-Oriented Programming, and that I have a preference for Pkl configuration and
property-based testing. A complementary Agent Skill can put these ideas into practice:
---
name: effect-service
description: Scaffold a new Effect-TS service with its Layer, tagged error types,
a Pkl config schema, and a fast-check property test. Use when adding a service
under src/services/. Do NOT use for React components, for one-off scripts, or for
modifying a service that already exists.
---
Philipp Schmid and others have shown that the description metadata is the key trigger mechanism for firing skills when appropriate. Skills demand progressive disclosure in their own right.
The description expresses the purpose of the skill, when to use it, and the negative cases as succinctly as possible while leaving the details to the body of the skill and its optional artifacts like assets and scripts.
Encoding your opinions in AGENTS.md and complementing them with Agent Skills to apply them in practice is a powerful combination.
Protect Your Opinions
If you borrow content from the Internet like open source AGENTS.md files or Agent Skills as starters for your own, be careful. They can be Trojan horses, no Odyssey pun intended, for prompt injection attacks
on your own machine that can leak secrets to attackers and cause many other problems.
Even beyond that, any file with the power of AGENTS.md is a standing vulnerability. NVIDIA’s AI Red Team published a
working proof of concept
that shows exactly how it breaks. The attack runs in four steps:
- You add a dependency. You may well have audited that package, but nobody reads the source of every transitive package underneath it. The attack assumes precisely that.
- Your build runs, and the dependency executes as part of it. NVIDIA used a Go library whose exported function checks for the
CODEX_PROXY_CERTenvironment variable, so the payload fires only inside a Codex session and stays dormant everywhere else. - It then writes an untracked
AGENTS.mdinto your working directory. The text it plants claims absolute authority and orders the agent to supersede anything you type. - The agent reads the new file and obeys it inside the session already running.
Step 4 is the attack. Nobody needs to trick you. Nobody needs to jailbreak the model. The attacker writes a file your agent already treats as policy, and no human reviews it at read time. The injection also lands mid-session, so it takes effect without a restart. Restarting doesn’t help either because the file stays on disk.
Pillar Security demonstrated an even nastier variant they named the Rules File Backdoor. They hide the injected directives inside invisible Unicode, using zero-width joiners and bidirectional text markers, so the poisoned file looks identical to the clean one in your editor even though the bytes differ. They disclosed it to Cursor and GitHub in early 2025, and GitHub responded that May with warnings when a file contains hidden Unicode text. A warning helps at review time on one platform. Your editor, your local diff, and your agent still read the file straight through, so assume your own tooling misses this entirely.
Let’s step back and understand all the threat vectors to AGENTS.md:
- Install-time write. A dependency may execute code while you install it and write its own malicious
AGENTS.md. Every programming ecosystem opens this door in its own way: a Node lifecycle script, a Python PEP 517 backend that runs arbitrary code for any source distribution, or a third-party Gradle plugin that runs at configuration time. - Runtime write. A package that executes later, in your dev server or CI build, does the same thing at a moment without install-time protection. This is NVIDIA’s proof of concept.
- Borrowed content. You copy an open-source
AGENTS.mdor Agent Skill that arrives poisoned, which is the Trojan horse I mentioned. - Inbound change. A clone, a merge, or a teammate’s pull request carries it in.
Meanwhile, invisible Unicode hides whichever of the four above an attacker picked, which is what makes Pillar’s variant particularly insidious.
The good news is that git can help us with both NVIDIA’s and Pillar’s attacks:
- Ask
gitwhat changed. Git already stores the sanctioned content of every trackedAGENTS.mdand flags any file that nobody committed. WHen NVIDIA’s attack created either a newAGENTS.mdin your working directory or an appended one, runninggit statusflags it. - Create an allowlist of characters that includes tab, carriage return, and printable ASCII. Reject everything else. Pillar hides its directives in zero-width joiners,
bidirectional overrides, tag characters, and variation selectors. An allowlist helps because none of them
lands between
0x20and0x7E. The rule holds no list of dangerous code points, so Unicode can ship a thousand new invisible characters next year and the test still catches all of them.
Now I know what you are thinking. This is a lot of work for a vulnerability you feel you do not need to worry about. You might be right. If you are a solo developer who is very careful about how you code with agents, your risk might be small. But if you work heavily with agent swarms like Hermes and OpenClaw, you’re lax on policy enforcement, and you rubber stamp every action your agents take, the risk is much greater than you realize. I personally like to use my build tools to help me with this sort of thing.
Here is an approach in Node. Wire it into package.json as "audit:agents": "node scripts/audit-agents.mjs", then chain it onto the
build with "build": "tsc -b && npm run audit:agents". The order is important because NVIDIA’s attacks hits while the build
runs, so an audit that goes first is pointless.
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
// every file your agents load as policy, at every depth
const NAMES = ["AGENTS.md", "CLAUDE.md", ".cursorrules", "SKILL.md"];
const PATHS = NAMES.map(n => `:(glob)**/${n}`);
// -z stops git from escaping the paths most likely to be poisoned
const git = (...a) => execFileSync("git", [...a, "-z", "--", ...PATHS], { encoding: "utf8" })
.split("\0").filter(Boolean);
// the first line holding a character outside tab, carriage return, and printable ASCII
const badLine = f => readFileSync(f, "utf8").split("\n")
.findIndex(l => !/^[\t\r\x20-\x7E]*$/.test(l)) + 1;
const failures = git("status", "--porcelain", "-uall").map(l => `drift ${l.trim()}`);
// a deleted file already shows up as drift, so skip paths that no longer exist
for (const f of git("ls-files").filter(existsSync)) {
const n = badLine(f);
if (n) failures.push(`hidden char ${f}:${n}`);
}
failures.forEach(f => console.error(f));
if (failures.length === 0) console.log("Agent instruction audit passed.");
process.exit(failures.length ? 1 : 0);
Here is the analogous Gradle task in Kotlin:
import java.io.ByteArrayOutputStream
import java.io.File
import javax.inject.Inject
abstract class AuditAgentFiles : DefaultTask() {
@get:Inject abstract val execOps: ExecOperations
@get:Internal abstract val repoRoot: DirectoryProperty
private fun git(vararg args: String): List<String> {
// every file your agents load as policy, at every depth
val paths = listOf("AGENTS.md", "CLAUDE.md", ".cursorrules", "SKILL.md").map { ":(glob)**/$it" }
val out = ByteArrayOutputStream()
execOps.exec {
workingDir(repoRoot.get().asFile)
// -z stops git from escaping the paths most likely to be poisoned
commandLine(listOf("git") + args + "-z" + "--" + paths)
standardOutput = out
}
return out.toString("UTF-8").split('\u0000').filter { it.isNotBlank() }
}
// the first line holding a character outside tab, carriage return, and printable ASCII
private fun badLine(f: File) = f.readText().lines()
.indexOfFirst { l -> l.any { it != '\t' && it != '\r' && (it < ' ' || it > '~') } } + 1
@TaskAction
fun audit() {
val root = repoRoot.get().asFile
val failures = buildList {
git("status", "--porcelain", "-uall").forEach { add("drift ${it.trim()}") }
// a deleted file already shows up as drift, so skip paths that no longer exist
git("ls-files").map { root.resolve(it) }.filter(File::isFile).forEach { f ->
badLine(f).takeIf { it > 0 }?.let { add("hidden char ${f.toRelativeString(root)}:$it") }
}
}
if (failures.isNotEmpty())
throw GradleException(failures.joinToString("\n", prefix = "\n") { " - $it" })
logger.lifecycle("Agent instruction audit passed.")
}
}
tasks.register<AuditAgentFiles>("auditAgentFiles") {
group = "verification"
repoRoot.set(layout.settingsDirectory) // the checkout root, not this subproject
}
// finalizedBy rather than dependsOn, so the audit runs after the build that plants the file
tasks.named("check") { finalizedBy("auditAgentFiles") }
The task class takes Gradle’s injected ExecOperations and resolves every path against the checkout root rather than the current
subproject. That keeps the audit compatible with the configuration cache, correct inside a monorepo, and identical on your machine and in CI.
There are some important details in those scripts. The :(glob)**/ pathspec finds every match at every depth, so the
monorepo layout from earlier needs no extra work and an attacker gains nothing by dropping a file three directories down. The list
of file names matters just as much because your agents treat CLAUDE.md, .cursorrules, and skill files as policy too. Add
whatever else your agents read. Finally, -z tells git to print raw paths. Without it git escapes any path holding a
non-ASCII character, the file lookup misses, and the audit skips the one file most likely to be poisoned.
These approaches have holes. I am choosing practicality that meets a base level of protection over maximalism. A maximal check costs too much to maintain and fires so many false positives that we turn the whole thing off and land right where we started:
- A smart quote fails the build, and so does a teammate named Björn. You retype the quote as ASCII and move on.
- Git respects
.gitignore, so a write intonode_modules/x/AGENTS.mdstays invisible. Agents do not load instruction files out of ignored trees, so I accept that. Add--ignored=matchingif you disagree. - An attacker who manages to commit a bad
AGENTS.mddefeats the drift half outright. Run the audit in CI, where the checkout starts clean and the build becomes the only thing that can dirty it. Then let the ASCII half and a human reviewer cover the rest. ClaimAGENTS.mdin aCODEOWNERSfile with no leading slash so it matches at every depth, and turn on the branch protection rule that requires code owner approval (becauseCODEOWNERSon its own only requests a reviewer).
One last key point. These checks protect the instruction files themselves and leave the documents they point to, like docs/TESTING.md
and docs/CONFIG.md, wide open. Add those paths to the list if that keeps you up at night.
Make Your Opinions Real
You and I have opinions on how to build the best software. For decades, these opinions made for inconsistent static analysis and code review at best and meaningless, wasteful social media debates at worst.
One of the great things about the age of AI is that it presents us with the opportunity to encode our opinions for agents via AGENTS.md. If we take time to understand the
art in crafting a thorough but efficient AGENTS.md, to learn how to complement it with Agent Skills, and to treat it with the same Zero Trust suspicion we would
treat a shell script the package wants to run, then we can turn our opinions into
an actionable blueprint for building great software and delivering the best experiences for our customers.




