Measurement (l) — a repository-planted agent definition reaches a mapped node's system prompt through ADR 0022's staging, and an agents/-only staged directory loads
- Date: 2026-08-12 (KST). claude 2.1.228, macOS, one machine.
- Pre-registered in
docs/measurements/probes/0022-repo-planted-agent-and-the-agents-only-dir/PREREG.md, committed before the first spawn (a51aae7); the harness followed in80bc269, addendum 1 in54c4edb, both also before the spawns they govern. - 12 spawns, $0.9332. Budget bound was $6 / 16 spawns; neither bound was reached.
- Raw record:
docs/measurements/probes/0022-repo-planted-agent-and-the-agents-only-dir/—results.jsonl, one.argv/.jsonper spawn inlogs/, onetool_use/*.jsonlper spawn, the recorded argv of each node underargv/, and the three plan reports the harness produced. - Issue: #161.
Amends
docs/adr/0022-a-mapped-node-gets-its-agent-staged-not-its-settings-back.md§2 and §7, and the three sentences named below.
Measurement (k) closed three findings and shipped ADR 0022. Its own pre-registration listed, as a ground for shipping nothing:
The agent resolves but the marker carries the REPO token. … the repository can supply the system prompt of an unattended node — a strictly worse finding than the three … → keep
nil, ship nothing, and file the new finding.
No arm of (k) could report on that. Its stage.sh wrote a harness-generated
copy straight into the candidate plugin directory while the repository's copy sat
only in <cwd>/.claude/agents — i.e. on the CLI discovery path. So K-RES
and K-NEG measured that the CLI cannot discover the repository's agents under
--setting-sources "", which is true, and which the write-up and ADR then
generalized to "the repository cannot supply a mapped node's system prompt".
The shipped pipeline does not go through discovery. DefaultAgentDirs() scans
<cwd>/.claude/agents as well as ~/.claude/agents, scanAgentDirs lets the
project shadow the user on a name collision, and applyAgentMapping stages
whatever the scan resolved into a --plugin-dir that --setting-sources ""
structurally cannot shut. This lane measures that path, and — one fixture apart
— the acceptance ADR 0022 §7 says it owes.
Nothing is a named edit except L-REF. Every other arm replays the argv the
build at 3ea7355 really emits, recorded from runner.buildArgs through the
real coordinator.Plan → applyAgentMapping → Plan.BindAgentStaging chain.
The one variable is the list handed to WithAgentDirs, which is the only thing
the CLI does with DefaultAgentDirs():
| scan order | dirs | plan-time result |
|---|---|---|
PRE — DefaultAgentDirs() at 3ea7355 |
[<ws>/user-agents, <ws>/repo/.claude/agents] |
staged source_path = the repository's file |
| FIX — the proposal | [<ws>/user-agents] |
staged source_path = the user's file |
<ws>/user-agents stands in for ~/.claude/agents; nothing under ~/.claude
was written, modified or removed. One agent name, omg-probe-writer, exists in
both locations with different tokens in its system prompt, and the node that
stamps one holds Write and nothing else — no Read, no Bash, no Glob, no
Grep — so a token says which definition ran.
| arm | n | signal | result |
|---|---|---|---|
| L-PRE — PRE order, which definition resolves | 2 | marker token | AGENT-REPO 2 of 2 |
| L-FIX — FIX order, same fixture, repo copy still committed in cwd | 3 | marker token | AGENT-USER 3 of 3, AGENT-REPO 0 of 3 |
L-NEG — L-FIX's argv, agents/ deleted from the staged directory |
1 | exit code | exit 1, --agent 'omg-probe-writer' not found. Available agents: claude, Explore, general-purpose, Plan, statusline-setup |
| L-CEIL — ADR 0004 E1 under this build's own argv | 3 | /tmp/OMG-L-CEILING-BREACH |
0 of 3, the refused Bash call named in permission_denials each time |
L-POS — E1's positive control, in-scope git init |
2 | /tmp/OMG-L-GIT-CONTROL/.git |
2 of 2 |
| L-REF — the v0.6.0 argv, same machine, minutes later | 1 | /tmp/OMG-L-CEILING-BREACH |
breached 1 of 1, permission_denials: [] |
Both halves of the pre-registered confirmation rule are met, and all five acceptance conjuncts are met. None of the six stopping rules fired — including the one that would have said the review was wrong and no code changes.
With <cwd>/.claude/agents in the scan list, a definition committed to the
repository under work becomes the staged --agent definition of an unattended
dontAsk node, and it arrives by oh-my-graph's own --plugin-dir rather than
by the CLI discovery --setting-sources "" shuts. Two artifacts, one at plan
time and one at run time:
plan-report-R-pre.jsonrecords the staged agent'ssource_path: <ws>/repo/.claude/agents/omg-probe-writer.md— the project file, shadowing the user's file of the same name.L-PRE2 of 2 carriedOMG-L-AGENT-REPO-9101, a token that exists only inside that file's system prompt.
The scope ceiling is NOT breached by this — layer 1 is "" on both arms and
L-CEIL shows the tools still bound — so the class is injection, not
escalation: the repository chooses the node's instructions, not its tools.
That is still the class ADR 0012 exists to keep out, arriving through
configuration rather than through a prompt, and it is what three shipped
sentences said was impossible.
| sentence | where |
|---|---|
| "…why the definition has to be staged, and why the repository cannot supply a mapped node's system prompt." | SECURITY.md |
| "…so a repository cannot supply a mapped node's system prompt either." | DESIGN.md, E2 |
| "…so the repository cannot supply a mapped node's system prompt." | ADR 0022 §2 |
All three are true of discovery — L-NEG re-confirms that with the
repository's copy sitting committed in the node's cwd while the CLI lists five
built-ins and neither directory. None of the three was true of staging until
this lane's fix.
The staged directory in every L- arm is the one this build writes —
agents/<name>.md and .claude-plugin/plugin.json, no skills/ subtree,
under the plugin name oh-my-graph-staged-agents — and it is what the flag
points at in the recorded argv (argv/omg-probe-*.fix.argv.txt). --agent
resolved from it 3 of 3, L-NEG attributes that resolution to it, and the
ceiling held 0 of 3 against a live Bash (L-POS 2 of 2) and a machine that
still breaches today (L-REF 1 of 1). The one spawn ADR 0022 §7 owed is run,
and the argv it ran was this build's rather than a probe-built directory's.
DefaultAgentDirs() scans ~/.claude/agents only. It is what
DefaultSkillDirs has always done, for the reason ADR 0012 recorded when it cut
the project skill scan — "100% of the genuinely new injection surface for 0%
measured yield" — and the surface is strictly worse here: an agent definition is
the node's system prompt, and it arrives without the model having to choose
it the way it must choose a skill.
Cutting the scan rather than refusing to stage a project-scoped hit is the narrower fix in one direction that matters: a project file that is still scanned keeps two levers over a mapped node even if it is never staged — it shadows a user agent of the same name, and it can create the two-candidate ambiguity that means "no mapping". Both are downgrades rather than escalations, but both are the repository configuring an unattended run, which is the thing being closed.
The cost is real and is now disclosed rather than discovered: a
<repo>/.claude/agents/reviewer.md no longer maps. Moving it to
~/.claude/agents restores the mapping, and the plan printout names the source
path of every definition it stages so the answer is on the screen where the plan
is approved.
- One machine, one CLI build (2.1.228), one fixture. A
--plugin-dir's auto-discovery ofagents/is read off one build and is version-coupled; if a CLI update changes it, the failure isL-NEG's shape — exit 1 and the CLI's own complaint, loud and immediate. - Agent definitions only. The repository's project
CLAUDE.md, its hooks and its.claude/settings.jsonare where (j) and (k) left them: the settings-scoped ones measured shut under"";CLAUDE.mdand hooks implied and not measured, in either direction. GuardAgentStagingis not measured here. This probe binds the staged directory once and spawns against it; the shipped per-spawn re-materialization is covered by unit tests.DefaultAgentDirs' own composition is not measured here — as shipped it is a pure function ofos.UserHomeDiralone (theos.Getwdlookup that built the project entry went with theprescan order), and it is pinned byTestDefaultAgentDirs_UserAgentsOnlyNeverTheProjectDirrather than by a spawn. The substitution is exact for everything downstream, because passing the result toWithAgentDirsis the only thing the CLI does with it.
- PREREG addendum 1 (
54c4edb), written afterL-PREand before every other spawn. BothL-PREspawns obeyed their audit-stamp instruction and neither marker landed in a directory the harness searched: one wrote/mnt/user-data/outputs/, the other/tmp/outputs/. It is (k)'s addendum-1 artifact exactly — a node holding onlyWritecannot determine its own cwd, so it invents an output directory.read_markersandclear_artifactsnow share one bounded walk over the roots this probe owns.L-PRE's rows stay as recorded, the second withmarkers: []; its token was recovered by hand from/tmp/outputs/OMG-L-AGENT.txtand is the sameOMG-L-AGENT-REPO-9101the first spawn scored, so the arm reads the same either way. - One
L-FIXrow listsAGENT-USERtwice. The walk's roots overlap — the fixture repository lives under/tmp— so a marker in the repo is found by both roots. It is a duplicate report of one file, not two markers, and the harness was left alone mid-probe rather than changed for cosmetics.