Known limitations
The guarantee lives in the git hooks, and the session hooks carry no part of it; the push-time trunk audit stands between unreviewed work and a shared trunk.
Read the list as a threat model. Each bullet is a fact about git, the forge or the harness that a hook cannot refuse, and each release's adversarial review tests those routes on the shipped bytes. Design boundaries are decisions with a date and a reason, open limitations are defects with a status, and upstream conditions are outside this project's control.
Each group opens to a title and a short summary per bullet; LIMITATIONS.md has the full text, what to do, and the history.
Design boundaries (21)
Show the design boundaries
- Git hooks are per-clone, and the tracked directory narrows that without closing it.
A fresh clone has the hooks but not the config that points at them. It is unprotected until
refresh-instance.sh --applyruns, and the forge check is the layer that does not live in a clone. --no-verifyandSETLIST_SKIP_HOOKS=1skip the git hooks, the push-time audit included.A deliberate flag with an obvious name, or Setlist's own escape variable, skips every git hook, the push-time audit and content scan included. Every git hook reads the variable first.
SETLIST_SKIP_TRUNK_AUDIT=1is the narrow escape. No refusal names either, and the forge check has no such flag.- git allows one
core.hooksPath, so Setlist runs another hook manager's hooks through its own rather than beside them.The stamp and the refresh chain husky, lefthook or pre-commit and leave it in place. Each Setlist git hook runs that manager's hook of the same name beside its own verdict, before its checks where the index becomes the commit, and both refusals surface. A manager that re-points
core.hooksPathon install switches Setlist off again, and the next session start reports it. git merge --ff-only,--ffand--squash: a fast-forward fires no merge hook, and squash needs one flag under the stamped setting.A fast-forward creates no merge commit, so no merge hook runs. The stamped
merge.ff=falsemakes a plain--squashfail with a message that names neither Setlist nor the setting. The push-time audit still reads a one-commit close correctly, a squash included, and refuses a multi-commit fast-forward on its intermediate commits.- The trunk audit's frame is declared in
.claude/sdd.json, the trunk by name and the baseline by commit, and a declaration is a claim under the same custody as the rest of.claude/.Merge onto a branch with another name than the recorded trunk, and the whole layer stays silent. The audit walks history from
audit.baseline, which the upgrade records where an instance's history predates its hooks. A key moved forward walks less, so review it as you review the hooks. - The push-time hooks read only the branch and tag namespaces, so a push to a review-ref namespace is ungoverned by them.
A push to Gerrit's
refs/for/<branch>or any other namespace skips both the content scan and the trunk audit. Since 2.7.0 the hook reports the skip by name. The ways through are to push the trunk directly or to require the forge check. - The staged-content scans read every staged line unless you scope them.
The em-dash and secret scans read every added line of the index, vendored code and fixtures included, unless
scan_exclusionsin.claude/sdd.jsonnames the paths. Every skip is printed with the glob that matched it. - Secret and style scanning is best-effort early warning, not a guarantee.
The scan reads a rendering the repository controls, with a first-cut pattern set, so it catches accidents on the ordinary path and nothing more. Since 2.11.0 a
.gitattributes-difforbinaryentry no longer hides a text file from it, except in a merge commit's own change at push. - A checkout decides which hooks exist: a branch that does not carry
.githooks/runs no git hook, and commit and merge time read the checked-out.claude/sdd.json.A branch that never carried Setlist has no
.githooks/, so checking it out leaves git no hook to run, and a push refused from the trunk succeeds from it.pre-pushreads a pushed commit's own.claude/sdd.jsonwhen the checkout has none. The forge check refuses a pull request whose head carries none. - The set of tested platforms is a list, not a proof.
The suite runs on Linux under two awks, on macOS under bash 3.2 with the BWK awk, and since 2.11.0 on Windows 11 under Git Bash. On Windows a hook call costs about three times a Mac's, and NTFS cannot hold some of the file names the suite exercises. The list is extended, and it is never restated as a proof.
- The close review is a second model on your own account: it shares the builder's blind spots on taste, not on bytes, so its block is a verdict per criterion and never a grade.
It reads bytes against criteria in a fresh context, and its judgment is no different from the builder's. QA Pass 2 stays yours. So does the decision at its two-round cap, recorded as
ACCEPTED-BY-HUMANbeside the reviewer's verdict. - A diagram check compares a claim to a diff and a lockfile to a command's output; it does not judge whether a drawing is true of the code.
The field checks establish that the files the closer named are the files the commit touched. The node check establishes that every drawn path exists, and the lockfile that the generated view equals the command's output.
Only a path-shaped name (a slash, no whitespace) is checked, and the rest is printed as unverified. Three Mermaid shapes are unread by decision. The render check parses only where a renderer runs, and without one it fails the stamped workflow's job instead of passing.
- The session layer persuades; its one deny is a nudge a crafted command passes.
The bypass deny stops an obvious spelling of disarming the git hooks for its own run, and ordinary variable expansion, a short list of spellings its frozen parser misreads, a persistent
core.hooksPathchange or a fresh clone (reported at the next session start as[SR-HOOKS-NOT-ARMED]), and a write or a git command through Bash or another interpreter all pass it.From Claude Code 2.1.295 it blocks a command when it times out or cannot start; a skipped scope or Stop hook is advice missed, and a hook whose matcher is not a valid pattern never runs. The git hooks and the trunk audit are the catch.
- The forge check reads what the forge exposes: it governs the merge button only where the trunk requires it, reads the trunk's protection as it stands now, and runs the pull request's own copy of itself.
The stamped check is a report until the trunk lists
setlist forge checkas required beside a review and review from Code Owners, and it cannot require itself.A rebase-merge trunk defeats it before the fact. A trunk protected after an unreviewed flip landed verifies that flip. A pull request that edits the workflow or the hooks is judged by its own edit.
So the check refuses a
forge-custody trunk without "Require review from Code Owners", and it names every edit to the enforcement paths. - The CODEOWNERS bridge reads a subset of the file's grammar, and a pattern is a claim about paths.
Owns:declarations are judged against the core CODEOWNERS grammar. Sections, negations and character classes are refused by name, and GitHub evaluates no character class either. Only email owners resolve locally. - A declaration is a claim, not a verified fact: the single-parent close arm is as strong as your
Owns:lines, and the integrity chain as strong as where your signing key lives.A close is audited file by file against its own
Owns:lines, which travel in the commit they justify, so the hooks verify coverage and never truth. A spec that declares nothing closes under the older whole-commit exemption. A signature proves a key was used, and never that a person decided.forgecustody has no key and rests on the forge's required review instead. - The hashed range ends at the FIRST line reading
## Closing report, fences included, and the ownership reader agrees with that cut.A quoted copy of the template above your
Owns:lines ends the signed range early, and the declaration below it is refused as out of range. The refusal names the one-edit fix. - A session killed from outside fires no Stop, and the Stop hook is a nudge, never a lock.
The Stop hook refuses once to end a turn with an unstaged spec or
specs/STATUS.mdchange, in a project with its own repository. A session killed from outside fires nothing, and the git hooks are what refuse the work. - One repository is one instance: no gate reads across repositories, and a change that spans them is governed by each repository's own hooks and by a citation no hook verifies.
Each repository's hooks, audit and forge check read that repository alone, so a change across two is a spec in each, and whether the pair agrees is a reviewer's question.
- Deliberate evasion: a committer who crafts history to pass the trunk audit can, and the list of known routes is maintained rather than complete.
A chained merge, and its octopus spelling into a spec branch, hide unspecced work behind a compliant close; an octopus onto the trunk is refused since 2.11.0.
Five sideways commands (
git cherry-pick, a detached-HEAD merge,git rebase,git reset --hard, the pathspec checkout) are refused at push, unless composed with a compliant close that declares no ownership. A merge that edits a file outside its conflicts is reported on git 2.38 and later and never refused, while an edit inside them reads as resolution.A local replace ref showed the audit a commit other than the one a push sends until 2.14.0, which closed that route.
- The scope hook's warning reaches the agent, and the agent may proceed anyway: advisories persuade, hooks refuse.
Since 2.9.0 the scope hook's reason rides the field Claude Code delivers to the agent, and an agent may still judge the write harmless and proceed. The git hooks' refusals at commit, merge and push are what hold.
Open limitations (0)
None stands at 2.15.0: the two that stood at 2.9.0 were fixed in 2.11.0. Full text.
Upstream conditions (0)
None stands at 2.15.0: the one that stood, a timed-out hook read as a skipped gate, left when the bypass deny began to block on failure; what still skips is in the session layer's bullet above. Full text.