A git hook runs the SpecSheriff tools a plugin keeps on the machine
When a session starts, the plugin for Claude Code (§s-zh2vcr3xjumppg5) copies the SpecSheriff tools it carries into its data directory, <plugins>/data/specsheriff-specsheriff-marketplace/, where the copy there is of another release or missing; <plugins> is CLAUDE_CODE_PLUGIN_CACHE_DIR where it is set and ~/.claude/plugins otherwise. The plugin for Codex CLI (§s-2pdy9gw2jrjvfb4) keeps no copy: its tools stand in its directory in Codex's cache, <Codex home>/plugins/cache/specsheriff-marketplace/specsheriff/<version>/tools/, where <Codex home> is CODEX_HOME where it is set and ~/.codex otherwise. ~ is the user's home directory, on Windows their profile directory.
Each git hook sheriff init writes holds the lookup of those tools, a call to the script for that hook in their hooks/, and what it does where it finds none. Where it finds the tools of both plugins, it takes those whose release file opens with the higher version, compared as semantic versions, and either where the two are equal. The script runs what the hook is to run (§s-477gtdrkw34dzxw), and refuses where Node is missing or older than the tools need, naming the version they need.
Where the hook finds the tools of neither plugin, it lets the commit or the push through and turns the git hooks off in that clone, unsetting core.hooksPath where it names .githooks. It says that no check ran because it found the SpecSheriff tools of no plugin on the machine, and that git config core.hooksPath .githooks turns the hooks on again.
The script run when a session starts names each git hook in .githooks/, and each of the agent's hooks for Codex in .codex/hooks.json, that holds what an earlier release of sheriff init wrote, and says that sheriff init replaces it (§s-7unxytshhc4md54).
Claude Code keeps each version of a plugin in a directory of its own, which changes with every update, and keeps the plugin's data directory across updates; so the plugin keeps a copy of its release there, at a path a hook written once can name. Codex keeps the one installed version of a plugin in its cache and deletes the cache when the plugin is removed, while it leaves the plugin's data directory; a copy kept there would outlive the plugin, so the hook looks in the cache, which goes with it.
The hook looks for the tools of both plugins because a user may move from one agent to the other, and takes the higher release because it holds every fix of the lower.
A hook that finds no tools is most often one whose user removed the plugin. Refusing would hold every commit of that user while checking nothing, so the hook lets it through, turns itself off so as not to say so at every commit, and tells the user how the commit was made and how to turn the hooks on again. Where the tools are found but cannot run, the plugin is there and its user expects the checks to hold, so the script refuses.
A hook is a file of the project, which only a commit changes, so a hook written by an earlier release keeps its lookup until sheriff init replaces it; the session start says so, because it runs in every clone.
The links it makes
The links made to it
The decisions taken on it
- Problem
The git hook refused where its script was missing, so once the plugin for Claude Code was uninstalled from its last scope, which deletes its data directory, a clone whose hooks a session had turned on would refuse every commit.
- Ruling
The owner: removing a plugin leaves no hook failing; a hook that finds the tools of no plugin lets the commit through with a notice and turns the hooks off in that clone.
The tests bound to it
- skills/tools/test/e2e/codex.test.ts · s-4mej6ccn7xu3d3p the session start of the agent's hooks for Codex names each git hook an earlier release of sheriff init wrote
- skills/tools/test/e2e/plugin.test.ts · s-4mej6ccn7xu3d3p a git hook runs the tools of the higher release where it finds both plugins', those of the plugin for Claude Code where the two are equal, and either where it finds one
- skills/tools/test/e2e/plugin.test.ts · s-4mej6ccn7xu3d3p a git hook that finds the tools of neither plugin lets the commit through, turns the git hooks off in that clone, and says how to turn them on again
- skills/tools/test/e2e/plugin.test.ts · s-4mej6ccn7xu3d3p a hook's script refuses where Node is missing or older than the tools need, naming the version they need
- skills/tools/test/e2e/plugin.test.ts · s-4mej6ccn7xu3d3p each git hook sheriff init writes holds the lookup of the tools of both plugins and a call to the script for that hook, and nothing else
- skills/tools/test/e2e/plugin.test.ts · s-4mej6ccn7xu3d3p the session start names each git hook an earlier release of sheriff init wrote, saying that sheriff init replaces it
- skills/tools/test/e2e/plugin.test.ts · s-4mej6ccn7xu3d3p when a session starts, the plugin copies its tools into its data directory where the copy there is of another release or missing