Code Review Agent Blueprint

Complete code review agent that reads file trees, runs linters, checks patterns, and suggests refactors. Ready-to-run with file system access and Git integration.

June 9, 2026
code-reviewagent-blueprintlinterrefactoringgit

Code Review Agent

An AI agent that reviews code like a senior engineer. It reads the file tree, understands context across files, runs linters, checks for anti-patterns, and suggests concrete refactors with before/after code.

Note:

Run this agent on PRs, pre-commit hooks, or as a standalone code quality tool. It reads local files — no API key needed for the file system tools, only for the LLM.

Agent File Structure

code-review-agentadd
agent.pyadd
tools.pyadd
linters.pyadd
config.jsonadd

Setup

1

Install Dependencies

Install the OpenAI client plus linter tools the agent will invoke.

pip install openai ruff pylint
2

Create config.json

Configure the agent. project_root tells the agent which directory to review. ignore_patterns skips generated files and dependencies.

{
  "openai_api_key": "sk-...",
  "model": "gpt-4o",
  "max_iterations": 8,
  "project_root": "./my-project",
  "ignore_patterns": ["node_modules", ".git", "__pycache__", "*.pyc", ".next", "dist", "build", "*.min.js"],
  "max_file_size_kb": 200
}

Note:

The agent reads files from your local filesystem. Only point it at code you trust the agent to access. Don't run on production servers.

3

Verify

Review a single file to test the setup.

python agent.py --path "./src/utils.py"

The agent should output findings with file locations and severity levels.

System Prompt

You are a senior code reviewer. Review code for bugs, anti-patterns, performance
issues, security vulnerabilities, and style violations. Follow this protocol:

1. THOUGHT: What aspects of the code need review?
2. ACTION: Read files, run linters, check Git diffs
3. Categorize findings as BUG, SECURITY, PERF, STYLE, or REFACTOR
4. For each finding, provide: file path, line range, severity (critical/high/medium/low),
   description, current code (snippet), suggested fix (snippet)
5. FINAL_REVIEW: Summarize all findings grouped by category

Rules:
- Flag real issues, not nitpicks. Prefer fewer high-signal findings over many low-value ones.
- If you identify a pattern (e.g., same issue in 5 files), flag it once and note the pattern.
- Suggest specific fixes with code, not vague advice.
- Acknowledge code that is well-written. Don't fabricate issues.
- For security findings, explain the attack vector, not just "this is unsafe."

Tool Definitions

Agent Tools

read_file
Read the full contents of a file. Respects max_file_size_kb from config — returns a warning for files exceeding the limit.

Values: path: string

list_directory
List all files and subdirectories in a directory, excluding ignore_patterns. Returns relative paths.

Values: path: string, recursive?: bool

run_linter
Run ruff (Python) or ESLint (JavaScript) on a file or directory. Returns formatted output with error codes and locations.

Values: path: string, linter?: 'ruff' | 'eslint'

git_diff
Show staged and unstaged changes as a unified diff. Used for PR review context.

Values: path?: string (optional, default: project_root)

search_pattern
Search for code patterns across the project using regex. Returns file paths and matching lines.

Values: pattern: string, file_pattern?: string (default *.py)

Tool Implementation

# tools.py
import os
import re
import subprocess
import json

PROJECT_ROOT = None
IGNORE_PATTERNS = []
MAX_FILE_SIZE_KB = 200

def _should_ignore(path):
    for pattern in IGNORE_PATTERNS:
        if pattern.endswith("/"):
            if path.startswith(pattern):
                return True
        elif path.endswith(pattern.lstrip("*")) or pattern in path:
            return True
    return False

def read_file(path):
    full = os.path.join(PROJECT_ROOT, path)
    if not os.path.exists(full):
        return f"ERROR: File not found: {path}"
    size_kb = os.path.getsize(full) / 1024
    if size_kb > MAX_FILE_SIZE_KB:
        return f"WARNING: File is {size_kb:.0f}KB, exceeds {MAX_FILE_SIZE_KB}KB limit. First 200 lines:"
    with open(full, "r") as f:
        lines = f.readlines()
    numbered = "".join(f"{i+1:4d}| {l}" for i, l in enumerate(lines[:300]))
    return numbered

