Most of us have a version of this story: you open a pull request, and within four minutes a reviewer points out the debug log you left in, the renamed variable you only renamed in half the call sites, or the TODO you wrote to yourself at 1 a.m. and forgot. None of these are deep problems. They are attention problems — and attention is exactly what you run out of after finishing the hard part of a change.
Lately I've been experimenting with a small habit: before I push, I run my own diff through a coding model with a narrow, boring prompt, and I treat the output the way I'd treat a spell-checker — not an authority, just a second pair of eyes that never gets tired. This post is the workflow, the script, and an honest list of where it fails.
Why self-review, and why before the PR
Human review is scarce and socially expensive. Every trivial comment a reviewer has to write costs goodwill and context-switching. The goal here is not to replace review; it's to burn down the category of comments that never should have survived to the PR stage, so human reviewers spend their time on design and intent instead of console.log archaeology.
There's also a psychological benefit I didn't expect: knowing a model will see the diff first makes me write cleaner intermediate code. It's a weak form of the "explain it to a rubber duck" effect, except the duck occasionally replies with something useful.
The constraint: this has to be free to be a habit
A workflow you run on every commit only survives if it costs nothing — no per-call invoice, no GPU sitting in the corner. For this experiment I used MonkeyCode, which currently offers free access to coding models and a free server option, so the whole loop runs without touching my own hardware or a paid API key. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I can't speak to how long the free tier lasts or what its limits are, so treat the availability as "good right now" and keep the script backend-agnostic — the design below works with any OpenAI-compatible endpoint, which is deliberate. If the free option disappears, you point the same script at whatever you're using next.
The artifact: a 40-line pre-push self-review script
The core idea: generate the diff against your merge base, stuff it into a tightly scoped prompt, and print the result into your terminal — never into the codebase.
#!/usr/bin/env bash
# selfreview.sh — run before pushing a feature branch
set -euo pipefail
BASE="${1:-main}"
DIFF=$(git diff "$(git merge-base HEAD "$BASE")" -- . ':(exclude)*.lock' ':(exclude)package-lock.json')
if [ -z "$DIFF" ]; then
echo "No diff against $BASE. Nothing to review."
exit 0
fi
# Truncate absurdly large diffs; big changes deserve a human first.
if [ "${#DIFF}" -gt 60000 ]; then
echo "Diff too large for a useful pass ($(echo "$DIFF" | wc -l) lines). Split it up."
exit 1
fi
PROMPT=$(cat <<'EOF'
You are reviewing a git diff the author is about to push.
Report ONLY these categories, and nothing else:
1. Leftover debugging artifacts (logs, breakpoints, commented-out code).
2. Renames or signature changes that look incompletely applied.
3. Obvious null/error paths that the change introduced but didn't handle.
4. Anything that looks committed by accident (secrets-shaped strings, local paths, fixtures).
For each finding: file, line context, one sentence. If a category has no findings, omit it.
Do not comment on style, naming preferences, or architecture.
End with a one-line verdict: SHIP-LOOKING or NEEDS-ANOTHER-PASS.
EOF
)
curl -s "$MCD_BASE_URL/chat/completions" \
-H "Authorization: Bearer $MCD_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg sys "$PROMPT" \
--arg diff "$DIFF" \
'{model: env.MCD_MODEL, messages: [
{role: "system", content: $sys},
{role: "user", content: $diff}
], temperature: 0}')" \
| jq -r '.choices[0].message.content'
Three environment variables (MCD_BASE_URL, MCD_API_KEY, MCD_MODEL) are the entire coupling to the provider. I hook it into my flow with a git alias:
git config alias.selfreview '!bash ./scripts/selfreview.sh'
Why the prompt is deliberately boring
The single biggest quality lever was restricting the categories. When I first tried this with an open-ended "review my code" prompt, the model produced confident, verbose, mostly useless commentary — the same failure mode as a junior reviewer trying to prove they read everything. Constraining it to four mechanical categories where language models are actually decent (pattern-shaped anomalies like leftover logs and half-applied renames) cut the noise dramatically. Explicitly banning style commentary mattered just as much; otherwise every run ends in a lecture about my variable names.
The temperature-0 setting and the one-line verdict exist for the same reason: I want a checklist, not a conversation.
What to delegate vs. what to keep human
After a few weeks of runs, here's my honest split:
| Task | Delegate to the model pass? | Why |
|---|---|---|
| Leftover debug logs, breakpoints | Yes | Pure pattern matching, near-zero judgment needed |
| Half-applied renames across a diff | Yes | Cross-hunk consistency is a strength; you will miss these |
| Secrets-shaped strings, local paths | Yes, as a supplement | Cheap catch, but keep real secret-scanning in CI |
| Error-path gaps it flags | Verify each one | Decent hit rate, real false positives |
| Logic correctness of the change | No | It can't know your intent |
| API/design decisions | No | This is what your reviewers are for |
| Test coverage judgment | No | It flatters whatever tests exist |
Failure modes, stated plainly
- It hallucinates findings about context it can't see. The model only gets the diff, so it will sometimes "flag" a call site that lives outside the diff and is actually fine. My rule: every finding must be verified in the actual file before I act on it. The pass proposes; I dispose.
- Large diffs degrade it. That's why the script hard-fails above a size threshold rather than silently truncating. A 2,000-line diff needs to be split into commits, not fed to a model.
- Free tiers are a gift, not a platform. Quotas, latency, and availability can change. The script's provider-agnostic shape is the mitigation, not an afterthought.
- Sensitive diffs don't go anywhere external. My previous post was about not handing credentials to agents; the same instinct applies here. If a branch touches anything proprietary-sensitive, self-review locally or skip the pass entirely.
Who should skip this
If your team already has fast, thorough review with low turnaround, the marginal value is small. If your diffs routinely contain confidential algorithms or customer data, an external model call is the wrong tool no matter the price. And if you find yourself fixing things because the model said so rather than because you verified them, stop — that's the failure mode, not the workflow.
Where it landed for me
The honest result: the script catches one or two genuinely embarrassing things per week, mostly debug output and one memorable half-renamed function. That's a modest yield, but the cost is near zero, and it has shifted the comments on my PRs noticeably toward substance. If you want to try the same loop, MonkeyCode's free model access and free server are a low-friction way to run it today — just keep the script backend-agnostic so the habit outlives any particular provider.
The deeper point isn't about any specific tool. It's that the cheapest review comment is the one that never has to be written, and a tired author is the worst person to catch their own 1 a.m. decisions. A boring, narrowly-scoped model pass turns out to be a surprisingly good stand-in for a fresh pair of eyes.
Top comments (0)