## How it holds

The guarantee lives in git hooks, which git runs from its own state after the shell is done. So it travels with the repository. A merge that brings feature code to the trunk without closing a spec or recording a chore is refused in your clone. The forge check asks the same questions on the pull request. Since 2.12.0 the audit's codes name every verdict it prints, and a git call the audit depends on that fails is refused by name, so an audit that read nothing never passes as clean. Since 2.13.0 the commit layer reads the same way: a commit-time hook whose git call fails refuses by name and never reads the failure as an empty answer. You adopt it in levels, in this order:

| Level | Who needs it | What it refuses |
|-------|--------------|-----------------|
| **The git hooks** (`pre-commit`, `pre-merge-commit`, `pre-push`) | Everyone, in every clone | A commit, a merge or a push whose history skipped a spec |
| **The forge check** | A team | The pull request's merge, where the trunk requires the check beside a review |
| **The session layer** | Optional | Nothing: it warns the agent, and its one deny nudges |
| **Custody** | Whoever approves specs | A build whose spec approval is missing or drifted (a key, or the forge's review) |

Since 2.14.0 the long commands, `/setlist:checkpoint`, `/setlist:upgrade` and `/setlist:validate`, keep each `SKILL.md` under 20,000 bytes and read their rarely needed sections from `references/` files beside it, so the steps a flip and a close depend on survive a long session's compaction. The upgrade to 2.14.0 names every edit an instance receives and migrates nothing in your own files: the new header lines and settings keys are read only where present. The upgrade to 2.15.0 delivers the changed hooks and the trunk audit and migrates nothing either. The plugin version it records is what puts the two push refusals in force, so a branch cut before the upgrade is not asked until it merges the trunk. Since 2.12.0 every instance carries a `verify` skill. Claude Code asks for a skill of that name before a commit, and this one runs your gate command and reports what it printed. It persuades the agent and refuses nothing, and the git hooks refuse whether or not it ran. Since 2.13.0 the bypass deny blocks a command when it times out or cannot start (Claude Code 2.1.295 and later), and the forge check reads no credential: the stamped workflow asks the forge and hands it the answers.

```mermaid
flowchart TD
  scope[".claude/hooks/scope-hook.sh"]
  subgraph clone["run by git from core.hooksPath, in your clone"]
    precommit[".githooks/pre-commit"]
    premerge[".githooks/pre-merge-commit"]
    prepush[".githooks/pre-push"]
    lib[".githooks/setlist-hook-lib.sh"]
  end
  subgraph forge["run by CI on the pull request, in no clone the committer controls"]
    workflow[".github/workflows/setlist-forge-check.yml"]
    check[".claude/hooks/forge-check.sh"]
  end
  audit[".claude/hooks/trunk-audit.sh"]
  sdd[".claude/sdd.json"]
  record[".claude/status.json"]
  scope -->|"reads the trunk and the role paths"| sdd
  lib -->|"reads the trunk and the role paths"| sdd
  check -->|"reads it at the head"| sdd
  precommit -->|"sources the predicates"| lib
  premerge -->|"sources the predicates"| lib
  prepush -->|"sources the predicates"| lib
  check -->|"sources the same predicates"| lib
  lib -->|"reads which specs this change closes"| record
  prepush -->|"gates the push on its exit status"| audit
  workflow -->|"runs forge-check.sh --base --head"| check
  check -->|"runs it over the merge in a scratch clone"| audit
  audit -->|"walks the trunk against it"| record
```

What the arrows assert: every refusal between unreviewed work and a shared trunk comes from `.githooks/pre-commit`, `.githooks/pre-merge-commit`, `.githooks/pre-push` through `.claude/hooks/trunk-audit.sh`, or `.claude/hooks/forge-check.sh`, and all four source one set of predicates in `.githooks/setlist-hook-lib.sh`. The scope hook's arrow is a read that ends in advice and never in a refusal. The trunk audit runs at every push and on every pull request. It reads history rather than commands, so it sees work that arrived through a renamed branch, a cherry-pick or the forge's merge button. It walks from the trunk audit's baseline, which `/setlist:upgrade` records in `.claude/sdd.json` where an instance's history predates its hooks. Refused merges complete the way the message says, at push as at commit. The armed check reports at session start when `core.hooksPath` or `merge.ff` is off, and the stamp refuses a bad answer by name. On a checked-out branch without `.claude/sdd.json`, `pre-commit` and `pre-merge-commit` exit in silence, and `pre-push` reads the pushed commits' own. A branch without `.githooks/` runs no hook. Another hook manager (husky, lefthook, pre-commit) is chained, its hook running beside Setlist's verdict. An octopus onto the trunk is refused by name. The hooks and the audit read history with replace refs ignored, so they judge the commits a push sends, and a failed read of HEAD is refused by name at commit and merge time (2.14.0). On a first push to an empty remote, `pre-push` audits the trunk the instance declares and names every other pushed branch it did not audit (2.14.0). A committer who crafts merges to evade the audit can, and those routes are the Known limitations. Does it work? Every release passes a dogfood gate: a fresh agent given only this repository takes a small application from an empty directory to a merged, twice-QA-passed feature. The suite behind the CI badge drives every hook through fixture repositories on Linux and macOS at every push, and its platform smoke on Windows.