def list_directory(path, recursive=False):
    full = os.path.join(PROJECT_ROOT, path)
    if not os.path.exists(full):
        return f"ERROR: Directory not found: {path}"
    items = []
    for root, dirs, files in os.walk(full):
        dirs[:] = [d for d in dirs if not _should_ignore(os.path.join(root, d))]
        for f in files:
            fpath = os.path.join(root, f)
            if _should_ignore(fpath):
                continue
            rel = os.path.relpath(fpath, PROJECT_ROOT)
            size_kb = os.path.getsize(fpath) / 1024
            items.append(f"{rel} ({size_kb:.0f}KB)")
        if not recursive:
            break
    return "\n".join(items) if items else f"Directory {path} is empty"

def run_linter(path, linter="ruff"):
    full = os.path.join(PROJECT_ROOT, path)
    cmd = ["ruff", "check", full, "--output-format=text"] if linter == "ruff" else ["npx", "eslint", full]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        return result.stdout or "No issues found."
    except FileNotFoundError:
        return f"Linter '{linter}' not installed. Run: pip install ruff"

def git_diff(path=None):
    import subprocess
    cmd = ["git", "diff", "--unified=3"]
    if path:
        cmd.append(path)
    result = subprocess.run(cmd, cwd=PROJECT_ROOT, capture_output=True, text=True)
    return result.stdout[:5000] if result.stdout else "No changes detected."

def search_pattern(pattern, file_pattern="*.py"):
    matches = []
    import fnmatch
    for root, dirs, files in os.walk(PROJECT_ROOT):
        dirs[:] = [d for d in dirs if not _should_ignore(d)]
        for f in files:
            if not fnmatch.fnmatch(f, file_pattern):
                continue
            full = os.path.join(root, f)
            if _should_ignore(full):
                continue
            rel = os.path.relpath(full, PROJECT_ROOT)
            with open(full, "r") as fh:
                for i, line in enumerate(fh, 1):
                    if re.search(pattern, line):
                        matches.append(f"{rel}:{i}: {line.rstrip()}")
            if len(matches) > 50:
                matches.append("... (truncated, showing first 50 matches)")
                break
    return "\n".join(matches) if matches else f"No matches for pattern: {pattern}"

Agent Initialization

# agent.py
import json
import argparse
import importlib
from openai import OpenAI
import tools as agent_tools

