A few months ago, while building a small app, I noticed that asking Claude Code to review my code on every save was both expensive and disruptive to my flow. Manually typing /review was reliable but easy to forget. After thinking it over, I realized the right moment was the one I was already pausing to review my work: the moment of git commit. And the right scope wasn't the whole repo — only what I was about to commit.
That led me to combine husky and lint-staged with Claude Code so that only staged files would be passed to a quick AI review. The result was quieter and more useful than I expected, and this article walks through the exact setup from a solo developer's point of view.
Why "staged files only" is the right scope
Claude Code is happy to read the entire repository, but doing that on every commit chops your rhythm into pieces. What I noticed in practice is that when you give it only the files you're about to commit, the feedback becomes sharper, not weaker.
The reason is simple. With git diff --cached you're handing Claude exactly the change you're proposing — nothing more, nothing less. It stops reaching for "general advice" and starts asking the right question: does this change make sense, by itself, right now?
There's a practical reason too: lint-staged's design — passing the staged file paths as command-line arguments — fits beautifully with Claude Code's --print (non-interactive) mode. You get a clean shell pipeline that scales to any number of changed files.
Prerequisites and the big picture
Before we begin, make sure your environment has:
- Node.js 20 or later (required by husky v9)
- The
claudeCLI on yourPATH(claude --versionshould return cleanly) - A repository with a
package.json
The setup itself is small:
- Initialize husky and create
.husky/pre-commit - Install lint-staged and configure which file globs trigger which task
- Have lint-staged invoke a tiny shell script that hands staged files to Claude Code
- Exit non-zero from the script if Claude raises any concern, blocking the commit
Step 1: Set up husky and lint-staged
Install both as dev dependencies:
# Run from the project root
npm install --save-dev husky lint-staged
# Initialize husky v9 (creates .husky/)
npx husky initnpx husky init writes a starter .husky/pre-commit:
# Default content
npm testReplace it with npx lint-staged:
# .husky/pre-commit
npx lint-stagedThen add the lint-staged config to package.json:
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"scripts/claude-staged-review.sh"
]
}
}The trick here is keeping the glob narrow. In my own project I deliberately exclude Markdown and config files. When the review target gets too wide, Claude has too many genres of files to reason about and the feedback dilutes.
Step 2: Write the review script
Create scripts/claude-staged-review.sh:
#!/usr/bin/env bash
# scripts/claude-staged-review.sh
# lint-staged passes staged file paths as positional arguments
set -euo pipefail
FILES=("$@")
if [ ${#FILES[@]} -eq 0 ]; then
echo "ℹ️ No files to review. Skipping."
exit 0
fi
# Prompt body sent to Claude
PROMPT=$(cat <<'PROMPT_EOF'
Below is a diff of files staged for the next git commit.
Act as a code reviewer and check ONLY these four points:
1. Obvious bugs, null safety, or type mismatches
2. Changes that need tests but have none added
3. Accidentally committed secrets (API keys, tokens, PII)
4. Serious naming or separation-of-concerns issues
If everything looks fine, output exactly "OK" on the first line.
If there are concerns, list them as terse bullet points.
Do NOT suggest refactors or general improvements.
PROMPT_EOF
)
# Get the staged diff for the changed files only
DIFF=$(git diff --cached -- "${FILES[@]}")
# Run Claude Code in non-interactive mode
RESULT=$(printf '%s\n\n```diff\n%s\n```\n' "$PROMPT" "$DIFF" | claude --print --model claude-haiku-4-5)
echo "$RESULT"
# If the first line is anything other than "OK", abort the commit
if [ "$(echo "$RESULT" | head -n1 | tr -d '[:space:]')" = "OK" ]; then
exit 0
else
echo ""
echo "❌ Claude flagged concerns. Fix them and commit again."
echo "💡 To bypass temporarily: git commit --no-verify"
exit 1
fiDon't forget to make it executable:
chmod +x scripts/claude-staged-review.shA few decisions in this script are worth explaining.
I'm calling claude --print --model claude-haiku-4-5 because pre-commit only earns its keep if it's fast. Sonnet and Opus catch slightly more, but the wait time will eventually push you to skip the hook entirely. With Haiku 4.5, I get a hit on the obvious-bug-or-secret category in roughly the time it takes me to type a commit message.
The prompt explicitly says "no refactor suggestions." Claude is naturally helpful — left alone, it will offer renames, decompositions, and design ideas. All good in code review, all distracting at commit time. Setting the boundary upfront removes that noise.
The exit logic checks "first line equals OK" rather than scanning the whole response. Claude sometimes writes "OK. Looks fine." or "OK." — both pass, which is what you want.
Step 3: Try it out
Make a trivial change and commit:
echo "console.log('test')" >> src/example.ts
git add src/example.ts
git commit -m "test pre-commit"You should see something like:
✔ Backed up original state in git stash
✔ Running tasks for staged files...
ℹ️ No files to review. Skipping.
✔ Applying modifications from tasks...
[main abc1234] test pre-commit
Now intentionally introduce a problem:
// src/example.ts
const apiKey = "sk-ant-api03-XXXXXXXXXXXXX"; // accidentally committed real token
console.log(apiKey);Claude will stop you:
- src/example.ts contains what looks like a real Anthropic API key.
Remove it and load the value from an environment variable.
- Rotate the key in your console — it may already be exposed.
❌ Claude flagged concerns. Fix them and commit again.
💡 To bypass temporarily: git commit --no-verify
That moment — the "oh no, I would have pushed that" moment — is the entire point of this setup. Without breaking your normal git flow, you get one extra pair of eyes at the right time.
What I learned running this in real projects
My first version was extravagant: every staged file, full Sonnet review, broad prompt. After three commits I was already using --no-verify by reflex. The hook was technically working but I had stopped trusting it.
Trimming it down to "staged files only, Haiku 4.5, four explicit checks" brought the average run time to four to six seconds. That's about how long it takes to write a half-decent commit message, so the friction disappears entirely.
The lesson, for me, was that the goal isn't to push Claude to its limit. It's to make Claude small enough to live inside the rhythm I already have.
Tuning knobs
Vary the prompt by file type
You can branch on ${FILES[@]} and use different prompts for tests vs. production code:
# Crude example: detect test files
if [[ "${FILES[0]}" =~ \.test\. ]]; then
PROMPT="$PROMPT_TEST"
else
PROMPT="$PROMPT_PROD"
fiThat said, I'd be cautious about getting clever here. Pre-commit scripts are easier to live with when they're boring and obvious.
Cost in practice
A typical 5–10 file commit reviewed by Haiku 4.5 lands at a small fraction of a cent. Even at 30 commits per day, my own bills stay around a dollar or two per month for a small project.
If you want to trim further, exclude *.test.* from the lint-staged glob, or drop docs/ from the review path entirely.
Conditional escalation to Sonnet
For larger changes (refactors, new features), you can route to Sonnet based on diff size:
LINES=$(echo "$DIFF" | wc -l)
if [ "$LINES" -gt 200 ]; then
MODEL="claude-sonnet-4-6"
else
MODEL="claude-haiku-4-5"
fi
RESULT=$(printf '...' | claude --print --model "$MODEL")Diffs over 200 lines get the more thoughtful model; smaller changes stay fast.
How this compares to similar approaches
Before settling on this setup I considered a couple of alternatives.
Claude Code's own Hooks (PreToolUse, PostToolUse, etc.) react to tool calls inside a Claude Code session. They're powerful — see the Claude Code Hooks Master Guide — but they don't fire on git events, so they don't fit this use case.
GitHub Actions PR review is the obvious cloud counterpart. It works well, but the feedback arrives a minute or two after pushing, which is too late for the "catch it before it leaves the laptop" goal of this article. The two approaches are complementary: a fast local pre-commit catches obvious issues, then a deeper PR review handles design-level concerns.
If you want to dig into claude --print itself, the headless automation guide for --bare and --print covers the underlying mode.
Troubleshooting
claude command not found
husky hooks run in a non-login shell, so your usual PATH may not be loaded.
# Add this near the top of .husky/pre-commit
export PATH="$HOME/.local/bin:/opt/homebrew/bin:$PATH"
npx lint-stagedThat covers most cases.
lint-staged says "no staged files matched"
This just means the staged files don't match your glob. A Markdown-only commit, for example, won't trigger *.{ts,tsx,js,jsx}. That's expected behavior — nothing is broken.
Bypassing the hook for hotfixes
When you genuinely need to skip review:
git commit --no-verify -m "hotfix: rollback"If you find yourself reaching for --no-verify often, take that as a signal to narrow the prompt further, not to disable the hook.
A starting point
Start with *.{ts,tsx,js,jsx} on a small project. Copy the script as-is, change one file, and commit. The first run tells you immediately whether your PATH and CLI access are wired correctly.
Once it's running, you'll notice that Claude Code stops feeling like a heavy reviewer you have to invoke and starts feeling like a quiet partner standing next to you at commit time. In my own work, this setup has caught two accidental key commits that would have been painful to recover from.
Thanks for reading. If you've been looking for a way to add an AI review step to your local workflow without slowing yourself down, I hope this helps you get there.