AGENTS.md and Agent Skills: what I learned by lifting the hood of AI Coding Agents
When I started using AI coding agents in my daily routine, I noticed a pattern that bothered me: the agent made mistakes that no dev on my team would make. It used the wrong formatter, ignored project naming conventions, and ran tests with the wrong command. It took me a while to understand that the problem wasn't the model. The problem was that the agent simply didn't know my project.

In this material, I explain, in the way I would have liked to have been explained, what AGENTS.md is, what Agent Skills are, how everything works behind the scenes, and finally, I deliver a step-by-step tutorial for you to implement each part in your repository today.
Part 1: Didactic Explanation
1.1 The README that AI reads
Think with me: the README.md exists for humans. It explains what the project does, how to install, how to contribute. But an AI agent needs completely different information:
- What is the build command?
- What is the style guide?
- How to run an individual test?
- What architectural patterns to follow?
The answer that the community found was to create a "README for machines". That's exactly what AGENTS.md is: a Markdown file that is located in the root of your repository (or in subdirectories, in the case of monorepos) containing instructions directed to AI agents.
1.2 Why an open standard matters
Before AGENTS.md, each tool invented its own proprietary file:
| Tool | Proprietary File | |---|---| | Claude Code | CLAUDE.md | | Cursor | .cursor/rules/ | | Windsurf | Memories / Rules | | Copilot | .github/copilot-instructions.md |
This created fragmentation. If you worked with two tools, you maintained two files saying the same thing. AGENTS.md was born as an open and ecosystem-agnostic standard, and today it is supported by more than 20 tools, including Codex (OpenAI), Cursor, Antigravity (Google DeepMind), Jules (Google), GitHub Copilot Coding Agent, Windsurf, OpenCode, Aider, Zed, Warp, RooCode, Gemini CLI, and Devin. According to the official agents.md website, more than 60,000 public repositories on GitHub already include a context file.
In my view, as someone who has seen many standard wars, this is the kind of convergence worth embracing early on.
1.3 What goes inside an AGENTS.md
The typical content covers four blocks:
# AGENTS.md
## Build & Test
- Build: `npm run build`
- Test all: `npm test`
- Test single: `npm test -- --grep "test name"`
- Lint: `npm run lint`
## Code Style
- Use TypeScript strict mode
- Prefer `const` over `let`
- Avoid `try/catch` where possible
- Single-word variable names preferred
## Architecture
- API handlers in `src/api/handlers/`
- Database models in `src/models/`
- Avoid circular dependencies
## Testing
- Avoid mocks when possible
- Test actual implementation
The central principle I defend here is: be concise and practical. No long texts explaining the project's philosophy. The agent needs clear rules and executable commands. Keep that phrase in mind, because the scientific research I will cite at the end confirms exactly that.
1.4 Hierarchy: from global to local
A powerful feature of the standard is the hierarchy of files. In OpenCode, for example, there are three levels:
~/.config/opencode/AGENTS.md → Global (all sessions)
./AGENTS.md → Project (repository root)
./packages/frontend/AGENTS.md → Specific package (monorepo)
The rule is simple: the closest file has priority. This allows a monorepo to have global rules in the root (formatting, naming, general conventions) and specific rules in each package (React 19 framework, CSS with Tailwind v4, tests with Vitest, for example).
1.5 How it works under the hood: the OpenCode case
This is where things get really interesting. The OpenCode article opens the source code (which is open source and has a clean implementation) and shows three mechanisms.
Mechanism 1: Discovery
The first step is to discover which instruction files exist. In OpenCode, this happens in the instruction.ts file, which defines a list of files in order of priority:
// Files that the system looks for, in order of priority
const FILES = [
"AGENTS.md",
"CLAUDE.md",
"CONTEXT.md", // deprecated
]
The search happens on two fronts simultaneously:
Front 1: user's global configuration. The system checks if the developer has a personal AGENTS.md that applies to all projects. Think of this as your universal preferences: the code style you like, the tools you always use.
function globalFiles() {
const files = []
files.push(path.join(Global.Path.config, "AGENTS.md"))
// Compatibility: also looks for CLAUDE.md from Claude Code
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
files.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
}
return files
}
Front 2: project context. In parallel, the system traverses the directory tree from bottom to top (from the current working directory to the repository root), collecting all AGENTS.md files it finds along the way:
// Hierarchical search: goes up from the current directory to the repository root
const matches = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
Priority rule in case of conflict: the closest file (most specific) wins. And AGENTS.md has priority over CLAUDE.md, which has priority over CONTEXT.md.
Mechanism 2: Injection into the system prompt
Once discovered, the files are read and injected directly into the LLM's system prompt. In the agent's main loop (prompt.ts):
// Assembling the system prompt
const system = [
...await SystemPrompt.environment(model), // Environment information (OS, directory, date)
...await InstructionPrompt.system() // ← AGENTS.md enters here
]
The content of AGENTS.md is prefixed with "Instructions from: /path/to/AGENTS.md" and concatenated to the system prompt. This means that every message the LLM processes already contains the instructions from your repository. The final assembly of the system prompt follows this order:
- Model's base prompt (Anthropic, GPT, Gemini)
- Environment information (OS, directory, date, git status)
- AGENTS.md content (build, test, style guide)
- Tool definitions (read, write, bash, grep, skill)
Mechanism 3: Lazy loading of nested instructions
Here's an elegant detail I didn't know before reading the code. OpenCode implements contextual lazy loading: when the agent reads a file in a subdirectory (e.g., src/components/Button.tsx), the system checks if there is an AGENTS.md in that subdirectory and, if so, injects its content automatically:
export async function resolve(messages, filepath, messageID) {
let current = path.dirname(filepath)
const root = path.resolve(Instance.directory)
// Goes up from the file's directory to the root
while (current.startsWith(root) && current !== root) {
const found = await find(current)
// Injects if: exists, is not the root file, and has not been loaded before
if (found && !system.has(found) && !already.has(found)) {
const content = await Filesystem.readText(found)
results.push({ filepath: found, content })
}
current = path.dirname(current)
}
}
The agent only receives the instructions from a subdirectory when it is actually working on files in that subdirectory. Without unnecessarily polluting the context.
1.6 Agent Skills: modular blocks of behavior
If AGENTS.md is the "project manual", Agent Skills are a more sophisticated concept: modular blocks of behavior that the agent can load on demand.
A skill is a directory containing a SKILL.md file with specialized instructions. The format follows YAML frontmatter plus Markdown body:
---
name: git-release
description: Create consistent releases and changelogs
---
## What I do
- Draft release notes from merged PRs
- Propose a version bump
- Provide a copy-pasteable `gh release create` command
## When to use me
Use this when you are preparing a tagged release.
Skills can contain additional files such as scripts, templates, and references:
my-skill/
├── SKILL.md # Main instructions (required)
├── reference.md # Detailed documentation (loaded on demand)
├── examples/
│ └── sample.md # Examples (loaded on demand)
└── scripts/
└── helper.py # Utility script (executed, not loaded into context)
1.7 Progressive Disclosure: the standard that makes everything scale
This is the most important concept in the entire material, in my opinion. Progressive disclosure is a UI design pattern borrowed for AI agent architecture. The idea:
Instead of loading everything into the LLM's context at once (which consumes tokens, increases cost, and can confuse the model), reveal information gradually, as needed.
The mechanism works in three layers:
| Layer | What it loads | When | Approximate cost | |---|---|---|---| | Layer 1: Indexing | Name and description of each skill | Always, in the prompt | ~50 tokens (minimum) | | Layer 2: Activation | Body of the SKILL.md with complete instructions | When the model calls the skill() | ~500 tokens (medium) | | Layer 3: Reference | Files in the skill directory (scripts, examples, docs) | On demand, via read_file | ~2000 tokens (maximum) |
Why does this matter? The LLM's context window is a finite and precious resource. Each wasted token is one less for reasoning. Progressive disclosure solves this: the model knows what exists (Layer 1), loads how to use it when it decides (Layer 2), and accesses deep details if needed (Layer 3).
1.8 How OpenCode implements Skills
Discovery (skill.ts)
The discovery algorithm is multi-directory and multi-scope:
// External directories compatible
const EXTERNAL_DIRS = [".claude", ".agents"]
const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
The search order ensures that the project overrides the global:
- Global:
~/.claude/skills/*/SKILL.md - Global:
~/.agents/skills/*/SKILL.md - Project:
.claude/skills/*/SKILL.md(hierarchically) - Project:
.agents/skills/*/SKILL.md(hierarchically) - Config:
.opencode/skills/*/SKILL.md - Custom: paths configured in
opencode.json - Remote: skills downloaded via URL (index.json)
The cross-tool compatibility is intentional: a skill created for Claude Code in .claude/skills/ works automatically in OpenCode.
A tool skill as a disclosure mechanism (tool/skill.ts)
The magic happens in the definition of the tool skill. OpenCode injects the index of all available skills directly into the tool's description:
const description = [
"Load a specialized skill that provides domain-specific instructions.",
"",
"Available skills:",
"",
"<available_skills>",
...accessibleSkills.flatMap((skill) => [
` <skill>`,
` <name>${skill.name}</name>`,
` <description>${skill.description}</description>`,
` </skill>`,
]),
"</available_skills>",
].join("\n")
This is Layer 1: the model sees