All posts

· 9 min read

A security agent that reviews your code before every commit

How to build a Claude Code subagent that reviews your staged diff, and a pre-commit hook that blocks the commit when it finds secrets or SQL injection. With the real files and where it breaks.

On this page
  1. What a subagent is in Claude Code
  2. Step 1: create the agent
  3. Step 2: test it by hand
  4. Step 3: the pre-commit hook
  5. Install it
  6. What happened in the test
  7. Where it breaks
  8. What I take from it

Video resources

Files from this episode, ready to copy or download.

  • Agent

    security-reviewer · reviewer.md

    Claude Code subagent that reviews the staged diff and ends with VERDICT: PASS or VERDICT: BLOCK.

    Install at.claude/agents/reviewer.md

    View contents
    ---
    name: security-reviewer
    description: Revisa cambios de código buscando vulnerabilidades de seguridad antes de un commit o PR. Úsalo cuando haya un diff para revisar.
    tools: Read, Grep, Glob, Bash
    ---
    
    Eres un revisor de seguridad de aplicaciones. Revisa SOLO los cambios que te pasen
    (el diff), usando el resto del repo únicamente como contexto.
    
    Busca:
    
    - Secretos o credenciales en el código (API keys, tokens, contraseñas, llaves privadas)
    - Inyección SQL, de comandos o de rutas
    - Input del usuario sin validar o sin escapar (XSS, deserialización insegura)
    - Autenticación o autorización rota o eliminada
    - Datos sensibles en logs o mensajes de error
    - Dependencias nuevas o sospechosas
    
    Reglas:
    
    - No inventes. Si no estás seguro, márcalo como "Posible" y explica qué falta para confirmarlo.
    - No comentes estilo ni rendimiento.
    - Por cada hallazgo: archivo:línea, severidad (CRÍTICA/ALTA/MEDIA/BAJA), por qué es un problema y cómo arreglarlo.
    - Si no hay hallazgos, dilo en una línea.
    
    La última línea de tu respuesta debe ser exactamente una de estas:
    VERDICT: BLOCK (si hay al menos un hallazgo CRÍTICO o ALTO)
    VERDICT: PASS (en cualquier otro caso)
    
  • Hook

    pre-commit · pre-commit

    Git hook that sends the staged diff to the agent and stops the commit when it answers BLOCK. After copying it: chmod +x .git/hooks/pre-commit.

    Install at.git/hooks/pre-commit

    View contents
    #!/usr/bin/env bash
    # Revisión de seguridad con Claude Code antes de cada commit
    
    DIFF=$(git diff --cached --unified=5)
    [ -z "$DIFF" ] && exit 0
    
    REPORT=$(printf '%s' "$DIFF" | claude -p \
      "Revisa este diff en staged según tus instrucciones." \
      --agent security-reviewer \
      --allowedTools "Read,Grep,Glob")
    
    echo "$REPORT"
    
    if printf '%s' "$REPORT" | tail -n 1 | grep -q "VERDICT: BLOCK"; then
      echo "❌ Commit bloqueado por security-reviewer. Revisa el reporte."
      exit 1
    fi
    

This file has two problems: an API key written straight into the code and a SQL query built with an f-string. I ran git add and git commit, and Git accepted it without a word.

import sqlite3

STRIPE_KEY = "sk_live_FAKE_0000000000000003"  # ficticia

def get_user(username):
    conn = sqlite3.connect("app.db")
    query = f"SELECT * FROM users WHERE name = '{username}'"
    return conn.execute(query).fetchone()

The key is fake, but if it were real it would already be in your history, and the moment you push, anyone can read it. That is how credentials leak: not because nobody knows better, but because at eleven at night nobody reviews their own diff. I use this daily: a Claude Code agent that reviews what is staged right before the commit and stops it when something is serious.

The video is at youtu.be/Cmw1km8dR5U (in Spanish) and both files are in the video repo. Here is the walkthrough and, at the end, where it breaks.

What a subagent is in Claude Code

A subagent is a Markdown file. The top is frontmatter with a name, a description of when to use it, and the tools it may use. Below that are the instructions: its role, what to look for, and how to answer.

Why this instead of typing “review my code” every time? Three reasons. The instructions are always the same, so the review is consistent. You can restrict its tools. And a script can invoke it, which is what lets us hook it into Git.

Step 1: create the agent

Inside the project I create .claude/agents/ and a file in it, reviewer.md. The file is called reviewer.md, but the agent is identified by its name field, which is security-reviewer. That is the name the hook uses later.

This is the full file, exactly as I used it:

---
name: security-reviewer
description: Revisa cambios de código buscando vulnerabilidades de seguridad antes de un commit o PR. Úsalo cuando haya un diff para revisar.
tools: Read, Grep, Glob, Bash
---

Eres un revisor de seguridad de aplicaciones. Revisa SOLO los cambios que te pasen
(el diff), usando el resto del repo únicamente como contexto.

Busca:

- Secretos o credenciales en el código (API keys, tokens, contraseñas, llaves privadas)
- Inyección SQL, de comandos o de rutas
- Input del usuario sin validar o sin escapar (XSS, deserialización insegura)
- Autenticación o autorización rota o eliminada
- Datos sensibles en logs o mensajes de error
- Dependencias nuevas o sospechosas

Reglas:

- No inventes. Si no estás seguro, márcalo como "Posible" y explica qué falta para confirmarlo.
- No comentes estilo ni rendimiento.
- Por cada hallazgo: archivo:línea, severidad (CRÍTICA/ALTA/MEDIA/BAJA), por qué es un problema y cómo arreglarlo.
- Si no hay hallazgos, dilo en una línea.

