The hooks
Each check lives in a command a git hook calls: .githooks/pre-commit runs sheriff export and sheriff render, then sheriff validate, sheriff binding and sheriff citations, and last sheriff types; .githooks/commit-msg runs sheriff message and then sheriff review on the file git hands it; and .githooks/pre-push runs the tests of the system's code by the command sheriff.config.json names as its testCommand.
A clone turns them on with git config core.hooksPath .githooks, which the SessionStart hook of the agent, the plugin's for Claude Code or the project's for Codex (§s-n76wt2bkqdmh78x), runs where core.hooksPath is unset, saying so, and reports where it names another directory. The SessionStart hook also names each git hook in .githooks/ that git's index holds without the executable mode and, on a system whose files keep that bit, each whose file lacks it, and says how to make each one executable on every system — chmod +x <hook> and git update-index --chmod=+x <hook>, then a commit — changing neither the mode of a file nor git's index.
The SessionStart hook of the plugin for Claude Code writes into the git configuration of the user, by git config --global, each setting of the runner claude (§s-xr5xxa9cxf2mq5u) the user has given as an option of that plugin (§s-zh2vcr3xjumppg5), wherever the configuration lacks it or holds another value. Where the git configuration lacks a setting of the runner sheriff review would take at a commit in the repository (§s-xr5xxa9cxf2mq5u, §s-4mej6ccn7xu3d3p), the SessionStart hook of either agent names each such setting, with the default sheriff review takes for it, and asks the agent to offer the user each default to confirm or replace and to write with git config --global the value the user answers, the default included. It asks so once for each runner on each machine, and marks it asked in the git configuration, as specsheriff.review.claudeAsked or specsheriff.review.codexAsked; where the mark stands it asks nothing.
At a commit, the checks read what is staged, written out to a directory of its own and read there against the repository's history, while the export and the map are written in the working tree, a failure to write them reported and stopping nothing. Whatever the checks found, sheriff types then runs in the directory the staged files are written out to; the pre-commit hook copies each file it writes there into the working tree, in place of what stands at the same path, and stages none of them, and what sheriff types refuses stops the commit.
Before a push, the pre-push hook compares the working tree with the commit each ref the push creates or updates names, directly or through a tag, leaving aside the files the working tree ignores. Where a file differs between them, stands in only one of them, or stands in the working tree without having been added, it refuses the push, naming each such file; otherwise it runs the tests in the working tree.
The agent's hooks are Claude Code's, which the plugin for Claude Code declares (§s-zh2vcr3xjumppg5), and Codex's, which sheriff init writes into the project (§s-n76wt2bkqdmh78x): before an edit made with the file tools or with apply_patch, sheriff guard judges it; after any edit, sheriff reindex, sheriff export, sheriff render and sheriff types run, and then sheriff validate, sheriff binding and sheriff citations, all over the working tree, its difference from the last commit read as the commit, and what they refuse is reported to the agent.
An edit made through the shell is not judged before it is made, nor is a file sheriff types writes after an edit; the checks after them and at the commit still read them.
A hook runs each check where the commit is made, so that none depends on whoever commits remembering it; kept in the repository and turned on by one setting, the hooks are the same in every clone.
The agent's hooks come with the plugin for Claude Code and with the project for Codex, so that every clone's agent is held by them; and they turn the git hooks on, so that no clone's gate depends on its user remembering to.
On a system whose files keep the executable bit, git skips a hook whose file lacks it, printing a hint and refusing nothing, so a commit there passes no check while the hooks are on. The session start reaches every clone, those where the git hooks are skipped and so cannot report among them, and every repository whose hooks were committed or staged again without the mode. It leaves the mode to the author, since a mode changed in the working tree alone is a difference the pre-push hook refuses, and recording it in the index is staging, which is the author's choice.
The settings the reviewer runs at are a choice of what the user pays, so only the user makes it (§s-xr5xxa9cxf2mq5u), and they make it once, when they install the plugin, as they decide to take the plugin itself, so that no later session is interrupted by it. Claude Code asks for a plugin's options when it enables the plugin and hands them to the plugin's own components, while the git hooks that run the reviewer run outside Claude Code; so the plugin's session start writes them where the git hooks read them, in any repository, so that a project set up later finds them there, and wherever they differ, so that a change the user makes to them reaches the reviewer. Codex asks nothing for a plugin it installs, and its agent's hooks stand in the project (§s-n76wt2bkqdmh78x), so the first session in a repository taking the method is the earliest its user can be asked; a user Claude Code enabled the plugin for without asking is in the same case. A hook cannot ask the user anything, so it has the agent ask. The question is put once, its mark keeping it from being put again, since a setting left unanswered is named at every review it costs (§s-xr5xxa9cxf2mq5u). Only the runner that would run is asked for, since a user is asked only about what they will pay for.
The types and runtime checks generated from the data record nodes are generated after every edit an agent makes and at every commit, so that they follow the nodes at the first edit or commit after every node they are generated from is checked (§s-kh8qcd5kxgh9z3j), and a person who edits the nodes without an agent has no hook between edits. At a commit they are generated from the staged nodes, because a commit is judged only by what it holds (§s-jau82aq9juv9zzh): what sheriff types refuses there is a fault of that commit, and the copy it writes is the one that commit has to hold.
At a commit it runs after the checks, so that they read what is staged rather than a copy the hook wrote, and whatever they found, since the check of the generated copies is among them and refuses exactly the commit whose copies it writes; after an edit it runs before them, so that they read the copies the edited nodes produce rather than report ones about to be replaced.
The copy goes to the working tree, since the directory the commit is written out to is gone once the hook ends, and the author stages from the working tree; it replaces whatever stands at that path, since a generated copy is never edited (§s-kh8qcd5kxgh9z3j).
What a commit holds is its author's choice, so the hook stages nothing; a commit holding a copy left behind nodes that are all checked still does not pass, since the check at the commit comparing each kept generated copy with what the staged nodes produce refuses it until its author stages the new one.
A failure to write the export or the map stops no commit, since the commit holds neither, and every check reads the commit being made and never what it does not hold (§s-jau82aq9juv9zzh).
A test run judges a commit only where the files it reads are those the commit holds, since every check reads the commit being made and never what it does not hold (§s-jau82aq9juv9zzh). The tests run in the working tree rather than in a copy of the commit, because they need what each checkout installs or builds for itself, which a copy of the commit lacks.
The files the working tree ignores are left out of the comparison of the working tree with each commit, because each checkout makes or keeps its own copy of them, so where a commit holds such a file, as a commit of a build's output does, the working tree's copy need not match it.
The refusal names the files the push is refused for, because whether each is committed, removed or ignored is for whoever pushes to decide.
The links it makes
- implements → What is checked, when, and what a failure costs
- implements → Where an agent writes, the checks run at each of its edits
- cites → Outgoing links are written, incoming links are built
- cites → A link carries its type
- cites → A link may cross a domain line
- cites → The plugin for Claude Code carries everything its work needs
- implements → The nodes are the specification
- cites → The agent's hooks for Codex CLI
- cites → Who runs the reviewer
- cites → A git hook runs the SpecSheriff tools a plugin keeps on the machine
The links made to it
The decisions taken on it
- Problem
The citation check was run by no hook, so citations it refused reached the main branch through commits every hook had passed, and nothing checked that a citation in a node had a link.
- Ruling
Following the owner's rulings on citations (§s-bpdqd9v72wg7vsz, §s-sn6au3vcr9uy5pm, §s-hr7j7scmfkkjnws): the pre-commit hook runs the citation check, and the check of the specification refuses a citation with no link.
- Problem
An automated security review found that the plugin's session start turns on whatever hooks
.githooks/holds in a repository whose root holdssheriff.config.json, so a repository someone opens could have git run code it brought, which git never does for a clone until its user turns its hooks on.- Ruling
The owner: the rule stands, and nothing is built against it. The consent that counts is the trust a user gives a folder in Claude Code, before which a plugin the project declares does not load; a repository taking the method runs its own code at every push anyway, since the pre-push hook runs its tests; and the case left open needs together the plugin installed by the user rather than declared by the project, Claude Code run without its dialog of trust, and a clone nobody looked at.
- Problem
The todo on this rule asks for post-merge and post-checkout hooks writing the export and the map, which follow a change that reaches the working tree through git only at the next edit an agent makes or the next commit.
- Ruling
The owner: it waits past the first release, 0.1.0, and the todo stays. The export and the map are views kept in the working tree, which git does not keep and no check reads, and they lag only until the next edit or commit.
- Problem
Nothing ran sheriff types, so after a change to a data record node the generated types and runtime checks stayed behind the nodes until somebody remembered to run it, while the base requires what is generated from the nodes to be regenerated after every change to them.
- Ruling
The owner: the agent's hooks run sheriff types after every edit, before the checks; the pre-commit hook runs it last, after the checks and whatever they found, on what is staged, its refusal stopping the commit and each file it writes copied into the working tree for the author to stage, nothing staged by the hook.
- Problem
The pre-push hook ran the tests over the working tree, its uncommitted changes and files never added included, while every check reads the commit being made and never what it does not hold.
- Ruling
The owner: the push is refused while the working tree differs from what it sends, files it ignores left aside, rather than the hook setting such changes aside itself; then the tests run in the working tree.
- Problem
The pre-commit hook stopped a commit where the export or the map could not be written from the working tree, as from a specification file broken there and not staged, though the commit holds neither.
- Ruling
Following §s-jau82aq9juv9zzh: a failure to write the export or the map at a commit is reported and stops nothing.
- Problem
The reason had the types regenerated after every change to the nodes, while they now wait until every node they are generated from is checked.
- Ruling
Following the owner's ruling on §s-kh8qcd5kxgh9z3j: code generated from the nodes waits until every node it is generated from is checked, no commit changing it meanwhile, and its lag is reported.
- Problem
A repository whose hooks git's index held without the executable mode, as one set up on Windows did, ran none of its checks in a clone on Linux or macOS, git skipping each hook with a hint, and nothing told its user so.
- Ruling
The owner: the session start names each git hook git's index holds without the executable mode and, where files keep that bit, each whose file lacks it, says how to make it executable on every system, and changes neither a file's mode nor the index.
- Problem
The reviewer runs at the settings each user confirms (§s-xr5xxa9cxf2mq5u), and nothing asked a user for them or wrote them where the git hooks read them.
- Ruling
The owner: the settings of the reviewer are a decision taken once, when the plugin is installed, not a question at each session; Claude Code asks for them as the plugin's options, which the session start of the plugin for Claude Code writes into the git configuration in any repository and wherever they differ, so that a change made in /config reaches the reviewer; Codex, which asks nothing at installation, has the agent ask at the first session in a repository taking the method, or spec-init where it runs first; the question is put once on each machine, and after it only the notice at each review reminds the user.
The work deferred on it
The export and the map follow a change that reaches the working tree through git — a pull, a merge, a checkout — only at the next edit an agent makes or the next commit; post-merge and post-checkout hooks writing them would close that.
The tests bound to it
- skills/tools/test/e2e/codex.test.ts · s-477gtdrkw34dzxw the session start of the agent's hooks for Codex names each setting of the runner codex the user's configuration lacks, with its default, and has the agent ask for them once on each machine
- skills/tools/test/e2e/codex.test.ts · s-477gtdrkw34dzxw the session start of the agent's hooks for Codex turns the git hooks on where core.hooksPath is unset, saying so
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw a changed data record node is committed before its confirmation, its types left as they were and their wait reported
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw a commit changing no code passes the hooks, and the export and the map are written
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw a commit goes on where the export and the map cannot be written from the working tree, which reports it
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw after an agent's edit the types are written before the checks run
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw the checks read what is staged, not what the working tree holds beside it
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw the commit-msg hook refuses a message that leaves out a node the changed code serves
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw the pre-commit hook refuses a commit whose specification does not hold together
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw the pre-commit hook stops a commit whose staged data record nodes cannot be generated
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw the pre-commit hook writes the types the staged nodes generate into the working tree, stages none of them, and the commit waits for its author to stage them
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw the pre-push hook compares every ref the push sends, tags and other branches included, leaving aside the files the working tree ignores
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw the pre-push hook refuses a push while the working tree differs from the commit it sends, naming each such file
- skills/tools/test/e2e/hooks.test.ts · s-477gtdrkw34dzxw the pre-push hook runs the tests by the command the settings name, and refuses a push without one
- skills/tools/test/e2e/plugin.test.ts · s-477gtdrkw34dzxw in a repository taking the method the session start names each setting the user's configuration lacks, with its default, and has the agent ask for them once on each machine
- skills/tools/test/e2e/plugin.test.ts · s-477gtdrkw34dzxw the plugin's session start names each git hook not executable, says how to make it so, and changes neither the file nor the index
- skills/tools/test/e2e/plugin.test.ts · s-477gtdrkw34dzxw the plugin's session start turns the git hooks on where core.hooksPath is unset, saying so, and reports another directory
- skills/tools/test/e2e/plugin.test.ts · s-477gtdrkw34dzxw the session start of the plugin for Claude Code writes each setting the user gave as an option, in any repository, wherever the configuration lacks it or holds another value
- skills/tools/test/unit/config.test.ts · s-477gtdrkw34dzxw reads the command that runs the tests of the system's code