Most teams that let a model draft documentation hand out permission per page or per section, which means every paragraph inherits the trust level of the easiest one on the page. That arrangement holds until a model writes a single sentence that promises timing, ordering, or compatibility inside an otherwise harmless tutorial. The problem is not tone or style: it is a promise that nobody owns, and it survives review because reviewers read prose instead of obligations.

This workflow moves the permission boundary down to the sentence and attaches a signatory to the small set of sentences that behave like contracts. Three artifacts carry it: a front-matter list of regions a model may draft, a registry of signed claims, and a gate script that fails a pull request when an unsigned or expired claim appears. The script and transcripts below are illustrative expected output for the sample files rather than measured results from a real repository, so reproduce them locally before trusting the gate in CI.

The three author modes

Before writing any code, decide which sentences are machine-draftable, which are machine-draftable but human-signed, and which are human-only. The table below is the decision rule I would defend in review; adjust the trigger column to your stack, but keep the separation between "drafted" and "signed" intact.

Claim class Example sentence Who may write Evidence on file Expires when
Navigation prose "Open Settings, then select API keys." Model UI label exists Never, normal review
Field description "retry_after is an integer count of seconds." Model, generated from schema Schema path Schema file changes
Behavioural promise "Retries are attempted at most 3 times." Model drafts, human signs Probe command + signed commit Cited source file changes
External contract "Requests above 100/min receive 429." Human only Probe + owner + ticket Service config changes
Deprecation "v1/charge is deprecated as of 2026-11." Human + owner Changelog entry Release notes change
Security or permission "The token may read but not delete." Human + security reviewer Policy document Policy changes

The last four rows are the ones the gate enforces. The first two rows are deliberately outside it, because a gate that fires on every sentence gets disabled within a sprint.

Step 1: Declare what a model may draft

Every doc file opens with front matter listing the region ids a model is allowed to touch, and each drafted region is wrapped in HTML comments. Anything outside a marked region is human text by definition, so a model that wants more room has to ask for a front-matter change in a visible diff.

--- draftable: [retries, payload] --- # Webhooks Consumers must acknowledge a delivery within 5 seconds or the attempt is considered failed. <!-- draft:start id=retries --> Retries are attempted at most 3 times within 10 seconds. <!-- draft:end --> <!-- draft:start id=payload --> The payload is sent as JSON. <!-- draft:end --> 

The gate fails when a marker id is missing from draftable, which is the cheapest defence against a later commit quietly widening the model's territory. Keep the marker ids short and stable; they are referenced in review comments more often than you expect.

Step 2: Keep a registry of signed claims

A claim enters the registry only after a human has checked the behaviour and run whatever probe proves it. The text field must match the sentence verbatim after whitespace collapsing, because the gate keys signatures by a hash of the normalised sentence.

# docs/claims.yaml version: 1 claims: - id: ack-window text: Consumers must acknowledge a delivery within 5 seconds or the attempt is considered failed. owner: "@avery" signed_at_commit: 4c1d9f2 files: - internal/delivery/worker.go probe: "go test ./internal/delivery -run TestAckWindow" - id: retry-window text: Retries are attempted at most 3 times within 10 seconds. owner: "@avery" signed_at_commit: 4c1d9f2 files: - internal/delivery/retry.go probe: "go test ./internal/delivery -run TestRetryWindow" 

Two properties matter here. Re-phrasing a signed sentence drops its signature and the gate fails closed, which is noisy but errs in the safe direction. The files list is the expiry mechanism: a signature is only valid until one of the cited source files changes after signed_at_commit.

Step 3: The gate

The script classifies sentences with a keyword list, splits the document into model and human regions, and checks the git history for staleness. Save it as tools/claimgate.py; it needs only pyyaml beyond the standard library.

#!/usr/bin/env python3 """claimgate: fail a docs change that adds an unsigned or expired claim.""" import hashlib import pathlib import re import subprocess import sys import yaml CLAIM_WORDS = re.compile( r"\b(must not|must|never|always|guarantee[sd]?|at most|at least|" r"no later than|within \d+\s?(?:ms|s|sec|seconds|min)|" r"breaking|deprecated|only if|rejects?)\b", re.IGNORECASE, ) MARKER = re.compile(r"<!--\s*draft:(start|end)(?:\s+id=([\w-]+))?\s*-->") FENCE = re.compile(r"``` .*? ```", re.DOTALL) def claim_key(text): flat = re.sub(r"\s+", " ", text.strip().lower()) return hashlib.sha1(flat.encode()).hexdigest()[:12] def segments(markdown): pos, inside = 0, False for m in MARKER.finditer(markdown): yield inside, markdown[pos:m.start()] inside = m.group(1) == "start" pos = m.end() yield inside, markdown[pos:] def sentences(text): text = FENCE.sub("", text) # examples are not claims  for para in re.split(r"\n\s*\n", text): for s in re.split(r"(?<=[.!?])\s+", para.strip()): s = " ".join(s.split()) if s: yield s def draftable_sections(markdown): m = re.match(r"^---\n(.*?)\n---", markdown, re.DOTALL) front = yaml.safe_load(m.group(1)) if m else {} return set((front or {}).get("draftable") or []) def commits_since(rev, paths): if not paths: return 0 out = subprocess.run( ["git", "rev-list", "--count", f"{rev}..HEAD", "--", *paths], capture_output=True, text=True, check=True, ) return int(out.stdout.strip()) def main(paths): registry = yaml.safe_load(pathlib.Path("docs/claims.yaml").read_text()) signed = {claim_key(c["text"]): c for c in registry["claims"]} problems, checked = [], 0 for path in paths: markdown = pathlib.Path(path).read_text() allowed = draftable_sections(markdown) used = {m.group(2) for m in MARKER.finditer(markdown) if m.group(1) == "start" and m.group(2)} for bad in sorted(used - allowed): problems.append(f"{path}: draft region '{bad}' is not declared in front matter") for in_draft, chunk in segments(markdown): for s in sentences(chunk): if not CLAIM_WORDS.search(s): continue checked += 1 where = "model region" if in_draft else "human text" entry = signed.get(claim_key(s)) if entry is None: problems.append(f"{path} [{where}]: unsigned claim: {s[:90]}") continue stale = commits_since(entry["signed_at_commit"], entry.get("files", [])) if stale: problems.append( f"{path} [{where}]: claim '{entry['id']}' needs re-signoff, " f"{stale} commit(s) touched {entry['files']}" ) for p in problems: print("FAIL", p) print(f"claimgate: {checked} claim sentence(s) checked, {len(problems)} problem(s)") return 1 if problems else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) 

