|
| 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. |
0 commit comments