mods/agents-md: the AGENTS.md project-instructions mod (#95409) · anthropics/claude-code@a92ea1c

GitHub

9 min read Original article ↗
1+

# agents-md

2+3+

`AGENTS.md` read the way Claude Code reads `CLAUDE.md`, as a plugin, under

4+

one option, `instructionFiles`:

5+6+

- `claude-md`: only `CLAUDE.md` is loaded, by the engine, as today. The plugin

7+

adds nothing.

8+

- `claude-md-or-agents-md` (the default): a project with no instruction files

9+

of its own gets its `AGENTS.md` files instead, loaded exactly where and how

10+

`CLAUDE.md` would be. "Of its own" is read off what the engine loaded for

11+

the context: a `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md` in any

12+

directory from the root down to the working directory leaves the whole

13+

project to the engine, and the plugin stays out (the organization's managed

14+

file, the person's `~/.claude/CLAUDE.md`, a `.claude/rules` file and an

15+

added directory's `CLAUDE.md` do not count, as the nested walk does not see

16+

them either). With none, every `AGENTS.md` and `.claude/AGENTS.md` on that

17+

path joins the instruction files the engine renders, and a `Read` under a

18+

subdirectory attaches that directory's `AGENTS.md` unless a `CLAUDE.md`

19+

there claims it.

20+

- `claude-md-and-agents-md`: every `AGENTS.md` is loaded beside `CLAUDE.md`,

21+

up and down the tree; a file `CLAUDE.md` already `@`-imports, or is a link

22+

to, is not loaded a second time (compared by path, then by content).

23+

- `managed-only`: the project's checked-in and private instruction files and

24+

the person's own are dropped from the context; the organization's managed

25+

`CLAUDE.md` and the engine's memory stay. The engine's nested `CLAUDE.md`

26+

attachments on `Read` are not an event yet and still arrive. (The engine's

27+

`claudeMdExcludes` setting also exists, for user, project and local files,

28+

and applies to the `AGENTS.md` files this plugin reads too.)

29+30+

How the files reach the model is the engine's doing, not the plugin's:

31+

`prompt.context` hands a hook the instruction files behind `claudeMd`

32+

(`{ path, kind, content, parent? }`, kinds `managed`, `user`, `project`,

33+

`local`, `memory`, in load order) and a hook answers the list changed. The

34+

engine then renders `claudeMd` from the answered files with its own preamble

35+

and framing, announces them by name, and keeps only the `managed` ones for an

36+

agent that omits project instructions (Explore, Plan, a custom agent with

37+

`omitClaudeMd`). So an `AGENTS.md` this plugin adds as a `project` file is, to

38+

everything downstream, a project instruction file: same place in the context,

39+

same framing, same omission rules, same announcement. An organization's

40+

prepended plugin on `prompt.context` sits above this one and has the last

41+

word on the files.

42+43+

`hooks/register.ts` is the module; everything under `hooks/` is its parts,

44+

importing `claude-code` and one another alone. `tests/` runs under

45+

`claude plugin test <this folder>`.

46+47+

## Setting the option

48+49+

As a built-in its option is the `/config` row "Project instructions", a

50+

picker over the four values, each described there. By hand it is

51+52+

```json

53+

{

54+

"pluginConfigs": {

55+

"agents-md@builtin": {

56+

"options": { "instructionFiles": "claude-md-and-agents-md" }

57+

}

58+

}

59+

}

60+

```

61+62+

in user settings (`~/.claude/settings.json`), `--settings`, or managed

63+

settings; a project's `.claude/settings.json` is not read for plugin

64+

options. Changing it reloads the module, and the next context the engine

65+

builds (the next turn after the reload, a new conversation, `/clear`, a

66+

compaction) carries the new mode's files. A hand-typed value outside the

67+

four is told once in the transcript and reads as the default. `/plugin`

68+

lists the plugin among the built-ins, where a person can turn it off; with

69+

it off the engine reads `CLAUDE.md` alone.

70+71+

The option was first keyed `projectInstructions`, with the values `claude`,

72+

`agents-fallback`, `both` and `none`. A value still stored under that key is

73+

honoured for now while `instructionFiles` reads as its default: `none` as

74+

`managed-only`, `claude` as `claude-md`, `agents-fallback` as

75+

`claude-md-or-agents-md`, `both` as `claude-md-and-agents-md`, any other

76+

value as `claude-md` (which adds nothing, never as the default, which loads

77+

`AGENTS.md`); the first `session.start` of a load says in the transcript how

78+

it was read. Once `instructionFiles` is set to anything but its default, the

79+

old key is not read and the transcript says to remove it.

80+81+

Run from this folder instead (`claude --plugin-dir mods/agents-md`), the

82+

same entry is keyed `"agents-md"`.

83+84+

## What it hooks

85+86+

| event | what the hook does |

87+

| --- | --- |

88+

| `session.start` | in every mode: passes the start straight through and floats the usage row for the configured mode, never awaited; the first start of a load logs how a stored `projectInstructions` value is read. The session's start never waits on this plugin |

89+

| `prompt.context` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`: walks `$.fs.ancestors` for the `AGENTS.md` files above the working directory and answers them as `project` instruction files, each `@` import its own entry after its file, each placed where a project file of its directory stands (root first, before the first deeper project file, else after the last project file, before memory); files the engine already holds by path or by content are left out; under `claude-md-or-agents-md` it answers nothing when the project has a `CLAUDE.md` of its own (among the handed files, else found by a `$.fs.ancestors` walk, so a `CLAUDE.md` the engine loaded and then withheld still counts), and logs which files it loaded once, and again after a move to another project root; handed unknown files (a hook above rewrote the `claudeMd` text) it adds nothing; the first context of a load sends the load row (counts) and the feature mark; under `managed-only` (matcher: a `project`, `local` or `user` file present): answers the list without those kinds |

90+

| `agent.spawn` on `fork: true` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`: a fork the Agent tool starts shares its parent's prompt prefix, so the parent loop's delivered nested files are copied to the fork's loop and not attached to it again (a `/fork` or `/subtask` fork does not raise `agent.spawn` yet and starts from an empty set, as every fork did before; a fork started in the same tool batch as a `Read` inherits that Read's file although its prefix holds a placeholder for it) |

