Skip to content

feat: install-hook — the v0.0.1 promise, three milestones late (#8) - #77

Merged
hermanngeorge15 merged 1 commit into
mainfrom
feat/install-hook-8
Aug 25, 2026
Merged

feat: install-hook — the v0.0.1 promise, three milestones late (#8)#77
hermanngeorge15 merged 1 commit into
mainfrom
feat/install-hook-8

Conversation

@hermanngeorge15

Copy link
Copy Markdown
Contributor

Closes #8 (HOOK-01). This is the POC: install the hook, try to commit a poisoned CLAUDE.md, get blocked.

$ injection-scanner install-hook
Installed pre-commit hook at .git/hooks/pre-commit.
Staged files are scanned before each commit; High and above block it.

$ 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 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/hooks

It reads core.hooksPath and --git-common-dir. Both assumptions break on a worktree, where .git is a file pointing elsewhere, and on any repo with a shared hooks directory.

Depends on #21

--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 — 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.

repos:
  - repo: https://github.com/UnityInFlow/injection-scanner
    rev: v0.0.3
    hooks:
      - id: injection-scanner

Tests

7 new, driving real git commit against 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.

  $ 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>
@hermanngeorge15
hermanngeorge15 merged commit 1b49334 into main Aug 25, 2026
1 check passed
@hermanngeorge15
hermanngeorge15 deleted the feat/install-hook-8 branch August 25, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pre-commit hook install command

1 participant