The #113 sweep rested on CLAUDE.md's premise that rtk either filters or passes through unchanged, so prefixing is always safe. Measured against rtk 0.42.4, that premise is false for several of the commands the sweep prefixed, and two skills were left giving wrong answers silently. Why: - `rtk git worktree list --porcelain -z` discards both flags and renders its own format. The `locked`/`lock_reason` fields git-worktrees Step 2 must emit are absent entirely, and paths under $HOME are abbreviated to `~/`. - `rtk git branch --list <name>` prints a phantom `* ` line even when nothing matches, so git-branches' stated ambiguity test — "output from both means the name is ambiguous" — reported every name as ambiguous. `tag --list` is a clean passthrough, so only one half broke. - `rtk git diff --name-only`/`--name-status` append a `Changes:` trailer to output documented as "one per line"; `--word-diff` emits none of the `[-removed-] {+added+}` markers its table describes; `rtk git log -L` truncates each line at ~72 chars, on the one command whose purpose is showing line content. - `rtk git stash pop` prints only `FAILED: git stash pop`, swallowing the conflict diagnostic and retained-entry message the surrounding prose tells the agent to rely on. Implementation notes: - Eleven sites reverted to bare `git`, each carrying its reason inline so the next sweep does not undo it. `mergetool` and `rebase -i` are reverted on clause 3's interactive limb only: the TTY defect does not reproduce — rtk filters exactly twelve subcommands and execs the rest — and ADR-0023 records that measurement rather than a convenient one. - ADR-0023 states the rule repo-wide with a third clause: a command whose output the skill parses, or which is interactive, stays bare. `plugins/git/README.md` is reduced to a pointer; its claim that gitea skills "contain no git/rtk mentions at all" was false, and its citation of `hard-rules.md` pointed at a file containing no occurrence of "rtk". - Eight gitea sites swept, all verified byte-identical passthroughs first. - `scripts/check-rtk-prefix.sh` gates clause 1. Run against main's pre-sweep corpus it reports 99 findings including every gitea site, so it would have caught the drift #113 was filed about. Impact: the gate covers clause 1 only, in shell-tagged fences and the opening span of Run cells. Clause 2 is not gateable — "Run `git switch`" and "`git switch` refuses" are the same tokens — and prose bullets are invisible to it. Both limits are recorded in gates.md rather than left implied. Refs: #113 ADR: 0023 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EeH8SCbcrCAQrtymkNuhKP
12 KiB
The rtk prefix marks executable commands only, and is repo-wide
Status: accepted (2026-09-08).
Context
CLAUDE.md states the org convention as a golden rule: "Always prefix commands with rtk. If RTK
has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always
safe to use." Issue #113 observed that the rule had never been written down for skill prose, where
a git <subcommand> mention can be either an instruction to execute or a reference to the concept,
and that the corpus had drifted into carrying both spellings with no stated rule. PR #130 swept the
git plugin and recorded a two-way split in plugins/git/README.md.
Review found two defects in that sweep, and both are in the premise rather than the execution.
RTK is not output-transparent. rtk git --help enumerates twelve filtered subcommands — diff,
log, status, show, add, commit, push, pull, branch, fetch, stash, worktree.
Everything else is a true passthrough. Inside that set the filter is not a formatting preference; it
changes what the command reports. Measured against rtk 0.42.4:
| Command | What rtk does to it |
|---|---|
worktree list --porcelain -z |
discards both flags; no NUL separators, no locked/lock_reason field at all |
worktree list -v |
abbreviates /root/… to ~/…, collapses column alignment |
branch --list <name> |
emits a phantom * line even on no match |
diff --name-only / --name-status |
appends a blank line and a Changes: trailer |
diff --word-diff[=color|=porcelain] |
emits no [-removed-] {+added+} markers; substitutes a diffstat |
log -L |
truncates each diff body line at ~72 characters with an ellipsis |
stash pop (on conflict) |
prints only FAILED: git stash pop, swallowing CONFLICT, Unmerged paths and the retained-entry notice |
stash list (empty) |
prints No stashes where git prints nothing |
Every one of those falsified a skill that was written against the bare output. git-worktrees's
Step 2 required locked and lock_reason from a command whose rtk rendering has never carried
them; git-log-format.md documented [-removed-] {+added+} markers beside a command that no longer
produces them. The two-way split could not see any of this, because both halves of it are about what
a sentence is doing and none of it is about what the command does.
The rule is not git-plugin-scoped. plugins/git/README.md claimed the gitea-* skills
"contain no git/rtk mentions at all". Five gitea-* SKILL.md files run git remote get-url origin in a fenced ```bash Step block — the README's own canonical example of "executable,
instructed" — plus git branch --show-current in a reference file and three git remote -v in
gitea-orchestrate.agent.md. A convention stated inside one plugin's README is invisible from the
plugin next door, which is how those eight sites stayed bare through the sweep that existed to find
them.
Decision
One rule, three clauses, repo-wide — every plugins/*/.apm/skills/** and
plugins/*/.apm/agents/** file, not the git plugin alone.
- Executable and instructed →
rtk git. Anything telling the agent to run a command now: an imperative step, a dispatch-table "Run" cell, a fenced code-block procedure.rtk git push -u origin <branch>. - Illustrative or referential → bare
git. Naming a flag's behaviour, quoting a doc's own heading, describing a command in the abstract, warning against an anti-pattern. "git switchrefuses rather than clobbering conflicting local edits." - Machine-parsed or interactive → bare
git, and say why inline. A command whose output the skill parses, where rtk is in the filtered set above; or a command that hands control to an interactive child process.
Clause 3 is the new one and it looks arbitrary without the table in Context, which is why the
measurements are recorded here rather than left in a PR thread. It is applied per subcommand and per
flag, not per skill: tag --list stays prefixed because rtk passes it through byte-identically,
while branch --list two words away goes bare because it does not. git remote get-url origin,
git remote -v, git branch --show-current, git log --oneline -1 and git add -u were all
re-measured as byte-identical passthroughs and are therefore prefixed, parsing notwithstanding.
Two consequences of that per-subcommand basis are worth stating, because both are load-bearing and neither is comfortable:
- rtk's filtered set is a moving target.
git rebaseandgit mergetoolare passthroughs on 0.42.4 — verified underscript(1), both inherit a real TTY, contradicting an earlier report that they did not. They stay bare anyway, on the interactive limb: a token filter has nothing to offer a command that hands control to an editor, and the prefix would only buy exposure to whatever a later rtk version decides to do with those subcommands. The same reasoning makes the inner call in`rtk git remote add origin-push $(git config remote.origin.url)`bare while the outer stays prefixed —configpasses through cleanly today, but its stdout becomes a remote URL that is then force-pushed to, and that is not a blast radius to lend to a future filter change. branch --show-currentsits on the sharp edge. It is in the filtered set, it is parsed, and it is prefixed — on a measurement, in a subcommand whose sibling--listis exactly the defect clause 3 exists for. If rtk'sbranchfilter is ever extended, that is the first site to break. It is called out rather than hedged, because a rule whose exceptions are unrecorded is the state this ADR is replacing.
A clause-3 site says so inline, in a few words. "bare, not rtk: rtk prints a phantom * line
even on no match". Without it the next sweep re-prefixes the command, which is how #113 recurs.
The rule lives here, and docs/spec/gates.md carries the gate. plugins/git/README.md is
reduced to a pointer. It had also cited git-workflow/references/hard-rules.md as a place the rule
was written down; that file contains no occurrence of "rtk", and the citation is removed rather than
repaired.
Clause 1 is enforced by a check-rtk-prefix pre-commit hook; clauses 2 and 3 are not enforceable
and are not gated. The hook checks the two places a git mention is unambiguously an instruction —
a line in a shell-tagged code fence, and the opening backticked span of a "Run" column cell — and a
deliberately-bare command opts out with the literal string ADR-0023 on its own line. Its coverage
limits are recorded in docs/spec/gates.md, not smoothed over.
Considered options
Add compatibility: frontmatter to every skill. These six plugins are installable by third
parties, and a consumer who installs git from the marketplace has no rtk on their PATH. Every
prefixed command in the corpus is a plain git invocation with a word in front of it, so the prefix
is droppable: delete rtk and the command is correct. A compatibility: line per skill would
state that in a machine-readable field. Rejected on cost. It is 39 lines of frontmatter restating one
sentence, it is preloaded into every agent's context every session under ADR-0020's budget — the
field is not free the way a line in a doc is — and it has no consumer: nothing reads
compatibility:, so the field would be a comment with a colon in it. The consumer situation is
documented here and in plugins/git/README.md instead, which is where a human installing a plugin
actually looks. The same two-line note is owed to the other five plugin READMEs and is not yet
written.
Move rtk to the execution layer entirely. Skills instruct bare git throughout; CLAUDE.md's
session rule handles prefixing at the point of execution. This is the strongest rejected option and
it deserves the space: it closes the consumer gap and all eight output defects at once, because the
executing agent knows what it is about to parse and the skill does not have to predict it. It also
removes clause 3 entirely — there is nothing to except. Rejected because the prefix is lost wherever
an agent copies a command literally, which is the common case for a fenced procedure block and the
whole reason dispatch tables exist. The org convention's value is that the prefix is already there
in the text the agent lifts; a rule that relies on the agent remembering to add it is the rule that
produced the drift in the first place. Worth revisiting if rtk ever ships a shell shim, which would
make the execution layer transparent and this trade different.
Keep the two-way split and fix the eight sites by hand. Rejected: the split has no vocabulary for "this command is executable, instructed, and must still be bare", so the eight sites would be unexplained exceptions and the next sweep re-prefixes them. That is the failure this ADR exists to stop, not a smaller version of it.
Gate clauses 2 and 3 as well. Rejected as undecidable. "Run git switch <branch>" and "git switch refuses rather than clobbering local edits" are the same token sequence; separating them is a
judgement about what a sentence is doing. A gate that guessed would fire on correct content, and a
gate that fires on correct content gets added to SKIP, which disarms clause 1 along with it.
The boundary the rule does not decide
Two shapes in the corpus resisted the two-way split. The three-clause rule resolves one and does not resolve the other; both are recorded so an author meeting a third one knows which kind it is.
git-worktrees/SKILL.md's tracking row carries both spellings in one Run cell — rtk git worktree add --track -b <branch> <path> <remote>/<branch> — always correct. git worktree add <path> <branch> expands to exactly this. Resolved: the clauses apply per mention, not per row,
per cell or per file. The first is the instruction (clause 1), the second names what the first
expands to (clause 2), and one table cell can hold one of each. The rule needed no change; the
gate did, and it checks only a Run cell's opening span for exactly this reason.
git-submodules/references/setup-and-update.md:80 has a git command inside a quoted argument to
another command — rtk git submodule foreach 'git pull origin main || :'. Not resolved: all
three clauses describe a command the reading agent executes, and the inner git pull is not one.
It is the literal text of an argument that git submodule foreach hands to a subshell running inside
each submodule's own working tree, where the local convention does not reach. The file already gets
this right and already justifies it in prose two lines below ("the git calls in it are the
submodule's own — that is the one place a bare git is correct"). An author meeting this shape
should do the same: leave the inner command bare and justify it inline. It is deliberately not
promoted to a fourth clause on one instance. The gate does not decide it either — it happens to pass
this line, because the segment containing the inner command begins with rtk, and that is an
accident of the split rather than an understanding of quoting.
Consequences
Eleven sites in plugins/git/.apm/skills/** revert to bare git under clause 3, each carrying a
short inline reason. Eight sites across plugins/gitea/.apm/skills/** and
plugins/gitea/.apm/agents/gitea-orchestrate.agent.md gain the prefix under clause 1, and one in
pc-run/SKILL.md that the #130 sweep's grep missed because the backtick opens with SKIP= rather
than git . plugins/git/README.md's Conventions section becomes a pointer here, minus a paragraph
that was false about the gitea-* skills and a citation to a file that does not carry the rule.
A check-rtk-prefix pre-commit hook and tests/test-check-rtk-prefix.sh land with it; the test runs
the gate against the pre-sweep corpus on main and asserts it fails there, because a gate that only
passes on the fixed tree proves nothing about the drift it was written for.