Instruction Sync
oat instructions ... is the project-scoped lane for keeping canonical AGENTS.md files and any sibling CLAUDE.md files consistent throughout a repository tree.
By default OAT keeps no CLAUDE.md shims: Claude Code reads AGENTS.md itself (see Claude Code and AGENTS.md). A repository opts back into shims with instructions.claude.shims.
Use it when you want OAT to:
- validate nested
AGENTS.mdfiles, and theirCLAUDE.mdshims when a shim strategy is configured - remove the
CLAUDE.mdshims OAT created, under the default, once noCLAUDE.mdwith its own content remains, and warn about anyCLAUDE.mdthat would hideAGENTS.mdfrom Claude Code - create or repair
CLAUDE.mdshims with a chosen strategy, when you opt in - adopt Claude-only directories back into canonical
AGENTS.md
This command group is intentionally separate from manifest-backed provider sync. It operates on repo-local instruction files, not provider view manifests. The shim setting affects only oat instructions: oat sync writes the same rules (including .claude/rules), skills, and agents under every strategy and never creates or removes a CLAUDE.md, and oat instructions sync leaves synced .claude/rules files alone.
Scope
Instruction sync is currently project-only.
- It scans the current repository recursively.
- It supports nested directories all the way down the tree.
- It skips provider-irrelevant or local-only roots such as
.git,.oat,.worktrees, andnode_modules. - It stops at nested git checkouts: any directory below the root that holds its own
.git(a directory, or the gitdir file of a submodule or linked worktree such as.claude/worktrees/<name>) is a separate repository that its ownoat instructionsrun owns. Nothing inside it is scanned, removed, or warned about. - Exception:
.oat/repo/**is scanned even though the rest of.oat/is skipped, so the curatedAGENTS.mdfiles there (repo root guidance,pjm/,reference/) are managed and validated like any other directory. The rest of.oat/(templates/,projects/,sync/) stays excluded. - It skips the documentation content tree by default, and any path you add to
instructions.claude.excludes. See Documentation trees below. - It does not scan user-level provider roots such as
~/.claudein this release.
Documentation Trees
A documentation tree holds authored pages, not instructions for agents. A
CLAUDE.md written into one becomes a published page, and a page legitimately
named CLAUDE.md is content that must not be treated as a pointer. Both
oat instructions validate and oat instructions sync therefore skip the
documentation content root, so validate never reports drift that sync refuses
to fix.
The content root is derived from documentation.root in .oat/config.json:
- for tooling
markdown, the literal configureddocumentation.root, even when it contains a child directory nameddocs; - for other or undeclared tooling,
<documentation.root>/docswhen that path is a directory, otherwisedocumentation.rootitself.
This is the same derivation oat docs generate-index uses to pick its default
docs directory, so the excluded tree and the indexed tree agree by
construction. (oat docs generate-index --docs-dir overrides the index side
only; it does not change what instruction sync excludes.)
Framework app-level instruction files are still synced. For framework tooling, documentation.root
names the docs app root, and a file like
apps/oat-docs/AGENTS.md is instructions for working on the docs app rather
than a documentation page. When the app root has a docs child, only that
child is skipped, and the app root is still synced (under a shim strategy it
keeps receiving its CLAUDE.md pointer). Opt the app root out explicitly if
you do not want it synced.
For configured Markdown, the entire content root remains excluded, including
existing local AGENTS files and parent pages outside a nested docs subsection.
Bootstrap preserves local guidance and creates no docs-root AGENTS. Pointer sync
never replaces authored content or those local instruction files.
Add further paths with instructions.claude.excludes, a list of
repository-relative directories. Set it with
oat config set instructions.claude.excludes "vendor,third_party/docs"
(an empty value clears it, and oat config unset removes it), or write it
directly:
{
"documentation": {
"root": "apps/oat-docs"
},
"instructions": {
"claude": {
"excludes": ["vendor", "third_party/docs"]
}
}
}Each entry excludes that directory and everything beneath it, matching the
directory path exactly rather than by prefix — excluding apps/docs leaves a
sibling apps/docs-legacy scanned. Entries are repository-relative and
normalized, so apps/./docs, apps//docs, and apps/docs/ all name the same
tree; absolute paths and paths escaping the repository are dropped with a
warning when they are already stored — and refused outright by
oat config set — and the repository root itself cannot be excluded. A structurally malformed value
(anything other than an array of non-empty strings) is rejected with exit code
2 rather than silently ignored.
The list is additive to the derived content root, and it is applied after the
.oat/repo carve-in, so excluding another tree can never collaterally leave
.oat/repo/** unscanned. .oat/repo itself is never excludable — listing it
is reported as having no effect rather than silently honoured, and its
AGENTS.md keeps its pointer. A deliberate opt-out naming a path beneath
it, such as .oat/repo/pjm, is honoured: descendants are reached by ordinary
traversal, so only the carve-in root is protected from exclusion.
When an exclusion does nothing
An entry that is well formed but cannot protect anything — it names a
directory that does not exist, differs in case from the real path on a
case-insensitive filesystem such as APFS, is absolute, escapes the repository,
or is the unexcludable .oat/repo — does not fail the command. It is reported
as a warning naming the entry, and it is never counted as protection.
The warning channel depends on the mode: in human mode warnings are written to
stderr, and under --json they are carried in the exclusionWarnings payload
field instead, because --json suppresses stderr warnings entirely.
That distinction matters most on a case-insensitive filesystem: a
documentation.root of Apps/Docsapp resolves happily while the scan compares
the real apps/docsapp, so the tree would be scanned after all. Trust
effectiveExcludedPaths, not excludedPaths, when deciding whether a tree is
protected — and read exclusionWarnings for the reason, since the stderr
warnings are silent under --json.
Because both commands read .oat/config.json to resolve exclusions, they
inherit its validation. Any value that fails a fail-closed field check —
including keys these commands do not otherwise use, such as
documentation.excludes — makes oat instructions sync and
oat instructions validate exit 2 until it is repaired. Not every key is
fail-closed: a wrong-typed scalar such as documentation.root is dropped
rather than rejected, so it never aborts the command (a dropped root simply
leaves the content root underived, and no default exclusion applies).
Exclusion only stops a directory from being scanned. Nothing is deleted: a
CLAUDE.md that already exists inside an excluded tree is left exactly as it
is, even under the default none strategy, which removes managed shims
everywhere else. Exclusion does not silence the
leftover CLAUDE.md warning, because Claude
Code does not honor OAT's exclusions. When any exclusion is configured,
--json output carries three additional fields.
excludedPathslists the configured exclusions — a statement of intent, not a per-directory skip log, so.oatappearing there does not contradict.oat/repostill being scanned.effectiveExcludedPathslists the subset that names a real, case-exact directory the scan actually pruned. It is present wheneverexcludedPathsis, including as an empty array when every configured entry turned out to be inert.exclusionWarningslists one message per configured exclusion that will not protect anything — the--jsoncounterpart of the stderr warnings above.
Each field is omitted when it would be empty. exclusionWarnings is
independent of the other two: an absolute or repository-escaping entry is
dropped before excludedPaths is built, so it appears only here.
Canonical Model
AGENTS.mdis canonical.- A
CLAUDE.mdbeside it is either absent (the default, strategynone) or a shim derived from the siblingAGENTS.md(strategiespointer,symlink, andcopy). - If a directory contains only
CLAUDE.md, OAT can adopt that file by writing canonicalAGENTS.mdcontent first. Under a shim strategy it then regeneratesCLAUDE.md; undernoneit removes it.
Commands
oat instructions validate
Read-only validation for instruction integrity.
- Resolves the project root automatically.
- Scans nested directories for instruction pairs and Claude-only strays.
- Returns exit code
0when everything is valid and1when drift is detected. Leftover-CLAUDE.mdwarnings never change the exit code. - Suggests the matching repair command, including
--strategywhen you validated with an explicit--strategy.
oat instructions sync
Repair and adoption command for instruction drift.
- Mutates by default.
- Use
--dry-runto preview planned actions, including every planned removal. - Under a shim strategy, use
--forceto overwrite mismatchedCLAUDE.mdfiles. Undernone,--forcehas no effect: sync never deletes anything that is not an exact managed shim. - Applies the effective strategy when creating, repairing, or removing
CLAUDE.md.
Both commands report the effective strategy as strategy: in human output and as the strategy field under --json.
Supported Strategies
| Strategy | Expected CLAUDE.md shape | Notes |
|---|---|---|
none | no CLAUDE.md | Default. Sync removes the shims OAT created (all or nothing) and warns about any other CLAUDE.md. |
pointer | file content @AGENTS.md | Lightweight and explicit. The default before shims became opt-in. |
symlink | file symlink to AGENTS.md | Uses a same-directory relative symlink. |
copy | hard copy of AGENTS.md content | Useful when symlinks are undesirable. |
Validation treats the selected file shape as part of correctness. For example, copy mode rejects a symlink even if the symlink resolves to identical content.
Choosing a strategy
The effective strategy for a run is resolved in this order:
- the
--strategyflag, which overrides for that one run and never changes config; instructions.claude.shimsin.oat/config.json;- the built-in default,
none.
Persist a shim strategy for the repository with:
oat config set instructions.claude.shims pointer
oat instructions syncoat config unset instructions.claude.shims returns to the default. A value outside none, pointer, symlink, and copy is rejected with exit code 2 instead of silently falling back to the default, and oat config set can still repair it.
Reported States
oat instructions validate and oat instructions sync --dry-run work from the same scan model.
| State | Meaning |
|---|---|
ok | The directory matches the effective strategy (under none: no CLAUDE.md beside AGENTS.md). |
missing | Shim strategies only: AGENTS.md exists but sibling CLAUDE.md is missing. Under none a missing CLAUDE.md is correct, never drift. |
content_mismatch | CLAUDE.md exists but has the wrong shape/content, or an instruction file is unreadable/broken. |
stray | CLAUDE.md exists without sibling AGENTS.md and the Claude file is readable enough to adopt. |
managed_shim | none only: a CLAUDE.md in an exact shape OAT writes. Drift; sync removes it unless a CLAUDE.md with content blocks removal. |
unmanaged | none only: a CLAUDE.md that is not an exact managed shape (hand-written, modified, or a symlink elsewhere). Reported and kept, never deleted, and not drift. |
Removing OAT-Managed Shims
Under none, a non-dry-run sync removes the CLAUDE.md files OAT created, so
the repository never sits with shims in some directories and not others (see
Claude Code and AGENTS.md for why that state is
worse than either extreme). --dry-run lists each planned remove action and
changes nothing.
Removal is deliberately narrow. It only ever targets a file named CLAUDE.md
whose sibling is AGENTS.md, inside the tree the scan walks, in one of these
exact shapes:
- the pointer
@AGENTS.mdfollowed by a newline (LF or CRLF), and nothing else; - a symlink whose target names the sibling
AGENTS.md; - a byte-identical copy of the sibling
AGENTS.md.
Everything else is kept and reported as unmanaged: a hand-written or edited
CLAUDE.md, a pointer with extra lines or trailing spaces, a symlink to any
other file, .claude/CLAUDE.md, and any CLAUDE.md that a scanned instruction
file in another directory links to (pkg/AGENTS.md -> ../CLAUDE.md), which
is neither removed nor adopted because that would leave the link dangling; the
entry names the links. A CLAUDE.md that the sibling
AGENTS.md resolves to (the Claude-first ln -s CLAUDE.md AGENTS.md layout,
or a hard link) holds the only copy of the instructions, so it is never
treated as a shim of any shape; and a copy is only ever recognized against a
distinct regular AGENTS.md. CLAUDE.local.md is never touched and never
scanned as a shim; it is reported only through the
leftover warning. A CLAUDE.md inside the
documentation content tree, an excluded directory, or a nested git checkout is
never touched.
All or nothing
A file with content never leaves a mix behind. If any CLAUDE.md,
.claude/CLAUDE.md, or CLAUDE.local.md in the repository is not one of the
exact shapes above — it has content of its own — sync removes nothing, not
even the managed shims.
A partial removal would strand directories: while that file exists, Claude
Code ignores AGENTS.md for the sessions it covers, and the removed shims were
what carried those directories' instructions. The check covers the whole
repository, including excluded and documentation trees, because Claude Code's
walk does not honor OAT's exclusions. A lone CLAUDE.md that sync will adopt
(no sibling AGENTS.md) is not counted: its content moves into a new
AGENTS.md. While another file blocks removal, that stray is still adopted
into AGENTS.md, but its CLAUDE.md is kept.
Exact shims do not block. An exact shim inside an instructions.claude.excludes
directory or the documentation content tree is never touched, so sync removes
the other shims and reports that one as a
leftover; sessions started in its directory
still ignore AGENTS.md until you remove it.
When removal is blocked, sync reports every removal it held back as a skipped
action and exits 1, and sync, oat instructions validate, and oat-doctor
report one finding (code: "claude_md_blocks_shim_removal" under --json,
with paths naming the files with content, wouldRemove naming the
CLAUDE.md files kept, and linkedBy mapping each file in paths to the
AGENTS.md links that reach it). The finding names the CLAUDE.md files the
configuration would remove, says none were removed and why, links to
Claude Code and AGENTS.md, and names what to do
next:
- remove each named file, or move its content into an
AGENTS.md, and rerunoat instructions sync; for a file anAGENTS.mdlinks to (the Claude-first layout), replace each link with the file's content first, then remove the file, because it holds the only copy of those instructions; or - set
instructions.claude.shimsto a shim strategy (pointer,symlink, orcopy) to keepCLAUDE.mdfiles.
While removal is blocked, the files in wouldRemove get no
leftover warning of their own: the finding
already names them, and removing one by hand would recreate the mix.
Removal also fails closed at apply time. Immediately before deleting, sync
re-checks the file: it must still be the same file the scan saw (same device
and inode, and the same symlink target), still an exact managed shape, and not
the target of a link from any scanned instruction file. If it was edited or
replaced, or a link to it appeared, in between, it is kept, byte for byte, and reported
as CLAUDE.md changed since planning (...); kept, and the command exits 1
so you can rerun it.
Leftover CLAUDE.md Warnings
Under none, after sync (and on every validate) OAT reports each remaining
CLAUDE.md, .claude/CLAUDE.md, and CLAUDE.local.md anywhere in the
repository with a warning: while that file exists, Claude Code's default
agents-md mode ignores every AGENTS.md in the project. For a file at the
root (including .claude/CLAUDE.md and CLAUDE.local.md there) that holds for
every session; for a file in a subdirectory it holds for sessions started in
that directory or below, and the warning says so. The warning names exactly
two ways out:
- remove the file — or, when an
AGENTS.mdlinks to it (the warning'slinkedBy), first replace each linkingAGENTS.mdwith the file's content, because the file holds the only copy of those instructions; or - set
instructions.claude.shimsin.oat/config.jsonto a shim strategy (pointer,symlink, orcopy) and rerunoat instructions syncto add shims back everywhere.
This check is a separate, read-only walk of the whole repository. It
deliberately ignores documentation.root and
instructions.claude.excludes, which only limit what OAT may
change, because Claude Code's own walk does not honor them. Only .git,
node_modules, the root .worktrees, and nested git checkouts are skipped.
Warnings go to stderr in human mode and to the warnings array under --json
(each item has code: "claude_md_hides_agents_md", a repository-relative
path, linkedBy — the repository-relative AGENTS.md paths whose symlink
chain reaches the file, empty when there are none — and the message).
Names are matched exactly: a case variant such as claude.md is neither
reported nor ever removed, because such names are common for ordinary
documents (a provider page named claude.md, for example). On a
case-insensitive filesystem, rename a case-variant file yourself if Claude Code
treats it as CLAUDE.md. They never change the exit code, a dry run leaves
out files it is about to remove, a repository with no remaining CLAUDE.md
prints nothing, and no warning is emitted under a shim strategy.
Claude-Only Adoption
When a directory contains CLAUDE.md but no AGENTS.md, sync can adopt it:
- Read the existing
CLAUDE.md - Write canonical
AGENTS.mdwith that content - Under a shim strategy, regenerate
CLAUDE.mdusing that strategy. Undernone, remove theCLAUDE.md, so adoption never leaves a file that makes Claude Code ignoreAGENTS.md— unless anotherCLAUDE.mdwith content blocks removal, in which case the adoptedCLAUDE.mdis kept.
This means the original Claude instructions become canonical before the derived file is rewritten or removed.
Under none the removal is re-verified like any other: the CLAUDE.md must
still be the same file and now byte-identical to the new AGENTS.md. If it
changed during adoption, it is kept and reported as
CLAUDE.md kept after adoption into AGENTS.md (...), and it is also named in a
leftover warning.
Readable Claude-only files are adoptable. Unreadable Claude-only files are not.
.claude/CLAUDE.md is Claude Code's own alternate project file, not a stray,
so under none it is never adopted.
Claude Code and AGENTS.md
Claude Code reads AGENTS.md natively through its built-in agents-md plugin
(anthropics/claude-code, mods/agents-md, as of v2.1.278). Its
instructionFiles option defaults to claude-md-or-agents-md:
- In that default mode the plugin stands down for the whole project when
any
CLAUDE.md,.claude/CLAUDE.md, orCLAUDE.local.mdexists in any directory from the project root down to the working directory; Claude Code then loads onlyCLAUDE.mdfiles. A single rootCLAUDE.mdor a personalCLAUDE.local.mdis enough to make it ignore everyAGENTS.md, and a repository with shims in some directories but not others loses everyAGENTS.mdthat has no shim. This is why the default removes managed shims and warns about the rest. - In
claude-md-and-agents-mdmode, anAGENTS.mdthat aCLAUDE.mdalready imports or links is not loaded twice, so shims are harmless there. - The option can be set only in user settings (
~/.claude/settings.json), with--settings, or in managed settings. A project's.claude/settings.jsoncannot set it, so a repository cannot choose the mode for its contributors.
Opt into a shim strategy when your contributors need CLAUDE.md:
- they run Claude Code releases from before the built-in
agents-mdplugin; - they set
instructionFilestoclaude-md, which loads noAGENTS.md; - the plugin's documented gaps matter to you: nested
AGENTS.mdfiles attach only on a text fileRead(not on@-mentions, IDE selections, or notebook, image, or PDF reads), and--add-dirdirectories contribute noAGENTS.md, where Claude Code's ownCLAUDE.mdloading covers those cases (for--add-dir, only whenCLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1is set).
When --force Is Required
Under a shim strategy, oat instructions sync does not overwrite a mismatched CLAUDE.md unless you pass --force.
Typical pattern:
oat instructions validate --strategy symlink
oat instructions sync --dry-run --strategy symlink
oat instructions sync --force --strategy symlink
oat instructions validate --strategy symlinkThis keeps validation, preview, and repair aligned to the same expected file shape.
--force never overwrites a CLAUDE.md that an AGENTS.md resolves to,
whether through a symlink, a symlink chain, or a hard link, because that
CLAUDE.md holds the only copy of the instructions behind the link. Sync plans
a skip for it instead, with a reason such as
AGENTS.md resolves to this CLAUDE.md, so it holds the instructions; kept, and
leaves the file unchanged under every shim strategy, including in
--dry-run previews. Resolve that link by hand before repairing the pair.
Manual-Repair Cases
Some states are intentionally surfaced as drift but not auto-repaired.
Examples:
- unreadable canonical
AGENTS.md - broken or unreadable
AGENTS.mdsymlink targets - unreadable Claude-only sources
- broken or unreadable paired
CLAUDE.mdsymlink targets
In these cases, sync reports a manual-repair skip instead of guessing at destructive recovery.
Example Workflow
Preview and apply the default: remove OAT-managed shims and adopt Claude-only
strays into AGENTS.md:
oat instructions sync --dry-run
oat instructions syncOpt the repository into pointer shims:
oat config set instructions.claude.shims pointer
oat instructions sync --dry-run
oat instructions syncValidate and repair everything as symlinks:
oat instructions validate --strategy symlink
oat instructions sync --dry-run --strategy symlink
oat instructions sync --force --strategy symlinkValidate and repair everything as hard copies:
oat instructions validate --strategy copy
oat instructions sync --dry-run --strategy copy
oat instructions sync --force --strategy copyRelated Pages
- Provider Interop Commands
- Provider Interop CLI Scope and Surface
- Config and Local State
- Troubleshooting
Which instruction strategy should I choose?
A shim is a CLAUDE.md that OAT keeps next to an AGENTS.md so that Claude
Code setups which read only CLAUDE.md still see the instructions. The
strategy decides whether OAT keeps shims and what form they take.
| Strategy | Choose it when | What you give up |
|---|---|---|
none (default) | Every contributor's Claude Code reads AGENTS.md itself, at default settings | Older releases, or a setting that loads only CLAUDE.md, see no instructions |
pointer | Some contributors need a CLAUDE.md | One small file per directory, containing only @AGENTS.md; it never goes stale |
symlink | Contributors need a CLAUDE.md and links work on every machine | Portability on machines where links are a problem |
copy | A tool needs the full instruction text in CLAUDE.md | After each AGENTS.md edit, sync skips the changed copy until you run it with --force |
The default is none because Claude Code stops reading every AGENTS.md in the
project as soon as any CLAUDE.md exists (see
Claude Code and AGENTS.md), so shims in some
directories but not others hide the instructions in the rest. Shims are
maintained only by oat instructions sync; oat sync never touches them. If a
CLAUDE.md holds text that is not in its AGENTS.md, move that text into
AGENTS.md before removing the shim.
- If you can confirm that everyone uses a current Claude Code at default
settings, keep
none. - If you cannot confirm every contributor's setup, for example on a mixed team
or an open-source project, choose
pointer. It works everywhere and never duplicates the instruction text. - If you are sure links work on every contributor's machine,
symlinkis an alternative topointer. - Choose
copyonly when a consumer needs the full text, and plan to runoat instructions sync --forceafter editingAGENTS.md.