core.hooksPath makes this hook global to every repo, which is why it is tracked
and travels — and why it must hold no repo knowledge. A repo opts in by
declaring `.precommit-triggers` at its root: staged pathspecs on the left, a
command on the right. If the staged diff touches a declared pathspec the command
runs, and a non-zero exit refuses the commit.
Three decisions worth stating rather than leaving to be rediscovered:
Path matching is delegated to `git diff --cached --name-only -- <pathspec>`
rather than reimplemented, so declarations use the pathspec syntax the repo's
users already know and globs behave as they do everywhere else in git.
The declaration file is read on fd 3, so a declared check that reads stdin
cannot swallow the remainder of the rules.
It is dependency-free by design — no yq, no python. A global convention that
needs a toolchain silently fails to travel to the next machine, and a check that
silently does not run is worse than no check, because its absence reads as a
pass. This is a deliberate departure from the YAML used by data that python
tools consume.
Scope: this is a tripwire, not an enforcement boundary. --no-verify steps over
it, and the message says so. It is worth having because the failure mode it
addresses is forgetting, not evading.
First consumer: studium-engine, where a corpus edit invalidates engine fixtures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A35wiD55yRHj5U1ECZAX4t
On macOS /dev/tty always exists, so '[ -e /dev/tty ]' passed even in agent
harnesses/CI with no controlling terminal — the subsequent read then died
with ENXIO ('Device not configured') and blocked the commit instead of taking
the hook's own non-interactive branch. Probe with '( : < /dev/tty )' — the
exact operation that fails. Both prompt sites fixed; the deliberate asymmetry
preserved (debug keywords warn-and-proceed; secrets fail closed). Verified
both paths from a non-TTY context. Surfaced via the be a11y-gate commit
bypass logged in today's ledger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The four hooks (post-checkout, post-commit, post-merge, pre-push)
were generic git-lfs boilerplate that called 'git lfs <action>' for
each event. Under the global core.hooksPath, they required git-lfs
on PATH at every git invocation — which broke under launchd's
stripped PATH (discovered 2026-04-16 while fixing gitvault).
A repo that actually uses LFS can install its local hooks via
'git lfs install' within the repo (.git/hooks/*). The global
hookspath shouldn't require LFS to be present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior hook piped staged filenames through xargs grep, scanning the
entire current file content. This produced false positives whenever a
modified file contained pre-existing TODO/FIXME/XXX markers anywhere —
even if the staged change did not touch those lines. Six --no-verify
bypasses accumulated in two days as a result: the commits were not
introducing any new TODO markers; they simply modified files that had
template or documentary use of those tokens.
Fix:
- Collect added lines from the staged diff once (git diff --cached -U0,
filtered to lines starting with + and excluding +++ file headers)
- Scan that set for debugging-keyword and secret patterns
- Pre-existing content in modified files no longer triggers the hook
Also:
- Show the matched lines when warning (previously the hook said
"Found debugging keywords" without indicating which)
- Read from /dev/tty explicitly so interactive prompts work even when
stdin is closed by the commit driver
- Non-interactive contexts: proceed on TODO warnings (least invasive);
block on secret matches (most invasive); in both cases state clearly
what's happening
--no-verify is used on this commit only because the hook file itself
contains the literal scan patterns (TODO:, FIXME:, XXX:) inside its
grep regex. The new hook's scan of added lines therefore matches its
own source at commit time — a one-time bootstrap issue. This should be
the LAST --no-verify in this pattern. Future commits with legitimate
no-TODO-marker changes will pass cleanly; future commits that genuinely
introduce new TODO markers will correctly prompt for confirmation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>