A pre-commit hook that runs an agentic AI code review on your staged changes using pi, then lets the commit proceed or blocks it based on a go/no-go decision. The reviewer keeps context across rejected rounds, so when you amend and retry it remembers what it already raised and is proportionally more lenient on resolved issues.
⚠️ Vibe-coded, experimental, pre-release. This project was written in an AI-assisted session (pi) from a design doc, then checked by a separate design-agent review, and its own commits were reviewed by this very hook ("dogfooded"). There is no stable release yet — pin to a commit SHA, not a tag. Treat it as a fun experiment, not a hardened production gate. See Pitfalls & caveats before adopting.
On git commit, the hook:
- Computes your staged diff and the staged tree hash.
- Asks pi to review the diff and call a custom
submit_review_decisiontool with"go"or"no-go". - go → the commit proceeds (and the reviewer's approving comments are
saved under
.git/pi-reviewer/reviews/). - no-go → the commit is blocked, the issues are printed, and the rejection is recorded. Amending and retrying resumes the same pi session, so the reviewer sees its previous feedback and eases up on things you've fixed.
It is not a linter or formatter. It targets issues those tools miss: logic errors, security misconfigurations, broken patterns, missing edge cases, architectural concerns. Trivial style is left to your other hooks.
The hook shells out to the pi CLI once per commit attempt:
pi --mode json --print \
--session-id <id> --session-dir <dir> \
--model <model> --system-prompt "<reviewer prompt>" \
--extension <bundled extension.ts> \
--tools read,grep,find,ls,submit_review_decision \
"<user prompt with the staged diff>"- JSON mode (
--mode json --print): pi runs non-interactively and emits a stream of typed NDJSON events. The hook scans the stream for atool_execution_startevent whosetoolNameissubmit_review_decisionand reads the decision from itsargs. It does not parse the assistant's prose — the decision comes only from the structured tool call. - Session continuity (
--session-id,--session-dir): pi's native session store holds the conversation across rounds. The hook generates one session id per review cycle and reuses it until ago(after which it is kept for a potential amend — see below) or a fresh non-amend commit clears it. - A custom tool (
submit_review_decision): defined by a small TypeScript extension bundled inside the Python package and loaded with-e. Onlydecisionis a required argument;issues,summary, andsuggestionsare optional but encouraged. A minimal review can always call{"decision": "no-go"}, which avoids schema-compliance failures. - Read-only exploration only: the
--toolsallowlist isread,grep,find,ls,submit_review_decision. The reviewer can look at surrounding code for context but cannot runbash,edit, orwrite. Safety is enforced by the allowlist, not by a sandbox. - No
--no-extensions: provider extensions (e.g. ollama) register the models you actually want to use, so disabling all extensions would break model resolution. The--toolsallowlist still disables any tools those extensions register; only provider registration (and extension startup code) runs. See the caveat on extension code below.
- pre-commit installed and managing your repo's hooks.
- pi installed and on your
PATH. If pi isn't found, the hook fails open (exit 0, no-op) so shared configs stay safe on machines without pi. - A model pi can resolve, backed by a provider whose credentials you have (see Provider / model notes). Every commit makes a real model call, so this implies network access and, for hosted models, cost.
- Python ≥3.11 in pre-commit's venv (the hook has no Python dependencies — standard library only).
Add the hook to your repo's .pre-commit-config.yaml. Because there's no
release tag yet, pin to a commit SHA from
github.com/Fifty-Nine/pi-review-hook:
repos:
- repo: https://github.com/Fifty-Nine/pi-review-hook
rev: 0b823df # pin to a real commit SHA; replace before adopting
hooks:
- id: pi-review
args: ["--model", "ollama-cloud/glm-5.2"]
- id: pi-review-notes # optional: git notes for finished reviewsThen install the hooks. pi-review-notes runs at the post-commit stage, so
install that hook type too:
pre-commit install
pre-commit install --hook-type post-commit # required for pi-review-notesBoth hooks are always_run: true, pass_filenames: false, and
require_serial: true (they compute their own diff / commit lookup and are
stateful), so they run on every commit regardless of staged file types.
pi-review-notes takes no arguments.
NixOS note: the build backend is hatchling (pure Python) specifically
because uv_build's compiled binary can't run on NixOS inside pre-commit's pip
build env. If you're on NixOS, point --pi-binary at your sandboxed pi if you
use one (e.g. --pi-binary pi-jailed).
Precedence: CLI args (pre-commit args) > env vars (PI_REVIEW_*) > defaults.
| Setting | CLI arg | Env var | Default |
|---|---|---|---|
| Model | --model |
PI_REVIEW_MODEL |
glm-5.2 |
| pi binary | --pi-binary |
PI_REVIEW_PI_BINARY |
pi |
| System prompt | --system-prompt / --system-prompt-file |
PI_REVIEW_SYSTEM_PROMPT |
built-in reviewer prompt |
| Session dir | --session-dir |
PI_REVIEW_SESSION_DIR |
.git/pi-reviewer/sessions |
| Archive sessions | --archive-sessions |
PI_REVIEW_ARCHIVE_SESSIONS |
off (delete on next fresh commit) |
| Review guidelines | --no-review-guidelines |
PI_REVIEW_NO_REVIEW_GUIDELINES |
on (auto-discover REVIEW_GUIDELINES.md) |
Examples:
hooks:
- id: pi-review
args:
- "--model"
- "ollama-cloud/glm-5.2"
- "--archive-sessions" # keep a gzipped jsonl of each reviewed session# Or configure via env (e.g. in CI, a direnv, or a shell wrapper)
export PI_REVIEW_MODEL=ollama-cloud/glm-5.2
export PI_REVIEW_ARCHIVE_SESSIONS=1- The
--modelvalue must resolve against a provider pi can see. Built-in providers (e.g. google) work out of the box; extension-registered providers such asollama/ollama-cloudrequire that provider extension to be installed in pi. Use theprovider/modelform to be explicit, e.g.ollama-cloud/glm-5.2oropenai/gpt-4o. - Run
pi --list-modelsto see what's available. A pattern that doesn't resolve makes pi exit non-zero, which the hook treats as an infrastructure failure and fails closed (blocks the commit) — so a stale model id can block all commits until fixed or bypassed. - The tool allowlist is
read,grep,find,ls,submit_review_decision. The reviewer can explore surrounding code read-only but cannot runbash, edit, or write. Note: user-installed pi extensions still load their code at startup (providers, event handlers); only their tools are disabled by the allowlist.
-
First round: a fresh pi session reviews your diff with the built-in (strict) reviewer prompt.
-
Rejection: the rejected tree hash and issues are recorded; the session is kept.
-
Same-tree auto-reject: if you retry
git commitwithout changing anything, the staged tree hash matches a prior rejection and the hook blocks immediately without calling pi (fast, no model cost). -
Follow-up round: when you amend and retry, pi resumes the session. The prompt tells it the round number, lists the previous issues, and instructs it to be proportionally lenient on previously raised issues that appear resolved while holding the line on new problems.
-
Approval (
go): the session is kept (lazy clear) so a subsequentgit commit --amendcan resume it; the approving comments (summary/suggestions) are printed at commit time and persisted to.git/pi-reviewer/reviews/. If--archive-sessionsis on, a snapshot of the session is archived to.git/pi-reviewer/archive/session-<id>.jsonl.gz(a gzipped jsonl of the session entries) at go time, so you can inspect or resurrect it later:# inspect the archive zgrep '"type":"message"' .git/pi-reviewer/archive/session-<id>.jsonl.gz # resurrect: put the jsonl back in the session store, then resume gunzip -c .git/pi-reviewer/archive/session-<id>.jsonl.gz \ > .git/pi-reviewer/sessions/$(date -u +%Y-%m-%dT%H-%M-%S-%3NZ)_<id>.jsonl pi --session <id> --session-dir .git/pi-reviewer/sessions
When the reviewer approves (go) but gives feedback, you can address it in
the same commit with git commit --amend. On Linux the hook detects the
amend by walking the process hierarchy (/proc) for the git --amend
invocation, then:
- Resumes the same session — the reviewer remembers its previous feedback.
- Shows the full change set the amended commit will contain (diff from the parent of the approved commit to your staged tree), not just the delta, and asks the reviewer to verify the feedback was addressed and re-evaluate the whole change.
- Keeps the session after the go (lazy clear) so further amends resume it too; it is cleared on the next non-amend commit.
- Carries the review note forward: the amended commit's git note keeps the previous review and appends the re-review (see Git notes below).
Caveats:
- Non-Linux: the
/procwalk is Linux-only. On other platforms amends fall through to the current behavior (fresh session, delta-only diff); useSKIP=pi-reviewif you need to bypass. - Rebase/cherry-pick: during a rebase (detached HEAD) the amend behavior
is skipped, so
rebase -i edit+git commit --amendgets a normal delta review instead.
Place a REVIEW_GUIDELINES.md at the root of your repo to give the reviewer
project-specific criteria — for example, what constitutes a blocking
("no-go") finding. Its contents are appended to the review prompt and
override the built-in criteria (the system prompt says so explicitly), so
the agent can be more confident about rejecting commits that violate your
rules. You can put anything there; it's plain Markdown.
# Review Guidelines
A "no-go" is required when:
- Secrets or credentials are committed.
- The change breaks the documented failure-mode matrix.
- Behavior changes are not covered by tests.The file is auto-discovered on every review. Disable with
--no-review-guidelines or PI_REVIEW_NO_REVIEW_GUIDELINES=1.
Add the optional pi-review-notes hook id to get a git note on every
commit describing its review. The hook runs at the post-commit stage: it
finds the just-created commit whose tree matches a finished (go) review and
attaches the review printout under a dedicated refs/notes/pi-review ref:
Decision: go
Summary: LGTM with minor notes
Suggestions:
- add a test
Session archive: ./.git/pi-reviewer/archive/session-<id>.jsonl.gz
The Session archive: line appears only when --archive-sessions is on.
Amended commits carry the previous review forward. When you
git commit --amend, the post-commit hook detects the amend via the reflog
(HEAD@{1} is not an ancestor of HEAD) and prepends the old commit's note
with Previous review (amended from <old-sha>): before the new re-review —
so the review history survives the amend (the orphaned commit is gc'd). If
the amended commit was not re-reviewed (SKIP=pi-review, --no-verify, or
an empty-diff message fix), the old note is carried forward with an
Amended without re-review audit line instead.
Commits with no associated review (e.g. SKIP=pi-review, --no-verify,
merges) get a brief "no review" audit note, so the note history is a
complete audit trail of what was and wasn't reviewed.
View the notes:
git log --notes=pi-review # notes inline in git log
git notes --ref=refs/notes/pi-review show HEAD # one commit's note
# one-time: make plain `git log` show the notes too
git config notes.displayRef refs/notes/pi-reviewNotes live in a ref, not in commits, so they are not pushed by default.
Share them with git push origin refs/notes/pi-review. Skip the hook with
SKIP=pi-review-notes.
# Skip only pi-review, keep all your other hooks running:
SKIP=pi-review git commit -m "..."
# (Not recommended) skip every hook:
git commit --no-verify -m "..."Use SKIP=pi-review during model/provider outages, when iterating fast, or
when the reviewer is wrong and you've judged the change safe.
This project is experimental and was largely vibe-coded. Please read these before relying on it:
- It calls a model on every commit. That means latency (seconds to tens of seconds) and, for hosted models, real cost per commit. The same-tree auto-reject avoids re-reviewing no-op retries, but every changed commit pays. Consider this for busy repos.
- Review quality == model quality. The reviewer can produce false positives (blocking a legitimate commit) or misses (approving flawed code). In our own dogfooding it caught a genuine ordering bug in the archiving code — and it also produced false positives on a correctly parameterized SQL query. It is an aid, not an oracle.
- Model patterns drift. Provider catalogs change (we hit an ollama catalog
change mid-development that made our pinned model id stop resolving). A stale
--modelmakes pi fail, which the hook treats as an infra failure and fails closed — blocking commits. Keep the model id current, orSKIP. - No session reset for v1. If you abandon a change mid-review and start a
different one, the old session context may carry over. The follow-up prompt
explicitly tells pi this might be a new change; in practice, rely on
go-keeps + fresh-clears andSKIP=pi-reviewto reset. - Large diffs are not handled. Very large staged changes may exceed the model's context window. Not truncated or summarized in v1.
- State lives under
.git/pi-reviewer/(not committed):state.json,sessions/,reviews/, andarchive/. It is per-repo and per-working-copy. - Git notes are not pushed by default. The
refs/notes/pi-reviewref is local until yougit push origin refs/notes/pi-review; collaborators won't see the notes otherwise. - pre-commit hides passed-hook output. On a successful review the printed
summary/suggestions aren't shown unless you run with
--verbose. The authoritative record is the JSON in.git/pi-reviewer/reviews/. - Rev pinning is manual. Pin
rev:to a commit SHA in your.pre-commit-config.yamland bump it deliberately after upstream changes. There is no autoupdate story yet. - Extension code runs at startup. Because the hook does not pass
--no-extensions, any pi extension you have installed will load its code (providers, event handlers). Their tools are disabled by the--toolsallowlist, but the extension code itself executes — be aware if you run untrusted extensions. - NixOS specifics. The default binary is
pi(not the bubblewrap-sandboxedpi-jailed); override with--pi-binary pi-jailedif you use that variant. The build backend ishatchlingprecisely becauseuv_build's binary can't run on NixOS inside pre-commit's build env.
| Scenario | Behavior | Exit |
|---|---|---|
pi not on PATH |
Skip (fail-open) | 0 |
| pi fails / model not found / network error | Fail-closed | 1 |
| pi runs but never calls the decision tool | Fail-closed | 1 |
| Unrecognized decision value | Fail-closed | 1 |
| Empty staged diff | Nothing to review | 0 |
| Same tree as a prior rejection | Auto-reject, no pi call | 1 |
Decision go |
Keep state (lazy clear, archive snapshot if enabled), proceed | 0 |
Decision no-go |
Record rejection, print issues, block | 1 |
| Post-commit note attach fails | Non-zero exit, review log kept for retry (commit already happened) | 1 |
When pi itself fails, the hook now includes the tail of pi's stderr in its message so the cause (e.g. "Model ... not found") is diagnosable.
This repo reviews itself with the pi-review hook (see
.pre-commit-config.yaml). Lint/format use make lint / make fmt
(nixpkgs#ruff; the PyPI ruff binary can't run on NixOS). Tests:
nix shell nixpkgs#python3 nixpkgs#uv -c uv run pytest # or: pytest -vFurther reading:
- Implementation plan
- Implementation plan: amend-after-go
- Architecture Decision Record
- AGENTS.md — living context for anyone working on the project
MIT.