Skip to content

Commit 2fc72b2

Browse files
authored
mods/agents-md: a Read attaches no nested AGENTS.md where the engine attaches nothing to a turn (#95417)
1 parent f708f4f commit 2fc72b2

6 files changed

Lines changed: 61 additions & 5 deletions

File tree

‎mods/agents-md/README.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ builds (the next turn after the reload, a new conversation, `/clear`, a
6666
compaction) carries the new mode's files. A hand-typed value outside the
6767
four is told once in the transcript and reads as the default. `/plugin`
6868
lists the plugin among the built-ins, where a person can turn it off; with
69-
it off the engine reads `CLAUDE.md` alone.
69+
it off the engine reads `CLAUDE.md` alone. No hooks setting or CLI mode turns
70+
it off (`disableAllHooks`, `allowManagedHooksOnly` and `--bare` govern
71+
settings hooks and installed plugins, not built-ins); where the engine loads
72+
no instruction files (`--bare` without `--add-dir`, `--safe-mode`,
73+
`CLAUDE_CODE_DISABLE_CLAUDE_MDS`) its walk finds none and it adds none,
74+
`CLAUDE.md` and `AGENTS.md` alike.
7075

7176
The option was first keyed `projectInstructions`, with the values `claude`,
7277
`agents-fallback`, `both` and `none`. A value still stored under that key is
@@ -88,7 +93,7 @@ same entry is keyed `"agents-md"`.
8893
| `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 |
8994
| `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 |
9095
| `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 |
96+
| `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; and nothing anywhere in a run where the engine attaches nothing to a turn, `--bare` with its `CLAUDE_CODE_SIMPLE` or `CLAUDE_CODE_DISABLE_ATTACHMENTS`, read on every Read through `$.env.get` as the engine reads them on every turn): 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 |
9297

9398
## What it calls on `$`
9499

