feat: install-hook — the v0.0.1 promise, three milestones late (#8) - #77
Merged
Conversation
$ injection-scanner install-hook
$ git commit -m "update spec"
./docs/CLAUDE.md
:3 CRITICAL Attempts to override agent instructions (PI001)
Commit blocked: prompt-injection patterns at high or above.
Explain a finding with: injection-scanner explain <PI0XX>
Commit anyway with: git commit --no-verify
60ms on a 40-file repository, against the 200ms budget the original criteria
set.
Three things a naive hook gets wrong:
- It would scan the WORKING TREE. The hook scans the staged blob via
`git show :path`, so a partially staged file is judged on what is actually
about to be committed. Otherwise you stage a clean version, leave the
payload unstaged, and pass.
- It would report the staging copy's path. Findings have to name
./docs/CLAUDE.md, not /tmp/tmp.XXXX/docs/CLAUDE.md — a path that does not
exist by the time the developer reads it. Solved by running the scan from
inside the staging copy rather than by string-editing the output.
- It would clobber an existing hook. A pre-commit hook is often the only thing
between a repository and a committed secret, so a foreign one is refused
with a pointer to --force, and our own is recognised by a marker line and
updated rather than erroring.
Hook location comes from git, not from assuming `.git/hooks`: it reads
`core.hooksPath` and `--git-common-dir`, both of which are wrong to assume on a
worktree, where `.git` is a file rather than a directory.
`--fail-on` defaults to `high` here rather than the CLI's `low`, so MEDIUM
heuristics inform without blocking a commit. That default only became meaningful
with the severity rebalance in #21 — before it, every finding was CRITICAL or
HIGH and the hook would have blocked on everything.
Also ships `.pre-commit-hooks.yaml`. Both entry points are needed, not one:
`pre-commit install` overwrites the plain git hook this command writes, so a
team using the framework would silently lose it.
Seven tests, driving real `git commit` against a real repository — including the
partially-staged case and the foreign-hook refusal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #8 (HOOK-01). This is the POC: install the hook, try to commit a poisoned
CLAUDE.md, get blocked.60ms on a 40-file repository, against the 200ms budget the original v0.0.1 criteria set.
Three things a naive hook gets wrong
It would scan the working tree. The hook scans the staged blob via
git show :path, so a partially staged file is judged on what's actually about to be committed. Otherwise you stage a clean version, leave the payload unstaged, and pass. Has its own test.It would report the staging copy's path. Findings must name
./docs/CLAUDE.md, not/tmp/tmp.XXXX/docs/CLAUDE.md— a path that no longer exists by the time the developer reads it. Solved by running the scan from inside the staging copy rather than string-editing the output.It would clobber an existing hook. A pre-commit hook is often the only thing between a repository and a committed secret. A foreign hook is refused with a pointer to
--force; our own is recognised by a marker line and updated rather than erroring.Hook location comes from git, not from assuming
.git/hooksIt reads
core.hooksPathand--git-common-dir. Both assumptions break on a worktree, where.gitis a file pointing elsewhere, and on any repo with a shared hooks directory.Depends on #21
--fail-ondefaults tohighhere rather than the CLI'slow, so MEDIUM heuristics inform without blocking a commit. That default only became meaningful with the severity rebalance — before it, every finding was CRITICAL or HIGH and the hook would have blocked on everything.Also ships
.pre-commit-hooks.yamlBoth entry points are needed, not one:
pre-commit installoverwrites the plain git hook this command writes, so a team using the framework would silently lose it.Tests
7 new, driving real
git commitagainst real repositories — the block, the clean pass, the repository-path assertion, the foreign-hook refusal, reinstall-is-not-an-error, the partially-staged case, and manifest validity. Full suite 24 binaries green, clippy clean.