The gate records probes but never runs them, which is a deliberate limit: an auto-executed probe invites people to sign anything green, and the signature is supposed to mean a human read the sentence against the behaviour.

Step 4: Reproduce the three outcomes

Run the gate against the sample file above with an empty registry. Expected output is three problems, and note that both the model region and the human sentence are flagged.

$ python tools/claimgate.py docs/webhooks.md FAIL docs/webhooks.md: draft region 'payload' is not declared in front matter FAIL docs/webhooks.md [human text]: unsigned claim: Consumers must acknowledge a delivery within 5 seconds or the attempt is considered failed. FAIL docs/webhooks.md [model region]: unsigned claim: Retries are attempted at most 3 times within 10 seconds. claimgate: 2 claim sentence(s) checked, 3 problem(s) 

After declaring both regions and adding the two registry entries with signed_at_commit: $(git rev-parse --short HEAD), the run is clean.

$ python tools/claimgate.py docs/webhooks.md claimgate: 2 claim sentence(s) checked, 0 problem(s) 

Now change one line in internal/delivery/retry.go, commit it, and run again. The retry claim expires on its own while the acknowledgement claim stays signed, which is the behaviour you want: expiry follows the proof, not the calendar.

$ python tools/claimgate.py docs/webhooks.md FAIL docs/webhooks.md [model region]: claim 'retry-window' needs re-signoff, 1 commit(s) touched ['internal/delivery/retry.go'] claimgate: 2 claim sentence(s) checked, 1 problem(s) 

Step 5: Run it where the docs change

The job below runs on pull requests that touch docs or source, and fetch-depth: 0 is required because the staleness check walks real history.

name: docs-claims on: pull_request: paths: ["docs/**", "internal/**", "src/**"] jobs: claimgate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: "3.11" - run: pip install pyyaml - run: python tools/claimgate.py $(git ls-files 'docs/*.md') 

The script is provider-agnostic, so the drafting side matters less than the boundary. Where a team has no paid runner, the operator describes MonkeyCode as offering free model access and a free server option; the free model access fits the drafting half, since the gate only cares that text lands inside declared regions, and the free server option can host the job for repositories that cannot spend CI minutes on a docs-only check. I have not verified quotas, hardware, retention, or durability for either, so treat those specifics as unconfirmed and measure them yourself before depending on them for gated merges.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

What the human still owns

  1. Writing the sentence in the first place when it is an external contract, a deprecation, or a permission statement; the model may propose, but the signatory writes or rewrites.
  2. Running the probe named in the registry entry and confirming that the observed behaviour matches the sentence, including units and rounding.
  3. Choosing the files list, which is the real editorial judgement: cite the file that would have to change for the sentence to become false.
  4. Re-signing after an expiry, which is a review of the behaviour rather than a rubber stamp on the diff.
  5. Pruning sentences that no longer deserve to be promises at all, because the cheapest fix for an expired claim is often to stop making it.

Limitations

  • The keyword list is a heuristic. Passive constructions, table cells, front-matter fields, and non-English pages will slip past it, so keep the list short and review its misses monthly.
  • Hashing normalised sentence text means a comma edit invalidates a signature. That fails safe but generates churn, and teams that reflow prose will feel it.
  • Git-based expiry only sees tracked file changes. A vendor changing their rate limits does not create a commit, so external contracts need an owner who watches the vendor, not just the repository.
  • Signatures can be forged by a tired reviewer who approves without running the probe. The gate raises the cost of that behaviour; it does not detect it.
  • The registry does not replace a documentation test suite. It tracks who promised what, not whether an example actually runs.

Who should not use this

Skip it if you maintain one README with no behavioural promises, or if you have no CI to run the gate. Skip it as well if your docs are mostly tutorials whose value comes from worked examples rather than guarantees, because the registry will collect empty entries and rot. And skip it if nobody on the team will run probes before signing, since a registry of unverified signatures is worse than no registry at all.

If you adopt the gate, the habit that keeps it alive is smaller than the tooling: put the registry change in the same pull request as the doc change, so the signature and the sentence ship together. I would be interested to hear which claim classes your team refuses to let a model touch even with a human signature attached.