@@ -97,7 +102,8 @@ apart; with `below` on a Read; it finds nothing on a thin client, whose
97102
workspace files are remote, as the engine's own walk does), `session.root`,
98103
`session.cwd`, `env.get` (`HOME` and `USERPROFILE`, once per load, the
99104
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`,
105+
the model hands a Read resolves where the Read tool reads it; `CLAUDE_CODE_SIMPLE`
106+
and `CLAUDE_CODE_DISABLE_ATTACHMENTS` on every Read), `ui.log`,
101107
`telemetry.log` and `telemetry.mark`.
102108

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

‎mods/agents-md/hooks/hooks.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"description": "AGENTS.md up and down the tree like CLAUDE.md, by the instructionFiles option: under claude-md-or-agents-md (the default; where the project has no CLAUDE.md of its own on the walk) or claude-md-and-agents-md, a prompt.context hook hands every ancestor AGENTS.md to the engine among its instruction files, skipping ones it already loaded, and a tool.call hook on Read attaches the AGENTS.md files of the directories between the project root and a read file, once a conversation and agent loop (an Agent-tool fork inherits its parent's); under managed-only, a prompt.context hook keeps only the organization's managed files and memory; under claude-md, nothing is added; in every mode a session.start hook sends the plugin's usage row through $.telemetry where that noun is seated",
2+
"description": "AGENTS.md up and down the tree like CLAUDE.md, by the instructionFiles option: under claude-md-or-agents-md (the default; where the project has no CLAUDE.md of its own on the walk) or claude-md-and-agents-md, a prompt.context hook hands every ancestor AGENTS.md to the engine among its instruction files, skipping ones it already loaded, and a tool.call hook on Read attaches the AGENTS.md files of the directories between the project root and a read file, once a conversation and agent loop (an Agent-tool fork inherits its parent's), except in a run where the engine attaches nothing to a turn (--bare, CLAUDE_CODE_DISABLE_ATTACHMENTS); under managed-only, a prompt.context hook keeps only the organization's managed files and memory; under claude-md, nothing is added; in every mode a session.start hook sends the plugin's usage row through $.telemetry where that noun is seated",
33
"modules": ["./register.ts"]
44
}

‎mods/agents-md/hooks/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './frames'
33
export * from './modes'
44
export * from './names'
55
export * from './register.js'
6+
export * from './switches'
67
export * from './telemetry'
78

89
export * as default from '.'

‎mods/agents-md/hooks/register.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Files from './files'
1010
import Frames from './frames'
1111
import Modes from './modes'
1212
import Names from './names'
13+
import Switches from './switches'
1314
import Telemetry from './telemetry'
1415

1516
/**
@@ -26,7 +27,9 @@ const NONE: readonly FsAncestor[] = []
2627
* standing alone. `managed-only`: the project's and the person's instruction
2728
* files dropped, the organization's kept. `claude-md-or-agents-md` (a project
2829
* with none of its own) and `claude-md-and-agents-md`: AGENTS.md files joined
29-
* to the engine's instruction files, nested ones on a Read. Every mode sends
30+
* to the engine's instruction files, nested ones on a Read except in a run
31+
* where the engine attaches nothing to a turn (--bare, which sets
32+
* CLAUDE_CODE_SIMPLE, or CLAUDE_CODE_DISABLE_ATTACHMENTS). Every mode sends
3033
* its usage rows through `$.telemetry` where that noun is seated and drops
3134
* them where it is not.
3235
*
@@ -182,6 +185,10 @@ export function register(on: On, options: PluginOptions): void {
182185
return result
183186
}
184187

188+
if (!(await attachesOnRead($))) {
189+
return result
190+
}
191+
185192
const [root, cwd] = await Promise.all([$.session.root(), $.session.cwd()])
186193
home ??= await homeOf($, cwd)
187194
const read = Frames.absoluteOf(e.file_path, cwd, home)
@@ -247,6 +254,31 @@ export function register(on: On, options: PluginOptions): void {
247254
})
248255
}
249256

257+
/**
258+
* Whether a Read attaches nested AGENTS.md files in this run: not where the
259+
* engine attaches nothing to a turn, a nested CLAUDE.md included, which is a
260+
* --bare run (it sets CLAUDE_CODE_SIMPLE) or one with
261+
* CLAUDE_CODE_DISABLE_ATTACHMENTS on.
262+
*
263+
* Read on every Read, as the engine reads them on every turn: a settings
264+
* `env` block or a managed delivery can flip either mid-session. The files of
265+
* the walk itself need no such check: where the engine loads no instruction
266+
* files `$.fs.ancestors` finds none.
267+
*
268+
* @param $ the engine, as the `tool.call` hook holds it
269+
* @returns whether nested files ride a Read's result here
270+
*/
271+
async function attachesOnRead($: EngineInterface): Promise<boolean> {
272+
const [simple, attachmentsOff] = await Promise.all([
273+
$.env.get('CLAUDE_CODE_SIMPLE'),
274+
$.env.get('CLAUDE_CODE_DISABLE_ATTACHMENTS'),
275+
])
276+
277+
return (
278+
!Switches.isSwitchedOn(simple) && !Switches.isSwitchedOn(attachmentsOff)
279+
)
280+
}
281+
250282
/**
251283
* The home directory a `~` in a Read's path stands for, read the way the
252284
* Read tool reads it.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './is-switched-on.js'
2+
3+
export * as default from '.'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* The spellings an environment switch is on with, as the engine reads its own.
3+
*/
4+
const ON_SPELLINGS: ReadonlySet<string> = new Set(['1', 'true', 'yes', 'on'])
5+
6+
/**
7+
* Whether an environment variable's value turns a switch on, read the way the
8+
* engine reads its own switches: `1`, `true`, `yes` or `on`, any case, trimmed.
9+
*
10+
* @param value the variable's value, or undefined when it is unset
11+
* @returns whether the switch is on
12+
*/
13+
export const isSwitchedOn = (value: string | undefined): boolean =>
14+
value !== undefined && ON_SPELLINGS.has(value.trim().toLowerCase())

0 commit comments

Comments
 (0)