Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/instruction-placement/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "instruction-placement",
"version": "0.18.3",
"version": "0.18.4",
"description": "Routes agent-instruction content to the surface that loads it at the right moment. The audit skill sweeps a repository's instruction layer and its ordinary markdown for content whose scope is narrower than the surface carrying it, meaning conventions keyed to one file type or one subtree sitting in an always-loaded CLAUDE.md or AGENTS.md, and for normative conventions stranded in documentation Claude never loads at all, then classifies each against a routing rubric and proposes a destination whose `paths:` glob is machine-validated before it is ever offered. Safety-class content (irreversible actions, secrets, data integrity, external publication, compliance, agent authority) is hard-denied from demotion and reported as held back rather than proposed, because demotion trades guaranteed presence for conditional presence: a deferred surface is absent until a read matches it, absent after a compaction until that trigger recurs, and never inherited by a subagent, which re-acquires it only by reading a covered path itself. Every accepted move regenerates an always-loaded index of deferred surfaces, which is what keeps a demoted rule discoverable from any context that has not happened to touch a path it covers. The audit is read-only and emits a diffable findings artifact; realignment is a separate skill gated per item with no blanket-approve path; a deterministic check skill gates that every rule glob still resolves and the index is current; a migrate skill moves a repository to AGENTS.md as the content home and keeps a CLAUDE.md shim while one is needed; and a setup skill verifies the one thing no other gate can see: that nothing in the repository stops Claude Code reading the index target, since a CLAUDE.md in the working directory or above it is read instead of the AGENTS.md beside it.",
"author": {
"name": "Melodic Software",
Expand Down
15 changes: 15 additions & 0 deletions plugins/instruction-placement/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
All notable changes to the `instruction-placement` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.18.4] - 2026-10-02

### Fixed

