[AI_TOOLS_LOG]// APRIL 17, 2026 // 7 MIN READ

I Put Two AIs in My Pre-commit Hook and Made Them Argue

#AI#PRE-COMMIT#CODEX#CLAUDE#CODE-REVIEW#AUTOMATION

Before every commit, I let Codex tear apart the staged diff. If Claude thinks Codex is wrong, the commit goes through. If both flag the same issue, the commit gets blocked.

This all runs inside pre-commit. Here's how it came together, and what each of the three layers is actually solving.

Why I Wanted to Move the First Review Earlier

Post-commit review still matters — I even built a separate argue bot to handle the Feishu round, which I wrote about elsewhere.

But some problems aren't worth waiting until after the commit to catch for the first time:

  • An image pasted from the design tool, still uncompressed
  • A file I touched just to reformat it, now mixed into a feature commit
  • An obvious logic hole in the staged diff that I missed because I was thinking about something else

What these have in common: they're cheapest to fix locally. Once the code is up, you get a report, context-switch back, and handle it — that's a much higher cost.

So I drew a clear line:

  • Before commit: handle mechanical issues and obvious risks that can be judged locally
  • After commit: review the impact on others, check what CI says, evaluate the PR as a whole

The pre-commit has three layers, ordered by certainty: image assets, formatting detection, adversarial review.

Layer 1: Image Assets — Let the Script Handle the Mechanical Stuff

Background: how it used to work

I work on Android projects. Images go in res/drawable, referenced in code as R.drawable.icon_name and in XML as @drawable/icon_name.

Design assets come from 蓝湖 (Lanhu) — you download the cut image as png or jpg, uncompressed. Getting one image into the project used to look like this:

  1. Cut the asset from Lanhu, download locally
  2. Open squoosh.app, upload the image
  3. Run both lossy and lossless compression, compare sizes, manually pick the smaller one
  4. Check whether converting to webp is worth it
  5. Download the compressed file under whatever name you want, drop it into res/drawable, then write the code

Every image required this whole cycle. The compression step especially was always manual trial-and-error: lossy or lossless, how much smaller did it actually get, is webp worth it — sometimes the compressed png ended up smaller and you kept it as-is.

How it works now

With the script, I do exactly one thing: copy the asset from Lanhu directly into the drawable directory. The rest is handled by pre-commit:

The script only processes images in the staged area — .9.png, gif, and animated webp are skipped. For everything else, it runs both lossy (cwebp) and lossless compression simultaneously, picks the smaller result, renames it to _opt.webp, then scans all kt, java, and xml files to update resource references from the old name.

One edge case: if I already compressed the image myself — it's a webp and the filename contains _opt. — the script skips it.

Naming conflicts (two different images compressing to the same output name) cause the script to block the commit rather than silently overwrite.

Layer 2: Pure Formatting — Don't Let It Contaminate Feature Commits

This layer has no AI at all. It's the most low-tech of the three, and also the most consistently useful.

My IDE auto-formats on save. Sometimes I open a file someone else wrote, change two lines, and the IDE rewrites the entire file's indentation and import order in the process. That kind of change mixed into a feature commit makes git blame, code review, and issue tracing all harder to read.

So the hook checks staged kt/java files for a specific pattern:

  • This file's last commit wasn't made by me
  • The staged content, stripped of whitespace, is essentially the same as HEAD

If that's true, it marks the file as "pure formatting" and gives me three choices: commit anyway (including the formatting), unstage the formatting files and commit only the functional changes, or cancel the commit entirely.

I've found this more useful than I expected. A lot of the time I had no idea the IDE touched someone else's file — this is the first place I find out.

Layer 3: Two AIs Arguing Over the Staged Diff

This is what the article is actually about.

What Codex is

Codex is OpenAI's code agent. There's an official plugin openai/codex-plugin-cc that installs directly inside Claude Code:

/plugin install codex@openai-codex

Once installed, it provides /codex:adversarial-review — a tool specifically designed to challenge code: question implementation choices, stress-test assumptions, find potential crash paths and data risks. It doesn't care about naming or formatting.

I wired this into scripts/pre_commit_review.py so it runs automatically on every git commit.

How the flow works

  1. Extract the code files with actual changes from the staged diff (kt, java, py, js, etc.)
  2. Codex does adversarial review
  3. Codex finds nothing → commit passes, Claude never gets involved
  4. Codex finds issues → hand them to Claude and ask it to argue the other side
  5. Claude finds Codex's objections invalid → commit passes
  6. Claude also confirms the issue → commit blocked, both judgments printed

The review only covers: logic bugs, crash risks, data corruption, obvious performance issues, security vulnerabilities. Naming, comments, and style don't belong here — that's for after the commit.

Why two models

Two reasons.

First, a single model misfires too easily. Codex sometimes challenges tradeoffs that are intentional — and reasonable given the local context. Claude, playing defense with access to the full surrounding codebase, can push back more accurately than a one-vote veto by Codex alone.

Second, mutual fallback. When Codex struggles, Claude can catch what it missed. When Claude's quota is tight, Codex can run the whole flow independently. Both models can complete this layer on their own — they just produce fewer false positives together.

I Also Built a Cache for This Layer

Without caching, this kind of setup quickly makes you want to run --no-verify.

I hash each file's diff content with SHA256, take the first 24 characters as the key, and store review results in .git/review-cache/. If the code hasn't changed, the next commit hits the cache immediately — no model call. Change the code, the hash changes, the cache invalidates automatically.

# Clear all cache (force re-review)
rm -rf .git/review-cache/

# Check how many entries are cached
ls .git/review-cache/ | wc -l

When the cache hits, the extra latency is essentially zero. When it misses, you wait for the model — anywhere from a few seconds to a few minutes depending on diff size.


When I look at this pre-commit layer now, the priorities are pretty clear:

  • Mechanical work like image assets — push it to the script
  • Noise like pure formatting — keep it out of feature commits
  • Obvious mistakes in the staged diff — catch them locally while the context is hot

By the time code actually lands, I'm not dealing with a stack of things I could've fixed earlier. I'm dealing with the part of the review report that actually deserves attention.

SX
Written by sunxu

Android engineer. Code and words.

© 2026 sunxu · CC BY-NC 4.0Non-commercial · Attribution required