TOOL_SCHEMAS = [
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read the full contents of a file",
            "parameters": {
                "type": "object",
                "properties": {"path": {"type": "string"}},
                "required": ["path"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "list_directory",
            "description": "List files and subdirectories in a directory",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "recursive": {"type": "boolean", "default": False}
                },
                "required": ["path"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "run_linter",
            "description": "Run a linter (ruff or eslint) on a file or directory",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "linter": {"type": "string", "enum": ["ruff", "eslint"]}
                },
                "required": ["path"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "git_diff",
            "description": "Show staged and unstaged changes as a unified diff",
            "parameters": {
                "type": "object",
                "properties": {"path": {"type": "string"}},
                "required": []
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_pattern",
            "description": "Search for regex patterns across project files",
            "parameters": {
                "type": "object",
                "properties": {
                    "pattern": {"type": "string"},
                    "file_pattern": {"type": "string", "default": "*.py"}
                },
                "required": ["pattern"]
            }
        }
    }
]

SYSTEM_PROMPT = """You are a senior code reviewer. Review code for bugs, anti-patterns,
performance issues, security vulnerabilities, and style violations. Follow this protocol:

1. THOUGHT: What aspects of the code need review?
2. ACTION: Read files, run linters, check Git diffs
3. Categorize findings as BUG, SECURITY, PERF, STYLE, or REFACTOR
4. For each finding, provide: file path, line range, severity (critical/high/medium/low),
   description, current code (snippet), suggested fix (snippet)
5. FINAL_REVIEW: Summarize all findings grouped by category

Rules:
- Flag real issues, not nitpicks. Prefer fewer high-signal findings over many low-value ones.
- If you identify a pattern, flag it once and note the pattern.
- Suggest specific fixes with code, not vague advice.
- Acknowledge code that is well-written. Don't fabricate issues.
- For security findings, explain the attack vector, not just 'this is unsafe.'"""

def run_agent(path, config):
    client = OpenAI(api_key=config["openai_api_key"])
    agent_tools.PROJECT_ROOT = config["project_root"]
    agent_tools.IGNORE_PATTERNS = config.get("ignore_patterns", [])
    agent_tools.MAX_FILE_SIZE_KB = config.get("max_file_size_kb", 200)

    query = f"Review this code path: {path}. Read all relevant files, run linters, check for bugs, performance issues, and security vulnerabilities. Check git diff for context if available."

    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": query}
    ]

    for i in range(config.get("max_iterations", 8)):
        response = client.chat.completions.create(
            model=config.get("model", "gpt-4o"),
            messages=messages,
            tools=TOOL_SCHEMAS,
            temperature=0.2
        )

        msg = response.choices[0].message
        messages.append(msg)

        if msg.content and "FINAL_REVIEW:" in msg.content:
            return msg.content.split("FINAL_REVIEW:", 1)[1].strip()

        if not msg.tool_calls:
            messages.append({
                "role": "user",
                "content": "Continue your review. Use tools to read files and run linters, then provide FINAL_REVIEW."
            })
            continue

        for tool_call in msg.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)
            func = getattr(agent_tools, func_name)
            result = func(**func_args)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })

    return "Agent reached max iterations."

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--path", required=True, help="File or directory to review")
    parser.add_argument("--config", default="config.json")
    args = parser.parse_args()

    with open(args.config) as f:
        config = json.load(f)

    review = run_agent(args.path, config)
    print(review)

Walkthrough

Reviewing src/auth.py — a file with SQL injection risk and a missing rate limiter.

1

Agent reads the file tree

The agent calls list_directory("src", recursive=True) to understand the project structure. It sees auth.py, database.py, middleware.py, routes.py.

2

Reads the target file

read_file("src/auth.py") returns the full file. The agent spots:

  • f-string in SQL query (no parameterization)
  • Password hashing but no salt
  • No rate limiting on login endpoint
3

Checks related files

The agent calls read_file("src/middleware.py") to see if rate limiting is handled elsewhere. It's not. Calls search_pattern("f\".*INSERT|f\".*SELECT|f\".*DELETE") to find all raw SQL queries.

4

Runs linter

run_linter("src/auth.py", "ruff") confirms: no type annotations on login(), unused import, bare except clause.

5

Delivers FINAL_REVIEW

FINAL_REVIEW:

[CRITICAL] SECURITY — src/auth.py:42-46
SQL injection via f-string in login query. User input is concatenated directly.
Fix: Use parameterized queries.

[HIGH] SECURITY — src/auth.py:52
Password hashed without salt. Rainbow table attack vector.
Fix: Use bcrypt with auto-generated salt.

[MEDIUM] PERF — src/auth.py:18
No rate limiting on login endpoint. Brute-force attack possible.
Fix: Add rate limiter middleware or use a library like slowapi.

[LOW] STYLE — src/auth.py:1, 14, 29
Missing type annotations on public functions.

Customization

Linter Configuration

ruff config
Add pyproject.toml [tool.ruff] section to customize linter rules. The agent runs ruff with default rules unless configured.

Values: path to pyproject.toml or ruff.toml

eslint config
For JavaScript/TypeScript projects, the agent can run ESLint. Requires .eslintrc or eslint.config.js in the project.

Values: path to eslint config

file_pattern
Files matching this pattern are included in the review. Use to focus on specific file types.

Values: *.py, *.js, *.ts, *.tsx

Note:

Large file handling. Files over 200KB are truncated to the first 300 lines. The agent will note this in its review. Increase max_file_size_kb if you need full-file review, but be aware this uses more tokens.

Key Takeaway

A code review agent is most useful when it catches issues humans miss: SQL injection in rarely-touched files, missing error handling in edge cases, and pattern-level bugs across multiple files. Don't use it for style nitpicks — linters already do that. Focus the agent's system prompt on high-signal findings.