91+

| `tool.call` on `Read` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`, for a file under the session's project root (`$.session.root()`, read live, so `/cd`, a host's directory change and worktree moves are followed and a moved root starts the delivered sets and the fallback decision over; a file elsewhere gets nothing, as the engine attaches no nested `CLAUDE.md` there): walks only the directories strictly between the root and the read file (`$.fs.ancestors` with `below: root`, as the engine walks only those for a nested `CLAUDE.md`, never up to the filesystem root again) and attaches their `AGENTS.md` files not yet given to that agent loop, not already among the context's instruction files (by path or, for a project file, by text) and not claimed by a `CLAUDE.md` of the same directory (or imported by one), as `context` after the tool result, framed `Contents of <path>:` byte for byte as the engine frames a nested `CLAUDE.md`, whatever its size; each file once per loop and conversation (the context's recomputation after a compaction or `/clear` starts the count over), the context's files never; a Read that attached files sends the nested row. A `~` or `~/` path is read under the home directory as the Read tool reads it |

92+93+

## What it calls on `$`

94+95+

`fs.ancestors` (with each found file's `parts`: the file and its imports

96+

apart; with `below` on a Read; it finds nothing on a thin client, whose

97+

workspace files are remote, as the engine's own walk does), `session.root`,

98+

`session.cwd`, `env.get` (`HOME` and `USERPROFILE`, once per load, the

99+

profile first on a Windows spelling of the working directory, so a `~/` path

100+

the model hands a Read resolves where the Read tool reads it), `ui.log`,

101+

`telemetry.log` and `telemetry.mark`.

102+103+

`$.telemetry` is the [telemetry](../telemetry) plugin's noun; where that

104+

plugin is not seated the calls find no noun and are dropped without a trace,

105+

and nothing else changes.

106+107+

## What it logs

108+109+

Counts and closed choices only; no path and no file text. Each row goes

110+

through `$.telemetry.log`, so it exists only where the telemetry plugin

111+

does:

112+113+

| event | when | properties |

114+

| --- | --- | --- |

115+

| `agents_md_mode` | once per fresh load, at `session.start` | `mode` (`claude-md` \| `claude-md-or-agents-md` \| `claude-md-and-agents-md` \| `managed-only`), `is_interactive` |

116+

| `agents_md_load` | the first context of a load, under `claude-md-or-agents-md` and `claude-md-and-agents-md` | `mode`, `file_count` (`AGENTS.md` files handed to the engine), `import_count` (their `@` imports), `total_content_length`, `yielded` (`claude-md-or-agents-md` stood down for a `CLAUDE.md` of the project's own), `walk_failed`; with it one `$.telemetry.mark` for feature `agents_md`: `ok`, or `sad` with reason `walk_failed` |

117+

| `agents_md_nested` | a Read that attached nested files | `mode`, `file_count` |

118+119+

## Where it still differs from CLAUDE.md

120+121+

All of these apply only to the modes that load `AGENTS.md`,

122+

`claude-md-or-agents-md` (the default) and `claude-md-and-agents-md`. Each

123+

names a loader fact a plugin cannot reach through the events it has today.

124+125+

1. Nested files attach on a text `Read` only. The engine also attaches a

126+

directory's `CLAUDE.md` for a file `@`-mentioned in the prompt, for the

127+

IDE's opened file or selection, and for the `Read` tool's notebook, image

128+

and PDF results.

129+

2. A nested file the plugin attaches is not registered in the loop's

130+

read-file state, so after a compaction the engine does not restore it

131+

among the recently read files (the plugin attaches it again at the next

132+

`Read` under that directory instead), and a change to it mid-session is

133+

not re-announced.

134+

3. `/cd` carries the new tree's `CLAUDE.md` in its own notice; the plugin's

135+

files for the new tree arrive in the same next request through the

136+

engine's instructions announcement instead.

137+

4. Paths compare by spelling; the engine resolves a symlinked alias of the

138+

working directory before deciding a file is inside it.

139+

5. `--add-dir` directories contribute no `AGENTS.md`, where the engine can

140+

load their `CLAUDE.md`.

141+

6. `/memory` and the `#` shortcut do not know `AGENTS.md` files, and the

142+

engine's own initial-load row does not count them (this plugin's

143+

`agents_md_load` row does).

144+

7. An `@` import outside the working directory inside an `AGENTS.md` is

145+

honoured only once the approval the engine asks for a `CLAUDE.md`'s

146+

external imports has been given (without it the import is left out, as a

147+

`CLAUDE.md`'s is); the approval dialog itself is raised for `CLAUDE.md`

148+

imports alone.

149+

8. A subagent that is not a fork gets a nested `AGENTS.md` at its own first

150+

`Read` under that directory even when its parent's loop was already given

151+

it; the engine does not hand such a subagent the nested `CLAUDE.md` again.

152+

A fork matches the engine on both sides.

153+154+

## Testing

155+156+

claude plugin test mods/agents-md

157+158+

`tests/register.test.ts` covers the default mode: a project with `AGENTS.md`

159+

alone gets it as a project instruction file and one transcript line naming

160+

it, a project with a `CLAUDE.md` of its own is left to the engine without a

161+

walk, a failed walk leaves the context as handed, and the start hands

162+

`$.telemetry` the mode row alone where a test seats a provider for that

163+

noun, and goes on untouched where none is seated.