DEV Community

Cover image for How I Put a Code Reviewer in Front of Every git push to Main with a Claude Code Subagent
Digital Craft Workshop
Digital Craft Workshop

Posted on • Originally published at Medium

How I Put a Code Reviewer in Front of Every git push to Main with a Claude Code Subagent

Pasted image

Three Wednesdays ago I shipped a regression to production. The diff looked fine. CI was green. I ran the dev server, clicked through the happy path, pushed, and went to bed.

The next morning a real user emailed me to say my landing-page signup button did nothing.

The bug was obvious in hindsight. The AI had quietly removed an await from an analytics call, the call threw inside a try block, and the catch swallowed it along with the rest of the handler. Three minutes to revert. Took the rest of the morning to feel okay about it again.

I'm a solo builder. No senior engineer reads my PRs. Nothing sits between my keyboard and main. I let Claude Code write most of my code now — which means most of what hits main was written by something that doesn't know it shouldn't swallow exceptions.

Sidebar: this story is one piece of how I think about AI as a solo founder's tool — I have a 5-email series on the broader pattern if you want the bigger picture.

So I built the second pair of eyes. One Claude Code subagent, one 30-line pre-push hook, shared across every repo I own. It runs on every push to main, takes about 20 seconds, and has caught real bugs four times in three weeks.

Here's how it works.

Subagent checkpoint intercepts a push to main and forks into approve or block pathsThe architecture

Three pieces, nothing exotic.

A subagent definition — a markdown file that tells Claude how to act when invoked as a reviewer. Lives in .claude/agents/code-reviewer.md.

A pre-push git hook — bash script that gathers the diff, fires up a headless Claude Code instance pointed at the subagent, and prints the verdict.

A .shared/ folder + symlinks — both files live once at the top of my foundary-tools/ monorepo and symlink into each child repo's .claude/ and .git/hooks/. One install script wires everything.

Three pieces because each one owns a different lifetime. The subagent definition is the slowest-changing — I edit it maybe once a week as I learn what false positives look like. The hook is the contract with git — set it up once, leave it alone. The symlinks are infrastructure — wired during install-hooks.sh and forgotten. Bundling them into one script would mean re-installing across nine repos every time I tweak a prompt rule. Keeping them separate means the prompt edits live in version control and propagate for free.

The reviewer only fires on pushes to main. Feature branches push freely. The cost is one Opus call per release-shaped push — pennies — and the upside is a 20-second sanity check before a regression goes live.

The whole thing fits in three files — and the most important one isn't the hook.

The subagent definition

The subagent is a plain markdown file with frontmatter. Claude Code reads the description, knows when to dispatch to it, and uses the body as the system prompt.

---
name: code-reviewer
description: "Reviews staged commits before push. Flags"
regressions, swallowed errors, removed awaits, broken
invariants, leaked secrets, and missing migrations.
model: opus

tools: Read, Grep, Glob, Bash

You are reviewing a diff that is about to land on main\.

Output exactly one of:

  • "APPROVE: "
  • "BLOCK: \n"

Block only for things that will break production or leak
secrets. Style nits, naming preferences, and "could be cleaner"
notes do not block. The author is a solo builder shipping
fast; assume they accepted any trade-off that isn't a bug.

Always check:

  • Removed await\ on async calls
  • New catch\ blocks that swallow without logging
  • Hard-coded URLs that look like prod endpoints
  • .env\ keys printed to logs
  • Schema changes without a matching migration file
  • console.log\ of request bodies (usually leaks PII)

The output contract is one line. No essay, no "great work, here are some thoughts." Either APPROVE: ... or BLOCK: .... The hook script greps for the prefix and acts on it.

The subagent commits to a verdict instead of hedging. Without this contract, every review came back as a four-paragraph essay that ended with "but ultimately, this is your call."

The block criteria are narrow. I don't want the reviewer to lecture me about my variable names or suggest I add comments. I want it to catch things that will page me in the morning.

The solo builder shipping fast line in the prompt is load-bearing. Without it, the reviewer was blocking pushes for things like "consider extracting this into a helper" — which is exactly the kind of advice I added the reviewer to ignore.

If you've ever fought with Claude over getting it to follow a strict output format, the same lesson applies here: give the model a verdict to commit to, not an essay to write.

The pre-push hook

The hook is a bash script at .git/hooks/pre-push. Git invokes it before talking to the remote and aborts the push if the script exits non-zero.

!/usr/bin/env bash

set -euo pipefail

REMOTE_BRANCH="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "")"
[[ "$REMOTE_BRANCH" == */main ]] || exit 0

DIFF="$(git log --stat origin/main..HEAD; git diff origin/main..HEAD)"
[[ -n "$DIFF" ]] || exit 0

echo "Running code-reviewer subagent..."
VERDICT="$(echo "$DIFF" | claude \
--dangerously-skip-permissions \
--print \
--agent code-reviewer 2>&1)"

echo "---"
echo "$VERDICT"
echo "---"

if [[ "$VERDICT" == BLOCK:* ]]; then
printf "Override? [y/N] "
read -r REPLY </dev/tty
[[ "$REPLY" =~ ^[Yy]$ ]] || exit 1
fi

It only runs for pushes whose upstream tracks main. A push to a feature branch returns early. That's the difference between 20 seconds added to every push and 20 seconds added to releases.

Side note: if you're wiring Claude Code deeper into your own workflow, I put the setup I lean on into a short, free email series — The Claude Code Memory Starter — one small lesson at a time.

The script ignores git's ref-list stdin and recomputes the diff from origin/main..HEAD. The combination of git log --stat and git diff gives the reviewer commit messages plus the actual hunks — messages help it figure out intent when judging whether a deletion was deliberate.