La última línea de tu respuesta debe ser exactamente una de estas:
VERDICT: BLOCK (si hay al menos un hallazgo CRÍTICO o ALTO)
VERDICT: PASS (en cualquier otro caso)

The prompt is in Spanish because that is how I recorded the video; write yours in whatever language you review in. In short, it tells the agent to review only the diff, to look for secrets, injection, unvalidated input, broken auth, sensitive data in logs and suspicious dependencies, and to skip style and performance.

Three design decisions. I tell it not to make things up: if it is unsure, it says “Posible” instead of presenting a finding. I require file, line, severity and a fix, so the report is actionable. And the last line is always VERDICT: BLOCK or VERDICT: PASS. That line is what the Git script reads, so it has to be predictable.

Also notice tools: Read, Grep, Glob, Bash. I come back to that Bash below, because it is the part I like least in my own file.

Step 2: test it by hand

Before automating anything, I tested it by hand. The demo repo had no Git yet, so first git init, then git add, then I opened Claude Code and told it to run the agent on what is staged.

This is what happened in the recording: Claude detected the changes, launched a subagent (the security-reviewer) and concluded the commit should be blocked. It explained why: there was a Stripe key in the code, and although mine said FAKE, it had the format of a real key. It also flagged the SQL injection, showed how to fix it with a parameterized query, and suggested keeping the secret in an environment variable.

If it does not find what you expect here, this is the moment to tune the instructions, not after wiring it into Git.

Step 3: the pre-commit hook

Git has hooks: scripts that run at specific moments. pre-commit runs right before the commit is created, and if it exits with an error, no commit is made. This is the full script, at .git/hooks/pre-commit:

#!/usr/bin/env bash
# Revisión de seguridad con Claude Code antes de cada commit

DIFF=$(git diff --cached --unified=5)
[ -z "$DIFF" ] && exit 0

REPORT=$(printf '%s' "$DIFF" | claude -p \
  "Revisa este diff en staged según tus instrucciones." \
  --agent security-reviewer \
  --allowedTools "Read,Grep,Glob")

echo "$REPORT"

if printf '%s' "$REPORT" | tail -n 1 | grep -q "VERDICT: BLOCK"; then
  echo "❌ Commit bloqueado por security-reviewer. Revisa el reporte."
  exit 1
fi

It does four things:

  1. It takes the staged diff with git diff --cached. If it is empty, it exits successfully and spends nothing.
  2. It opens Claude headless (claude -p), pipes the diff in over stdin and asks it to use the security-reviewer agent. With --allowedTools "Read,Grep,Glob" I state which tools it should use.
  3. It prints the whole report so you can read it.
  4. It checks the last line. If it contains VERDICT: BLOCK, it exits with exit 1 and Git cancels the commit.
PASS BLOCK git commit pre-commit hook security-reviewer agent VERDICT commit created commit cancelled

Install it

Both files are in the video repo. Copy reviewer.md into .claude/agents/ in your project, and the script to .git/hooks/pre-commit. Then make it executable:

chmod +x .git/hooks/pre-commit

Remember that .git/hooks/ is not versioned: everyone on the team has to install it in their own clone.

What happened in the test

I made a change and committed again. The hook took a while, returned the report, and the commit was blocked because of the credentials in the code. I followed the report’s suggestions: the key moved to an environment variable and the query to parameters.

import os
import sqlite3

STRIPE_KEY = os.environ["STRIPE_KEY"]

def get_user(username):
    conn = sqlite3.connect("app.db")
    return conn.execute("SELECT * FROM users WHERE name = ?", (username,))

I staged and committed again, and this time it went through. That is what I wanted to check: the kind of commit Git used to accept without complaint now gets stopped, and the fix lets it pass.

Where it breaks

This is not infallible, and I would rather say so myself.

It is a language model. It can give false positives and, worse, miss something. The report on the same diff can change between runs; I did not measure it, but with a model I would not rule it out. For secrets, put a deterministic tool like gitleaks next to it, one that matches patterns and does not get tired. The agent is the second layer, the one that understands context.

git commit --no-verify skips the hook. It is a personal aid, not a mandatory control. If you need to enforce it, it also has to live in CI.

Your diff leaves your machine. The staged code is sent to the model API. I noticed it in the recording itself: the hook runs Claude headless and the code goes out. If you work with code that cannot leave your environment, check the data policy before turning it on.

It takes time and costs money. In the test the wait was noticeable. I did not measure seconds or cost, so I will not give you numbers. Large commits take longer. Small commits help, and not only for this.

The agent has Bash and the hook does not allow it. My published reviewer.md lists Bash in tools, while the hook passes --allowedTools "Read,Grep,Glob". The hook’s intent is that the reviewer only reads. But the agent file declares more than it should, and I did not verify in the recording what exactly happens if the model tries to use Bash in -p mode. For a read-only reviewer, Bash is surplus. My recommendation is to remove it from the frontmatter: an agent that only analyzes code should not be able to run commands, and that guarantee should not depend on a flag in another file.

Two details of the script. If claude fails or there is no network, REPORT is empty, there is no VERDICT: BLOCK, and the commit goes through. And since the hook only looks at the last line, if the model adds text after the verdict, the block does not trigger. If that worries you, change tail -n 1 to grep -q "^VERDICT: BLOCK" over the whole report.

What I take from it

A Markdown file with clear instructions and a short hook, and you have a second pair of eyes on every commit. It does not replace a real security review or gitleaks, but it catches exactly what slips by late at night. And do not hand everything to the AI: validate what you read and understand the fix, because an error you do not understand today is one you will not be able to fix tomorrow.

I built it to check secrets and SQL injection, but you can adapt it to your own rules. Try it in your repo and tell me in the video comments what you added to it and what it caught first.