{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "@id": "https://www.vidyasource.com/blog/put-your-opinions-to-work-with-agents-md-and-keep-them-safe/",
  "url": "https://www.vidyasource.com/blog/put-your-opinions-to-work-with-agents-md-and-keep-them-safe/",
  "mainEntityOfPage": "https://www.vidyasource.com/blog/put-your-opinions-to-work-with-agents-md-and-keep-them-safe/",
  "headline": "Put Your Opinions to Work with AGENTS.md and Keep Them Safe",
  "description": "AGENTS.md turns your opinions into standards every AI coding agent enforces. Respect the instruction budget, then defend the file from prompt injection.",
  "datePublished": "2026-08-14T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "Neil Chaudhuri",
    "jobTitle": "President",
    "url": "https://www.linkedin.com/in/neil-chaudhuri/",
    "sameAs": "https://www.linkedin.com/in/neil-chaudhuri/"
  },
  "publisher": {
    "@id": "https://www.vidyasource.com/#organization"
  },
  "image": "https://www.vidyasource.com/img/blog/put-your-opinions-to-work-with-agents-md.webp",
  "keywords": [
    "AI",
    "Software Engineering",
    "Programming",
    "Architecture",
    "TypeScript",
    "Kotlin",
    "Functional Programming",
    "Testing",
    "Open Source",
    "DevSecOps"
  ],
  "articleBody": "I've seen enough social media debates to understand that software engineering is the most opinionated profession in the world. [Tailwind CSS](https://tailwindcss.com) either ruins your markup or saves your team. Agile died a decade ago, or it never actually started.\nMicroservices are dead or completely necessary to scale. To be honest, I have always found it all absurd. Don't people get tired of having\nthe same arguments every other week?\n\nWhile I try to avoid the fights, I am no exception when it comes to having strong engineering opinions. I believe [static type systems](https://www.vidyasource.com/blog/why-types-and-tests-are-both-essential-in-programming/) should make invalid states impossible to express, so a boolean never replaces a state machine\nand magic strings never replace real types.\nI believe type safety should extend to configuration, which is why I always use [Pkl](https://pkl-lang.org) rather than bury myself in endless, error-prone YAML files.\nI believe [property-based testing](https://kotest.io/docs/proptest/property-based-testing.html) finds\nthe 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\ncode uses [Effect](https://effect.website) and our Kotlin code gets very close to true effects with [context parameters](https://kotlinlang.org/docs/context-parameters.html).\n\nWhatever 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.\nBoth 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\n[bottleneck](https://www.vidyasource.com/blog/beyond-github-culture-reconsider-pull-requests/) because reviewers are busy doing their real jobs, so reviews are typically rubber stamps that allow all sorts of\nviolations of your coding standards.\n\nAI changes that. Specifically, the `AGENTS.md` file changes that. Write an opinion down once, and every coding agent applies it everywhere. Well, almost.\n\n## What is AGENTS.md?\n\n`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\nrequest, which makes it a configuration layer between the model's base instructions and your actual code.\n\nOn December 9, 2025, the Linux Foundation announced the formation of the [Agentic AI Foundation](https://aaif.io/), and OpenAI donated [AGENTS.md](https://agents.md/) to the AAIF alongside Anthropic's Model Context Protocol and Block's goose.\nI [wrote about why that matters](https://www.vidyasource.com/blog/agentic-ai-foundation-ambassador/) when AAIF named me an inaugural Ambassador. The adoption numbers are clear. More than 60,000 repositories, almost 24,000 GitHub stars, and\ncountless tools use `AGENTS.md`. The people (and their coding agents) have spoken.\n\n`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\nshould be aware of.\n\n## AGENTS.md Discoverability\n\nThere 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\ninstruction 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.\n\nThe 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\nhierarchy. For example, you can instruct Claude Code to use your `AGENTS.md` with their `@` import syntax:\n\n```markdown\n# CLAUDE.md\n\n@AGENTS.md\n\n# Claude-specific instructions\n```\n\nClaude Code expands the `@` import at session start and loads the shared file as if it were inline. A symlink does the same job:\n\n```bash\nln -s AGENTS.md CLAUDE.md\n```\n\n## Balancing the Instruction Budget\n\n`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\ntask 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\nopinions when they need to update a Spring Boot repository.\n\nMatt Pocock has a term for this: the [instruction budget](https://www.aihero.dev/a-complete-guide-to-agents-md). According to Matt, frontier AI models follow roughly 150 to 200\ninstructions with consistency. Smaller models follow fewer. The specific numbers don't\nmatter, 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,\na [study](https://huggingface.co/papers/2511.12884) of 2,303 real agent context files across 1,925 repositories found that they read like\ncomplex configuration code rather than documentation, growing through frequent small additions that nobody ever prunes. The same\nstudy found that developers pile in build commands, implementation details, and architecture while security surfaces in a mere\n14.5% of files. People clearly went off on their tech opinions in these files unrestrained by the character limits of social media.\n\n`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\nknow 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\nAI 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\nbuild 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\nmy opinions and creates a setup specific to my product. That's perfect for `AGENTS.md`.\n\nIn 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.\nGive it a short project description, the \"pillars\" of the code base representing your foundational patterns and practices at a high level,\nand bespoke commands. Resist the urge to enumerate file structures, which change constantly, creating a maintenance headache and wasting tokens on wild goose chases.\n\n## Progressive Disclosure\n\nThe 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\ndeveloper-level \"[culture of context](https://www.vidyasource.com/blog/create-a-culture-of-context-for-ai/)\" that agents need to thrive. Create context files representing the various concerns across your product like UI, testing, and\nobservability. 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`.\nThen 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\nignoring `FRONTEND.md`, `TESTING.md`, and whatever other context files you have expressing your opinions. This focuses the agent on the context it needs while\nlimiting bloat on topics irrelevant to the task at hand.\n\nHere is a sample `AGENTS.md` file that balances the instruction budget while expressing key opinions including what *not* to do.\n\n```markdown\n# AGENTS.md\n\n## Project\n\n- Package manager: `npm`\n- Full build: only when requested\n- Before changing code, inspect relevant existing patterns and docs.\n\n## Rules\n\n### Required\n\n- Never hardcode credentials, hostnames, or URLs. Runtime configuration comes through Pkl.\n- Model invalid states out of existence with types. Do not use booleans as state machines or nullable fields to represent meaningful absence.\n- Use typed error values (`Either`, `Result`, or Effect's error channel). Do not throw across module boundaries.\n- Keep the functional core pure: no clock, randomness, or I/O. Inject these dependencies at the boundary.\n- Add behavior tests before implementation for new behavior. For refactors, type-only changes, and trivial changes, use judgment.\n\n### Conventions\n\nFollow these docs when relevant:\n\n- TypeScript / Effect: `docs/TYPESCRIPT.md`\n- Testing / Kotest / fast-check: `docs/TESTING.md`\n- Pkl configuration: `docs/CONFIG.md`\n\n## Permissions\n\n### No approval needed\n\n- Read files\n- Run tests\n- Run `npm typecheck`\n- Lint touched files\n\n### Ask first\n\n- Add or update dependencies\n- Change CI\n- Change infrastructure\n- Rename or remove a public export\n- Make changes outside the task's scope\n\n### Never\n\n- Commit secrets\n- Force-push\n- Rewrite prose in documents I authored\n```\n\nThis 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.\nDetails on specific concerns like `TESTING.md` live elsewhere, and they cost nothing until the agent needs them for a testing task.\n\nNotice 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.\n\nYou 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.\nConsider something like this:\n\n```bash\nrepo/\n├── AGENTS.md\n├── docs/\n│   ├── TYPESCRIPT.md\n│   ├── TESTING.md\n│   └── CONFIG.md\n├── apps/\n│   ├── web/\n│   │   └── AGENTS.md        # web-specific rules\n│   └── api/\n│       └── AGENTS.md        # API-specific rules\n└── packages/\n    ├── core/\n    │   └── AGENTS.md        # core-specific architecture\n    └── config/\n        └── AGENTS.md\n```\n\nIn 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\nlinks 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\nand 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`.\nI personally prefer keeping things at the top level and using lower level `AGENTS.md` files sparingly. That's your call.\n\nSpeaking 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\nrepeats 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\nover time.\n\n## Then Add Skills\n\n`AGENTS.md` is always resident to express your opinions and high-level patterns for your code base. An [agent skill](https://code.claude.com/docs/en/skills) loads only when its description triggers to apply your opinions to specific, repeatable tasks. They complement each other.\n\nFor 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\nproperty-based testing. A complementary Agent Skill can put these ideas into practice:\n\n```markdown\n---\nname: effect-service\ndescription: Scaffold a new Effect-TS service with its Layer, tagged error types,\n  a Pkl config schema, and a fast-check property test. Use when adding a service\n  under src/services/. Do NOT use for React components, for one-off scripts, or for\n  modifying a service that already exists.\n---\n```\n\n[Philipp Schmid](https://www.philschmid.de/agent-skills-tips) 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.\nThe `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.\n\nEncoding your opinions in `AGENTS.md` and complementing them with Agent Skills to apply them in practice is a powerful combination.\n\n## Protect Your Opinions\n\nIf 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](https://www.youtube.com/watch?v=Mzw2ttJD2qQ) pun intended, for prompt injection attacks\non your own machine that can leak secrets to attackers and cause many other problems.\n\nEven beyond that, any file with the power of `AGENTS.md` is a standing vulnerability. NVIDIA's AI Red Team published a\n[working proof of concept](https://developer.nvidia.com/blog/mitigating-indirect-agents-md-injection-attacks-in-agentic-environments/)\nthat shows exactly how it breaks. The attack runs in four steps:\n\n1. You add a dependency. You may well have audited that package, but nobody reads the source of every\n   transitive package underneath it. The attack assumes precisely that.\n2. Your build runs, and the dependency executes as part of it. NVIDIA used a Go library whose exported function checks for the\n   `CODEX_PROXY_CERT` environment variable, so the payload fires only inside a Codex session and stays dormant everywhere else.\n3. It then writes an untracked `AGENTS.md` into your working directory. The text it plants claims absolute authority and orders\n   the agent to supersede anything you type.\n4. The agent reads the new file and obeys it inside the session already running.\n\nStep 4 is the attack. Nobody needs to trick you. Nobody needs to jailbreak the model. The\nattacker writes a file your agent already treats as policy, and no human reviews it at read time. The injection also lands\nmid-session, so it takes effect without a restart. Restarting doesn't help either because the file stays on disk.\n\nPillar Security demonstrated an even nastier variant they named the [Rules File Backdoor](https://www.pillar.security/blog/new-vulnerability-in-github-copilot-and-cursor-how-hackers-can-weaponize-code-agents).\nThey hide the injected directives inside invisible Unicode, using zero-width joiners and bidirectional text markers, so the\npoisoned file looks identical to the clean one in your editor even though the bytes differ. They disclosed it to Cursor and\nGitHub in early 2025, and GitHub responded that May with\n[warnings when a file contains hidden Unicode text](https://github.blog/changelog/2025-05-01-github-now-provides-a-warning-about-hidden-unicode-text/).\nA warning helps at review time on one platform. Your editor, your local diff, and your agent still read the\nfile straight through, so assume your own tooling misses this entirely.\n\nLet's step back and understand all the threat vectors to `AGENTS.md`:\n\n- **Install-time write.** A dependency may execute code while you install it and write its own malicious `AGENTS.md`. Every programming\n  ecosystem opens this door in its own way: a Node lifecycle script, a Python [PEP 517](https://peps.python.org/pep-0517/) backend\n  that runs arbitrary code for any source distribution, or a third-party Gradle plugin that runs at configuration time.\n- **Runtime write.** A package that executes later, in your dev server or CI build, does the same thing at a moment without\n  install-time protection. This is NVIDIA's proof of concept.\n- **Borrowed content.** You copy an open-source `AGENTS.md` or Agent Skill that arrives poisoned, which is\n  the Trojan horse I mentioned.\n- **Inbound change.** A clone, a merge, or a teammate's pull request carries it in.\n\nMeanwhile, invisible Unicode hides whichever of the four above an attacker picked, which is what makes\nPillar's variant particularly insidious.\n\nThe good news is that `git` can help us with both NVIDIA's and Pillar's attacks:\n\n- **Ask `git` what changed.** Git already stores the sanctioned content of every tracked `AGENTS.md` and flags any file that\nnobody committed. WHen NVIDIA's attack created either a new `AGENTS.md` in your working directory or an appended one,\nrunning `git status` flags it.\n- **Create an allowlist of characters that includes tab, carriage return, and printable ASCII. Reject everything else.** Pillar hides its directives in zero-width joiners,\nbidirectional overrides, tag characters, and variation selectors. An allowlist helps because none of them\nlands between `0x20` and `0x7E`. The rule holds no list of dangerous code points, so Unicode can ship a thousand new invisible\ncharacters next year and the test still catches all of them.\n\nNow 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.\nIf 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\nwith 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\ngreater than you realize. I personally like to use my build tools to help me with this sort of thing.\n\nHere is an approach in Node. Wire it into `package.json` as `\"audit:agents\": \"node scripts/audit-agents.mjs\"`, then chain it onto the\nbuild with `\"build\": \"tsc -b && npm run audit:agents\"`. The order is important because NVIDIA's attacks hits while the build\nruns, so an audit that goes first is pointless.\n\n```js\n#!/usr/bin/env node\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\n\n// every file your agents load as policy, at every depth\nconst NAMES = [\"AGENTS.md\", \"CLAUDE.md\", \".cursorrules\", \"SKILL.md\"];\nconst PATHS = NAMES.map(n => `:(glob)**/${n}`);\n\n// -z stops git from escaping the paths most likely to be poisoned\nconst git = (...a) => execFileSync(\"git\", [...a, \"-z\", \"--\", ...PATHS], { encoding: \"utf8\" })\n  .split(\"\\0\").filter(Boolean);\n\n// the first line holding a character outside tab, carriage return, and printable ASCII\nconst badLine = f => readFileSync(f, \"utf8\").split(\"\\n\")\n  .findIndex(l => !/^[\\t\\r\\x20-\\x7E]*$/.test(l)) + 1;\n\nconst failures = git(\"status\", \"--porcelain\", \"-uall\").map(l => `drift        ${l.trim()}`);\n\n// a deleted file already shows up as drift, so skip paths that no longer exist\nfor (const f of git(\"ls-files\").filter(existsSync)) {\n  const n = badLine(f);\n  if (n) failures.push(`hidden char  ${f}:${n}`);\n}\n\nfailures.forEach(f => console.error(f));\nif (failures.length === 0) console.log(\"Agent instruction audit passed.\");\nprocess.exit(failures.length ? 1 : 0);\n```\n\nHere is the analogous Gradle task in Kotlin:\n\n```kotlin\nimport java.io.ByteArrayOutputStream\nimport java.io.File\nimport javax.inject.Inject\n\nabstract class AuditAgentFiles : DefaultTask() {\n    @get:Inject abstract val execOps: ExecOperations\n    @get:Internal abstract val repoRoot: DirectoryProperty\n\n    private fun git(vararg args: String): List<String> {\n        // every file your agents load as policy, at every depth\n        val paths = listOf(\"AGENTS.md\", \"CLAUDE.md\", \".cursorrules\", \"SKILL.md\").map { \":(glob)**/$it\" }\n        val out = ByteArrayOutputStream()\n        execOps.exec {\n            workingDir(repoRoot.get().asFile)\n            // -z stops git from escaping the paths most likely to be poisoned\n            commandLine(listOf(\"git\") + args + \"-z\" + \"--\" + paths)\n            standardOutput = out\n        }\n        return out.toString(\"UTF-8\").split('\\u0000').filter { it.isNotBlank() }\n    }\n\n    // the first line holding a character outside tab, carriage return, and printable ASCII\n    private fun badLine(f: File) = f.readText().lines()\n        .indexOfFirst { l -> l.any { it != '\\t' && it != '\\r' && (it < ' ' || it > '~') } } + 1\n\n    @TaskAction\n    fun audit() {\n        val root = repoRoot.get().asFile\n        val failures = buildList {\n            git(\"status\", \"--porcelain\", \"-uall\").forEach { add(\"drift        ${it.trim()}\") }\n            // a deleted file already shows up as drift, so skip paths that no longer exist\n            git(\"ls-files\").map { root.resolve(it) }.filter(File::isFile).forEach { f ->\n                badLine(f).takeIf { it > 0 }?.let { add(\"hidden char  ${f.toRelativeString(root)}:$it\") }\n            }\n        }\n        if (failures.isNotEmpty())\n            throw GradleException(failures.joinToString(\"\\n\", prefix = \"\\n\") { \"  - $it\" })\n        logger.lifecycle(\"Agent instruction audit passed.\")\n    }\n}\n\ntasks.register<AuditAgentFiles>(\"auditAgentFiles\") {\n    group = \"verification\"\n    repoRoot.set(layout.settingsDirectory)   // the checkout root, not this subproject\n}\n\n// finalizedBy rather than dependsOn, so the audit runs after the build that plants the file\ntasks.named(\"check\") { finalizedBy(\"auditAgentFiles\") }\n```\n\nThe task class takes Gradle's injected `ExecOperations` and resolves every path against the checkout root rather than the current\nsubproject. That keeps the audit compatible with the configuration cache, correct inside a monorepo, and identical on your machine and in CI.\n\nThere are some important details in those scripts. The `:(glob)**/` pathspec finds every match at every depth, so the\nmonorepo layout from earlier needs no extra work and an attacker gains nothing by dropping a file three directories down. The list\nof file names matters just as much because your agents treat `CLAUDE.md`, `.cursorrules`, and skill files as policy too. Add\nwhatever else your agents read. Finally, `-z` tells `git` to print raw paths. Without it `git` escapes any path holding a\nnon-ASCII character, the file lookup misses, and the audit skips the one file most likely to be poisoned.\n\nThese approaches have holes. I am choosing practicality that meets a base level of protection over maximalism. A maximal check\ncosts too much to maintain and fires so many false positives that we turn the whole thing off and land right where we started:\n\n- A smart quote fails the build, and so does a teammate named Bj&#246;rn. You retype the quote as ASCII and move on.\n- Git respects `.gitignore`, so a write into `node_modules/x/AGENTS.md` stays invisible. Agents do not load instruction files out\n  of ignored trees, so I accept that. Add `--ignored=matching` if you disagree.\n- An attacker who manages to commit a bad `AGENTS.md` defeats the drift half outright. Run the audit in CI, where the checkout starts clean and the build becomes\n  the only thing that can dirty it. Then let the ASCII half and a human reviewer cover the rest. Claim `AGENTS.md`  in a `CODEOWNERS`\n  [file](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) with no leading slash so it\n  matches at every depth, and turn on the branch protection rule that requires code owner approval (because `CODEOWNERS` on its own only requests a reviewer).\n\nOne last key point. These checks protect the instruction files themselves and leave the documents they point to, like `docs/TESTING.md`\nand `docs/CONFIG.md`, wide open. Add those paths to the list if that keeps you up at night.\n\n## Make Your Opinions Real\n\nYou 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\nand meaningless, wasteful social media debates at worst.\n\nOne 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\nart 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](https://csrc.nist.gov/pubs/sp/800/207/final) suspicion we would\ntreat a shell script the package wants to run, then we can turn our opinions into\nan actionable blueprint for building great software and delivering the best experiences for our customers."
}