--dangerously-skip-permissions is fine here because the subagent has read-only tools (Read, Grep, Glob, Bash). It can't edit anything or push.

The read -r REPLY </dev/tty is the one tricky line. Pre-push hooks get git's ref list on stdin, so you have to redirect from the controlling TTY to read the user. Without this, the prompt prints and immediately accepts an empty answer.

Terminal output of the pre-push hook printing a BLOCK verdict with an Override prompt

**One hook, one repo. The next part is how it became one hook across nine.**

Sharing one config across every repo

I have nine repositories in ~/foundary-tools/. I'm not going to copy a hook and a subagent definition into each one and then forget which is the canonical version after the third edit.

The fix is a .shared/ folder at the top of the monorepo.

foundary-tools/
├── .shared/
│ ├── agents/code-reviewer.md
│ ├── hooks/pre-push
│ └── install-hooks.sh
├── drippery/
├── article-forge/
├── grownote/
└── ...

install-hooks.sh walks every direct child directory and creates two symlinks per repo.

!/usr/bin/env bash

set -euo pipefail
SHARED="$(cd "$(dirname "$0")" && pwd)"

for repo in "$SHARED/.."/*/; do
[[ -d "$repo/.git" ]] || continue
mkdir -p "$repo/.claude/agents"
ln -sf "$SHARED/agents/code-reviewer.md" \
"$repo/.claude/agents/code-reviewer.md"
ln -sf "$SHARED/hooks/pre-push" \
"$repo/.git/hooks/pre-push"
chmod +x "$repo/.git/hooks/pre-push"
echo "wired: $(basename "$repo")"
done

Symlink topology — one .shared/ folder fanned out to nine repos

**Run it once. Edit** `code-reviewer.md` **later and every repo picks up the change on the next push.** No copy, no drift.

If you have one repo, the symlink trick is overkill — just put the files directly in .claude/agents/ and .git/hooks/. The subagent and the hook are the part doing the work.

The harder problem isn't sharing the rule. It's making myself willing to obey it.

Soft warning, not hard block (and --no-verify still works)

The first version of the hook used exit 1 on BLOCK: and forced me to amend or escape-hatch. I rewrote it within a day.

I push during moments where stopping is expensive — the kid wakes up, the evening ends, the meeting starts in eight minutes. A hard block on a false positive doesn't help anyone; it just teaches me to disable the hook.

The soft warning works because it changes the default. Default is don't push. Pressing Enter aborts; pressing y proceeds.

I read the verdict, decide in two seconds, and either fix or override. Four times in three weeks, the verdict was right and I went back to fix. Roughly ten times the verdict was wrong and I pressed y and shipped.

If I had set hard block, I'd have disabled the hook by week two. The override is what keeps the hook installed.

This is the same lesson I learned with my pre-deploy script that refuses to add a staging environment: I keep using the tools that make the safe path easy without locking the risky path away.

git push --no-verify bypasses every git hook, including this one. I don't try to "fix" that. If I need to ship a hotfix at 11pm and the reviewer is hallucinating about a deleted file, I want one well-known flag to skip the check, not a fight with my own tooling.

The reviewer is a default-on layer. It's not a security gate. Security gates belong in CI, where I can't --no-verify past them. The reviewer's job is to catch the bugs I'd catch myself if I weren't tired.

What I deliberately do not ask the reviewer

I tried adding more checks at first. I removed most of them.

The first version flagged every function over 30 lines for "consider extracting a helper." By the third push it had blocked a 45-line tenant migration that I had no intention of splitting — the SQL was inherently sequential. I deleted that rule the same evening. Same story with "magic numbers" — the reviewer kept flagging timezone offsets and HTTP status codes. Out.

The reviewer does not:

  • Run tests (CI does)
  • Run the type checker (CI does)
  • Check formatting (prettier --check does)
  • Look for style violations (taste, not bugs)
  • Suggest refactors (different mode of work)

It checks the things a human reviewer would catch in a hurried PR scan — swallowed exceptions, dropped awaits, schema drift, leaked secrets — and stays out of everything else. That narrow scope is why it runs in 20 seconds and why I trust the verdict enough to act on it.

If it flags everything I stop reading it.

Reviewer scope: swallowed exceptions, dropped awaits, schema drift, leaked secrets vs tests, type checks, formatting, namingWhat this gets you

Three weeks in, the receipts:

  • Four real catches (two dropped awaits, one swallowed error, one debug console.log of a JWT)
  • Roughly ten false positives, all dismissed by pressing y
  • Zero "I disabled the hook because it was annoying" moments
  • A single canonical config across nine repos
  • About $0.40 in Opus calls

The JWT one was the closest to a real incident. I'd added a console.log(req.headers) while debugging a webhook signature mismatch, forgot to remove it, and went to push. The reviewer flagged the leaked token in five seconds. That alone earned the next year of the reviewer's existence.

The .shared/ symlink trick stops mattering when you only have one repo, but the subagent and the hook still work — just live in the repo directly.

If you're already using Claude Code, the whole setup is one subagent file, one hook script, and an install-hooks.sh if you have more than one repo. About 30 minutes from cold to first wired push.

This is the third Claude Code workflow I've shipped this month that ends up being a short script. The other two — a skill that turns an hour of project setup into 90 seconds and a memory layer that survives between sessions — follow the same pattern. Most of my wins lately are thirty lines of glue around something Claude Code already ships.

If this was useful, you might also like The Claude Code Memory Starter — a short, free email series on getting Claude Code to remember what matters between sessions.

Top comments (0)