ADR-068: Write-Time Guard and Append-Only Check for Accepted ADRs
Status
Section titled “Status”Accepted
Context
Section titled “Context”ADR-066 put the governance files behind permissions.ask
rules after one run of the 240-run test read ADR-052, renamed the
script it forecloses, and then edited 21 ADR files, a rule and a check so that everything
agreed. It left existing ADRs out, and said why: “a path rule cannot tell a new Proposed record
from an edit to an Accepted one. They are guarded by rule 1 and by review.” The files that run
went through first are therefore the ones with no guard.
The sibling repository, go-performance-starter, has guarded its records since 2026-07 with a PreToolUse hook that looks at whether the file already exists, and since 2026-10-12 with a check that reads the diff. On 2026-10-12 that repository was tested with and without its documents (clownware/go-performance-starter#162, 108 runs, three arms, two models). Two results bear on this decision:
- With the documents, no run edited a record, a rule or a check to fit its change. Without them,
every run that renamed the gate task also rewrote the check guarding it (11 of 11), five of
them with that check at
blockand the hooks on. The documents carry the decision; the machinery does not replace them. - The hook did what it was built for: it refused eleven hand-edits of generated files and no legitimate one. A hook only sees the edit tools. That harness refused shell writes by permission mode, so the shell route was closed by the harness and not by the repository.
This repository’s own history bears on it too. Replaying the check below over every commit since 2026-07-12 that modified an Accepted record: 23 commits, 11 with findings. Records here have been amended in place, for example a cron line in ADR-042 changed from nightly to weekly to monthly with no trace of the earlier values. Append-only is therefore a change of practice in this repository, not a description of it.
Limits. The Go test is one repository, three runs per cell, tasks written by a Claude model that knew the rules, and it never tested the guard as its own condition. Nothing here has been tested on this repository.
Decision Drivers
Section titled “Decision Drivers”- Guard the files the one observed rewrite went through, which ADR-066 could not
- Keep the maintainer able to amend a record in an ordinary session, as ADR-066 does for the other governance files
- Catch what a hook cannot see: a shell write, a script, a human’s editor
- Do not launch a gate that the repository’s own history fails
Considered Options
Section titled “Considered Options”Option 1: Leave existing ADRs to rule 1 and review (status quo)
Section titled “Option 1: Leave existing ADRs to rule 1 and review (status quo)”Pros: nothing to maintain; ADR-066’s governance-diff workflow already lists changed
records on a pull request.
Cons: the list says a record changed, not whether text was added or replaced. The one rewrite the test observed went through exactly these files.
Option 2: Deny every edit to an existing ADR, with a kill-switch
Section titled “Option 2: Deny every edit to an existing ADR, with a kill-switch”The Go starter’s design: the hook denies, and ADR_GUARD_OFF=1 lets the operator through.
Pros: strongest at write time.
Cons: an environment variable is a second mechanism beside the ask rules ADR-066 chose,
and the maintainer has to restart a session to amend a record.
Option 3: Ask at write time, report at diff time (this ADR)
Section titled “Option 3: Ask at write time, report at diff time (this ADR)”The hook returns ask for an existing record, which is the outcome ADR-066 chose for the other
governance files. A check reads the diff and reports accepted text that was removed or
rewritten, at warn.
Decision
Section titled “Decision”We will go with Option 3.
- Accepted records are append-only. An amendment appends a dated note, changes the Status
line, records a check’s move between
warnandblock, or fills the graduation log. A decision that no longer holds is superseded by a new record. Text is not rewritten in place. Formatting that keeps the words (re-wrapping, a code fence’s language tag, emphasis) is not a rewrite. Proposed records are drafts and may change freely. - A PreToolUse hook guards existing records at write time.
scripts/src/adr-guard.tsruns on Edit, Write and MultiEdit. For adocs/adr/NNN-*.mdfile that already exists it returnsask, with the legal moves in the reason: an interactive session puts the edit in front of the maintainer, and a session with nobody to approve is refused. A new record gets no verdict, so a Proposed ADR can still be added (ADR-066).AGENTS.mdis denied outright: it is generated (ADR-045) and has no legal hand-edit. - A check reports rewritten text.
adr-append-onlycompares every record that was Accepted on the default branch with the working tree and lists each removed or rewritten line. It runs in the enforcement suite locally, and in thegovernance-diffworkflow on a pull request, where it annotates the line. It starts atwarn. - The guard and the check are governance files.
scripts/src/adr-guard.tsandscripts/src/adr-append-only.tsjoin thepermissions.askrules and thegovernance-diffpaths.adr-guard-wiredreports a settings file whose hook is gone or no longer covers all three edit tools.
Implementation Details
Section titled “Implementation Details”- Both scripts import only Node built-ins. The hook starts with plain
node(Node 24 runs TypeScript directly), and the workflow runs the check without installing dependencies. - The check’s pure half takes a
git diff -U0and is unit-tested; the runner adds the diff to the snapshot asadrDiff. With no base to compare against (a shallow clone, a fresh fork) the check has no findings instead of a false one. - The CI quality job checks out shallow, so the check is skipped there by design. The
governance-diffworkflow already fetches full history and only runs when a governance file changes, so the pull-request signal costs seconds on those pull requests and nothing on the rest. - The hook’s
askand the path rules inpermissions.askare independent. A record edit asks once, through the hook.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- The files the observed rewrite went through now ask before they change
- A rewritten sentence in an Accepted record is named, with its line, on the pull request
- The maintainer amends a record in an ordinary session by answering one prompt
Negative
Section titled “Negative”- Every Edit, Write and MultiEdit starts a Node process, about 70 ms on the maintainer’s machine
- The check will report on amendments of the kind this repository has made until now. That is
the change of practice, and it is why the check does not start at
block - A hook cannot see a shell write. The check can, but only reports
Neutral
Section titled “Neutral”- ADR-066 gains an amendment note pointing here
- The registry gains two entries; its schema is unchanged
Validation
Section titled “Validation”- Metric 1:
adr-append-onlyreports nothing on ten consecutive pull requests that amend an Accepted record. Only then is graduation toblockconsidered, under ADR-064 - Metric 2: no legitimate edit is refused by the hook in an interactive session; an unattended session that tries to edit an existing record is refused
- Metric 3: a rerun of the script-rename task with the hook on, against ADR-066’s wording alone. Until that is run, the hook’s effect on this repository is untested
References
Section titled “References”- ADR-066 — the gap this closes
- ADR-064, ADR-045, ADR-035, ADR-052
- clownware/go-performance-starter#162 — the Go starter’s test; its ADR-033 is the design this ports
- chrispezza/portfolio#106 — the 240-run test of this repository
Enforcement
Section titled “Enforcement”- Testable consequences:
- TC-1: a record that is Accepted on the default branch loses no text.
- TC-2:
.claude/settings.jsonrunsscripts/src/adr-guard.tsbefore Edit, Write and MultiEdit. - TC-3: the guard asks for an existing record, says nothing for a new one, and denies
AGENTS.md.
- Checks:
- TC-1 → check
adr-append-only(status: warn) - TC-2 → check
adr-guard-wired(status: warn) - TC-3 →
scripts/src/__tests__/adr-guard.test.tsviatest:unit(status: block)
- TC-1 → check
- Not machine-checkable: whether an approval prompt was answered with thought; whether an appended note says what changed and why; whether a shell write to a record was intended.
- Graduation log: (empty at creation; entries added when a check changes status)
Date: 2026-10-12
Participants: Chris Pezza, Claude
Outcome: Accepted