# AGENTS.md — internal/mirror **One filesystem invariant, in every directory of a tree:** ``` AGENTS.md is the real file; CLAUDE.md is a symlink pointing at it. ``` **Imports the standard library and nothing else** — no issue, no tracker, no configuration, no login. That is not a stylistic preference: this walks whatever directory it is pointed at, on any machine, and a package that reached for a project's configuration could not be run outside a project. Two tests hold it, see [`internal/AGENTS.md`](../AGENTS.md). | file | what is in it | |---|---| | `mirror.go` | `Sync`, `Check`, `Result`, and `fixDir` — the seven branches | | `mirror_test.go` | one case per branch, plus the walk, the skip list and idempotence | | `layering_test.go` | the two tests that keep this package at the bottom | ## Why the invariant Two agent harnesses read two different filenames for the same document. A repository that keeps both as real files keeps **two documents**, and they drift — silently, until somebody reads the stale one and believes it. One real file with a link beside it is the only arrangement where that cannot happen. `AGENTS.md` is the real file rather than `CLAUDE.md` because the convention is not one vendor's: a repository that names its documents after a single tool has picked a side it did not need to pick. ## The seven branches Every one of them is either lossless or a refusal. **Nothing here deletes content.** | starting state | what happens | |---|---| | `AGENTS.md` real, no `CLAUDE.md` | create the symlink | | `CLAUDE.md` real, no `AGENTS.md` | rename to `AGENTS.md`, link back | | `CLAUDE.md` symlink → `AGENTS.md` | canonical; nothing to do | | `CLAUDE.md` symlink elsewhere | re-point it | | `AGENTS.md` symlink → real `CLAUDE.md` | reversed layout; swap it round | | both real, identical content | replace `CLAUDE.md` with the symlink | | both real, **different** content | **refuse**, and name the directory | The last row is the reason the other six can be automatic. One of those two files is somebody's writing and no rule here knows which, so a merge is not attempted and not offered — the conflict is reported and the directory is left exactly as it was. Two smaller refusals sit beside it, both about a symlink with no target: a broken `AGENTS.md` with no `CLAUDE.md` beside it, and a `CLAUDE.md` pointing at something that is gone. Neither is repairable without inventing content. **The link target is relative.** `CLAUDE.md -> AGENTS.md`, never an absolute path: a tree that is moved, copied, cloned or mounted somewhere else keeps working, and an absolute link would point at wherever the repair happened to run. ## Two properties the tests hold **A repair that fails halfway reports nothing.** An unwritable directory, a race with an editor — the fix is abandoned and no line is added. Claiming a repair that did not happen is worse than silence: the next run finds the same state, and the operator has now been told twice that it was handled. **Sync converges in one pass.** Every case in the table runs `Sync` twice and fails if the second run still has work. This matters more here than it looks: the command that wraps this package runs on `PreToolUse(Bash)`, so a state that reported itself fixed without converging would re-report on every Bash call, forever. `Check` is the same walk with the writes turned off — one code path, not a second implementation that might disagree — so a check that says nothing is a promise about the run that follows it. ## What is skipped `node_modules`, `__pycache__`, `venv`, `vendor`, and every dot-directory. Somebody else's tree is somebody else's business: a vendored dependency's `AGENTS.md` rewritten here is a diff nobody asked for. `.git` gets the same treatment for a second reason — it is not a place to be creating symlinks. ## What does not belong here The JSON a `PreToolUse` hook reads and writes, the decision to run at all, and the exit status. Those are [`cmd`](../cmd/AGENTS.md)'s, in `mirror.go` there; this package takes a directory and returns a `Result`. That split is what lets the repair be tested against a temp directory without a hook payload anywhere near it. ## Keeping this file true - **Scope:** `mirror.go` and its tests — the invariant, the seven branches, the skip list, and the two properties above. - **Update it when** a branch is added or its outcome changes (the table is the contract), a directory is added to or dropped from the skip list, or the link stops being relative. - **Do not** document the command, the hook payload or the exit codes here. [`internal/cmd`](../cmd/AGENTS.md) owns those.