- `migrate`'s shim rule checks every directory between the repository root and a nested
`AGENTS.md` for a `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md`, with the same walk as
`ip_entry_points_on_path`, instead of only the directory holding the file. A blocker anywhere on
the path, or an unreadable directory, keeps every shim (#5800).
- The shim rule's `InstructionsLoaded` condition also scans the `hooks:` frontmatter of skill,
command and agent files in the repository, the config root, the plugins root, the managed
settings directory and each `--add-dir` directory the operator names, following symlinked
roots and links inside them. A hit, a root that cannot be resolved or read, a symlink loop, an
unnamed `--add-dir` set, or a source not on disk keeps the shim until the operator answers for
it (#5794).

## [0.18.3] - 2026-10-02

### Changed
Expand Down
21 changes: 21 additions & 0 deletions plugins/instruction-placement/scripts/lib/discover.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,27 @@ commit_all "$alt"
ip_index_target_loaded "$alt" "AGENTS.md" >/dev/null 2>&1
assert_eq "an import from .claude/CLAUDE.md also makes the target reachable" "0" "$?"

# ==========================================================================
# Entry points on a nested path: the walk migrate's shim rule (condition A)
# runs for each nested AGENTS.md
# ==========================================================================
walk="$(mktemp -d "$TMP/x.XXXX")"
mkdir -p "$walk/svc/deep/.claude" "$walk/other"
printf '@AGENTS.md\n' >"$walk/CLAUDE.md"
printf 'local\n' >"$walk/svc/CLAUDE.local.md"
printf '@AGENTS.md\n' >"$walk/svc/deep/CLAUDE.md"
printf 'x\n' >"$walk/svc/deep/.claude/CLAUDE.md"
printf 'x\n' >"$walk/other/CLAUDE.local.md"
out="$(ip_entry_points_on_path "$walk" "svc/deep")"
assert_lists "every entry point from a nested directory to the root is listed, nearest first" \
"$walk/svc/deep/CLAUDE.md
$walk/svc/deep/.claude/CLAUDE.md
$walk/svc/CLAUDE.local.md
$walk/CLAUDE.md" "$out"
assert_has "an intermediate CLAUDE.local.md above a nested AGENTS.md is a blocker on its path" \
"$out" "$walk/svc/CLAUDE.local.md"
assert_lacks "a sibling directory's CLAUDE.local.md is off the path" "$out" "$walk/other/CLAUDE.local.md"

# ==========================================================================
# Determinism
# ==========================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ three. A root that cannot be resolved here is unknown, and fails every condition

| # | Holds when | Read it from |
|---|---|---|
| A | Nothing takes precedence over an `AGENTS.md`: no `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md` at or above the working directory other than the shims going, **and** no directory holding a nested `AGENTS.md` keeps a `.claude/CLAUDE.md` or a non-shim `CLAUDE.md` or `CLAUDE.local.md` of its own | The ancestor walk below, run from each directory contributors start sessions in; a Glob for the three names at every level inside the repository, tracked or not; and the operator for the other machines, whose ancestors (a `~/work/CLAUDE.md`) and uncommitted `CLAUDE.local.md` files this machine cannot see |
| A | Nothing takes precedence over an `AGENTS.md`: no `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md` at or above the working directory other than the shims going, **and** no directory on the path from the repository root to a nested `AGENTS.md`, that directory included, holds a `.claude/CLAUDE.md` or a non-shim `CLAUDE.md` or `CLAUDE.local.md` | The ancestor walk below, run from each directory contributors start sessions in; the nested path walk below, over every `AGENTS.md` inside the repository, tracked or not; and the operator for the other machines, whose ancestors (a `~/work/CLAUDE.md`) and uncommitted `CLAUDE.local.md` files this machine cannot see |
| B | **Project instructions** on this machine reads `AGENTS.md` with no `CLAUDE.md`: `claude-md-or-agents-md` (the default) or `claude-md-and-agents-md` | `pluginConfigs["agents-md@builtin"].options.instructionFiles` in `<config>/settings.json`, managed settings and any `--settings` file; absent everywhere is the default. Project and local settings are ignored for it, so never read them as the answer. `claude-md` or `managed-only` fails |
| C | The loader is present and not disabled on this machine | `/harness-ops:inventory --bundled`, `builtin_plugins.cc-plugin-agents-md` (`in_loader`, `load`, `gated`, `gate_flags`), and no `enabledPlugins` entry set `false` in any scope (`<config>` user settings, managed, project, local, `--settings`) for `agents-md@builtin` or the `id` the lane prints. Inventory absent, the lane `broken`, or the entry missing is unknown. With `disableAllHooks` or `allowManagedHooksOnly` set `true` in any scope, C holds only on v2.1.287 or later, the build on which built-in mods are verified to keep running under both; an older or unknown version is unknown |
| D | Every user, machine and organization the repository serves reads `AGENTS.md` directly | Ask the operator, with the list below. Any yes, and any "don't know", fails |
| E | Nothing reachable through the `@` import graph of any `AGENTS.md`, root or nested, lies outside the working directory | The import-graph walk below, from every `AGENTS.md` and from each session start directory. Any `EXTERNAL`, `UNRESOLVED` or `DEPTH` row fails |
| F | No hook depends on `InstructionsLoaded` reporting the `AGENTS.md` load, or the operator accepts losing that | Every hook source, inside the repository and out: grep for `InstructionsLoaded` in the repository's `.claude/settings*.json`, hook scripts and CI; `<config>/settings.json` and `<config>/settings.local.json`; the managed settings file and any `managed-settings.d/` beside it; every `--settings` file contributors pass; each installed plugin's `hooks/hooks.json` and `plugin.json` `hooks` under `<plugins>`; and the plugins loaded per session or outside an install: ask the operator whether contributors use `--plugin-dir`, `--plugin-url` or `CLAUDE_CODE_PLUGIN_DIRS`, and grep each directory or archive they name, plus every `<config>/skills/*/` that holds a `.claude-plugin/plugin.json`. A source that cannot be read here, and every other machine, is the operator's to answer. A hit the operator has not accepted, or a source nobody can answer for, fails |
| F | No hook depends on `InstructionsLoaded` reporting the `AGENTS.md` load, or the operator accepts losing that | Every hook source, inside the repository and out: grep for `InstructionsLoaded` in the repository's `.claude/settings*.json`, hook scripts and CI; `<config>/settings.json` and `<config>/settings.local.json`; the managed settings file and any `managed-settings.d/` beside it; every `--settings` file contributors pass; each installed plugin's `hooks/hooks.json` and `plugin.json` `hooks` under `<plugins>`; the `hooks:` frontmatter of every skill, command and agent file in the repository, under `<config>`, under `<plugins>` and in the managed settings directory, by the frontmatter scan below; and the plugins loaded per session or outside an install: ask the operator whether contributors use `--plugin-dir`, `--plugin-url`, `CLAUDE_CODE_PLUGIN_DIRS` or `--add-dir`, and scan each directory or archive they name, plus every `<config>/skills/*/` that holds a `.claude-plugin/plugin.json`. A source that cannot be read here, and every other machine, is the operator's to answer. A hit the operator has not accepted, or a source nobody can answer for, fails |

B and C read this machine only. The setting is per user and no repository can ship it, so D is
where the operator answers for every other machine.
Expand Down Expand Up @@ -63,6 +63,38 @@ one exemption, and only while `<config>` resolves to `$HOME/.claude`. With `CLAU
pointing elsewhere, the page does not say whether that file still counts, so the row fails A. An
`UNREADABLE` row is unknown, and fails A.

## The nested path walk for condition A

A nested `AGENTS.md` is read only where no blocker sits between it and the repository root, so
check every directory on that path, not only the one holding the file. Any of the three names in
any directory on the path fails A, whatever the memory page's nested-load rule says about that
directory; the pointer is in [`sources.md`](sources.md), "Blockers between the root and a nested
`AGENTS.md`". The walk is this plugin's own `ip_entry_points_on_path` in
`<plugin-root>/scripts/lib/discover.sh`, which emits the three names in a directory and in every
directory above it up to the root; the readability check is added here because that function skips
an unreadable directory silently:

```bash
. <plugin-root>/scripts/lib/discover.sh
root=$(cd <repo> && pwd -P)
find "$root" -name AGENTS.md -not -path '*/.git/*' -print 2>/dev/null || printf 'UNREADABLE\tfind %s\n' "$root"
# then, for each nested AGENTS.md found (the root one is the ancestor walk's):
rel=<its directory, relative to $root>
d=$rel
while :; do
{ [ -r "$root/$d" ] && [ -x "$root/$d" ]; } || printf 'UNREADABLE\t%s\n' "$root/$d"
[ "$d" = . ] && break
d=$(dirname "$d")
done
ip_entry_points_on_path "$root" "$rel" | sed 's/^/FOUND\t/'
```

Every `FOUND` row that is not a shim `remove-shims` would remove fails A, and so does every
`UNREADABLE` row, the `find` one included: a directory `find` cannot enter may hold an `AGENTS.md`
nobody checked. Worked example: with `svc/deep/AGENTS.md` and a `svc/CLAUDE.local.md`, the walk for
`svc/deep` prints `FOUND <root>/svc/CLAUDE.local.md`. That file is no shim, so A fails and every
shim stays, the root's included.

## The import-graph walk for condition E

The walk reuses this plugin's own import model in `<plugin-root>/scripts/lib/discover.sh`, the
Expand Down Expand Up @@ -92,6 +124,75 @@ for a in <every AGENTS.md>; do e_walk "$(ip_realpath "$a")" 0; done
imports were already approved. `UNRESOLVED` is a target that is not a file. `DEPTH` is an import
past the fourth hop, which the loader drops, so the chain is not what it reads. Each fails E.

## The frontmatter scan for condition F

Condition F counts every skill, command-file and subagent frontmatter that names
`InstructionsLoaded` as a hook source, whenever and wherever that component might run. Which
components can declare hooks, which events they accept, when those hooks are active and where each
component loads from are read live at the pointers in [`sources.md`](sources.md), "Frontmatter
hooks and `InstructionsLoaded`"; a pointer that comes to say something narrower does not loosen
the scan until that record changes.

Every root is resolved to its canonical directory first, and `find -L` follows symlinks inside it,
so a linked root or a linked skill is scanned at its target. A root that cannot be resolved,
including a dangling link, prints `UNREADABLE`, and so does a tree `find` cannot finish, a
symlink loop included, because `find` then exits non-zero.

```bash
fm_gone() { # true only when <path> provably does not exist: the nearest ancestor that does is searchable
local p=$1 up
while ! [ -e "$p" ]; do
[ -L "$p" ] && return 1 # a dangling link is not absence
up=$(dirname "$p")
if [ -d "$up" ]; then [ -x "$up" ]; return; fi
[ "$up" = "$p" ] && return 1
p=$up
done
return 1
}
fm_scan() { # <root>...: frontmatter naming InstructionsLoaded, and every root not fully read.
# A root written ?<dir> is optional: skipped when provably absent, UNREADABLE when it cannot be
# stat'd. Any other root is expected, and UNREADABLE whenever it cannot be read.
local a d r f
for a; do
d=${a#\?}
if ! [ -e "$d" ]; then
[ "$a" != "$d" ] && fm_gone "$d" && continue
printf 'UNREADABLE\t%s\n' "$d"
continue
fi
r=$(cd -P -- "$d" 2>/dev/null && pwd -P) || { printf 'UNREADABLE\t%s\n' "$d"; continue; }
find -L "$r" -type f \( -path '*/skills/*/SKILL.md' -o -path '*/commands/*.md' \
-o -path '*/agents/*.md' \) -not -path '*/.git/*' -print 2>/dev/null ||
printf 'UNREADABLE\t%s\n' "$d"
done | while IFS= read -r f; do
case $f in UNREADABLE*) printf '%s\n' "$f"; continue ;; esac
[ -r "$f" ] || { printf 'UNREADABLE\t%s\n' "$f"; continue; }
awk 'NR == 1 { if ($0 !~ /^---[[:space:]]*$/) exit; next }
/^---[[:space:]]*$/ { exit }
/InstructionsLoaded/ { print "HIT\t" FILENAME; exit }' "$f"
done
}
fm_scan <repo> '?<config>/skills' '?<config>/commands' '?<config>/agents' '?<plugins>' \
'?<managed-settings-dir>/.claude/skills' <each --add-dir directory> \
<each --plugin-dir and CLAUDE_CODE_PLUGIN_DIRS entry>
```

Scanning the whole repository covers the root `.claude/` and every nested `.claude/skills/` and
`.claude/agents/`. The repository and every root the operator names are expected; the user,
plugin and managed locations may not exist on a given machine, so they are optional, but one
behind a directory that cannot be searched still prints `UNREADABLE`. Ask the operator which
directories contributors add with `--add-dir`, `/add-dir` or the Agent SDK's equivalent. Each one
named is an expected root and is scanned whole, a superset of whatever configuration the
`--add-dir` pointer in `sources.md` says it loads; any plugins it enables are already under
`<plugins>`.

A `HIT` the operator has not accepted fails F. An `UNREADABLE` row, a root that cannot be
resolved, an `--add-dir` set the operator cannot name, or a source that is not on disk here
(skills synced from a claude.ai account, subagents passed as `--agents` JSON or deployed through
managed settings, a `--plugin-url` archive, and every other machine) is unknown until the
operator answers for it, and fails F.

## What D asks the operator

Ask each, and record the answer beside the condition:
Expand Down Expand Up @@ -134,8 +235,8 @@ What the memory page states, and the verdict that follows:

- `claude-md-or-agents-md`: a subdirectory's `AGENTS.md` loads "when Claude opens a file there
with the Read tool and that subdirectory has none of the three `CLAUDE.md` files of its own".
Nested shims leave with the root only when condition A holds for every such subdirectory;
a subdirectory with its own `.claude/CLAUDE.md` or `CLAUDE.local.md` keeps its shim, and since
Nested shims leave with the root only when condition A holds for every such subdirectory and
every directory between it and the root; a blocker anywhere on that path keeps its shim, and since
`remove-shims` takes root and nested together, the repository keeps all of them.
- `claude-md-and-agents-md`: "each directory's `CLAUDE.md` files first and its `AGENTS.md` after
them"; the page does not say when a subdirectory's file loads. A repository with nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ Contents: [The remote flag](#the-remote-flag-and-how-its-code-default-is-read)
[Canary recipe](#the-canary-recipe) · [Shim removal cost](#what-shim-removal-costs) ·
[Measuring the cutover](#what-the-loss-means-for-measuring-the-cutover) ·
[Hook gap recheck](#page-recheck-of-the-hook-gap) ·
[Built-in agents-md plugin](#the-built-in-agents-md-plugin)
[Built-in agents-md plugin](#the-built-in-agents-md-plugin) ·
[Frontmatter hooks](#frontmatter-hooks-and-instructionsloaded) ·
[Nested path blockers](#blockers-between-the-root-and-a-nested-agentsmd)

## The remote flag, and how its code default is read

Expand Down Expand Up @@ -431,3 +433,42 @@ The record behind the skill body's `## Boundary` section for `cc-plugin-agents-m
under `disableAllHooks` or `allowManagedHooksOnly`, or stops listing `cc-plugin-agents-md` as
one; a changelog entry names `AGENTS.md`, `instructionFiles`, `projectInstructions` or the
`agents-md` plugin; or the README's "Setting the option" paragraph on the old key changes.

## Frontmatter hooks and `InstructionsLoaded`

The record behind condition F's frontmatter scan in [`shim-droppable.md`](shim-droppable.md).

Condition F treats any skill, command-file or subagent frontmatter that names
`InstructionsLoaded` as a hook that may depend on the shim, and scans every location the pointers
below give for those components, plus each `--add-dir` directory the operator names, scanned
whole. Anything the pointers leave unsaid (whether such a hook fires in a given session, or inside
a subagent) keeps the shim. Read the mechanics live at the pointers; this record holds only that
decision.

- **Pointer**: <https://code.claude.com/docs/en/hooks#hooks-in-skills-and-agents>,
<https://code.claude.com/docs/en/hooks#hook-locations>,
<https://code.claude.com/docs/en/sub-agents#hooks-in-subagent-frontmatter>,
<https://code.claude.com/docs/en/sub-agents#choose-the-subagent-scope>,
<https://code.claude.com/docs/en/skills#where-skills-live>,
<https://code.claude.com/docs/en/skills#frontmatter-reference> and
<https://code.claude.com/docs/en/permissions#additional-directories-grant-file-access-not-configuration>.
All four pages fetched by the rung-1 route on 2026-10-02, each slug in `llms.txt`.
- **As of**: 2026-10-02
- **Recheck trigger**: any of these sections changes which components can declare hooks, which
events they accept, when those hooks are active, where skills, command files or subagents load
from, or what `--add-dir` loads.

## Blockers between the root and a nested `AGENTS.md`

The record behind condition A's nested path walk in [`shim-droppable.md`](shim-droppable.md).

Condition A treats a `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md` in any directory
between the repository root and a nested `AGENTS.md` (for example `svc/CLAUDE.local.md` above
`svc/deep/AGENTS.md`) as a blocker for that nested file. The walk is the one `scripts/lib/discover.sh`
`ip_entry_points_on_path` already runs for the reachability verdict and the wiring gate.

- **Pointer**: <https://code.claude.com/docs/en/memory#when-claude-code-reads-agents-md>, fetched by
the rung-1 route on 2026-10-02, slug in `llms.txt`.
- **As of**: 2026-10-02
- **Recheck trigger**: that section states which directories' files stop a subdirectory's
`AGENTS.md` from loading.
Loading