Diff review

Attach a compact working-tree diff to an LLM gate so the model reviews the actual change before the agent stops, instead of a guess reconstructed from the transcript.

A review hook that reads only the transcript sees scattered edit blocks, and it never sees changes made outside the agent’s own edits. The diff=True flag hands the model a single compact ccx diff, budget-bounded so it stays cheap, so the review is grounded in the real change. When ccx isn’t installed it falls back to a plain git diff.

"""Review the working-tree diff for leftover debugging artifacts before the agent stops."""

from __future__ import annotations

from captain_hook import Allow, Block, Event, Input, T, TouchedFile, llm_gate

# `diff=True` attaches a compact `ccx vcs diff` (a plain `git diff` when ccx is absent) as a
# <diff> block, so the model reviews the actual change instead of reconstructing it from
# the transcript. Stop carries no tool input, so the cheap pre-filter is the
# transcript-history condition TouchedFile — "did this session edit Python source?" —
# and the gate fires at most once before the agent stops.
llm_gate(
    "The working-tree diff is in <diff>. Did this change leave debugging artifacts in the "
    "code, such as a stray print/console.log/debugger, a commented-out block, or a "
    "temporary TODO marked for removal? Block only if the diff clearly adds one.",
    message="Remove the debugging leftovers before stopping: {reasoning}",
    diff=True,
    only_if=[TouchedFile("**/*.py")],
    events=Event.Stop,
    model="small",
    max_fires=1,
    tests={
        Input(
            transcript=[T.assistant(T.tool("Edit", file_path="src/app.py", old_string="a", new_string="b"))]
        ): Block(),
        Input(transcript=[T.assistant(T.tool("Edit", file_path="README.md", old_string="a", new_string="b"))]): Allow(),
    },
)
NoteWhat the inline tests cover

The inline tests pin the deterministic gate: a session that edited Python source reaches the model, one that didn’t stays silent. Under uvx capt-hook test the model verdict itself is stubbed to always confirm — exercise the real judgment by replaying live sessions.

What it does

diff=True resolves to evt.ctx.diff(), which prefers cc-context’s token-budgeted ccx diff and falls back to git diff when ccx is unavailable. The result is wrapped in a <diff> block alongside the recent-transcript window, so the model reviews the change directly.

diff=True                          # attach a compact <diff> of the uncommitted change
only_if=[TouchedFile("**/*.py")]   # cheap pre-filter: only sessions that edited Python source
events=Event.Stop                  # review once, right before the agent stops

Stop carries no tool input, so the pre-filter is a transcript-history condition rather than a tool-input one. Deterministic conditions run first, so the model only sees a session that edited source. Diff against something other than the working tree with diff="staged" or diff="HEAD~1".

Run it yourself

uvx capt-hook --hooks docs/examples test

See also