Systematic Bug Hunting with Claude Code
Audit a polyglot monorepo for security, performance, and reliability bugs.
Introduction
30 Second Summary
Production codebases hide bugs that pass every test and slip through code review. Finding them before they cause outages requires systematic thinking, not just reading line by line.
In this project, you will set up a practice monorepo with intentional production bugs seeded across security, performance, reliability, and tooling. Then you'll use Claude Code to systematically audit, diagnose, and fix each one with surgical precision.
What You'll Build
A complete audit workflow on a polyglot monorepo containing seeded bugs across security, performance, reliability, and tooling categories, orchestrated by moonrepo and diagnosed using Claude Code's plan mode.
By the end of this project, you'll have:
- A moonrepo-orchestrated monorepo with a Python gRPC backend and React TypeScript frontend, containing 5 seeded production bugs.
- 5 surgical bug fixes committed atomically with clear root-cause justifications that keep all existing tests passing.
- A FINDINGS.md audit report documenting each finding with severity, root cause, fix rationale, and systems-thinking commentary.
- Secret Mission: Use Claude Code's plan mode to find and fix a hidden 6th bug not documented in the original audit scope.
Are there any prerequisites?
This project assumes you already have Python 3.12+, Node.js v22+, pnpm, moon, Git, Claude Code, and the GitHub CLI installed. You also need an active Claude Pro or Max subscription.
Scaffold the Moonrepo Workspace
A real take-home assessment drops you into an unfamiliar polyglot monorepo and expects you to orient fast. This step builds the workspace you'll audit later.
You'll use moonrepo to orchestrate a Python gRPC backend and a React TypeScript frontend inside a single repository. By the end of this step, both projects will have their dependencies installed and their task runners configured.
In this step, get ready to:
- Create the monorepo directory structure with moonrepo initialization.
- Configure moonrepo to orchestrate both projects with per-project task definitions.
- Initialize Python and TypeScript environments with all dependencies installed.
Create the monorepo structure
Every monorepo starts with a root directory, a version control system, and an orchestrator. You'll create all three in a few commands.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder, initialize Git, and scaffold moonrepo by running these commands:
mkdir audit-drill && cd audit-drill
git init
moon init --minimal
What do these commands do?
- mkdir audit-drill && cd audit-drill creates the project folder and moves into it.
- git init initializes a Git repository so moonrepo can track file changes.
- moon init --minimal scaffolds a .moon folder with a minimal workspace.yml config and updates .gitignore.
- Create the project directories by running this command:
mkdir -p apps/server/src apps/server/tests apps/client/src
- Confirm the directories exist by running:
ls apps/server apps/client
You should see src and tests listed under apps/server, and src listed under apps/client.
Command not found for moon?
Make sure moon is installed globally and available on your PATH. If you installed it recently, try opening a fresh terminal window so your shell picks up the new binary.
Help me troubleshoot why moon isn't found on my PATH.
Configure the workspace
Moonrepo needs to know where your projects live and which toolchains to use. You'll configure the workspace-level settings, then define per-project tasks.
- Open .moon/workspace.yml in your editor and replace its contents with the following:
projects:
server: 'apps/server'
client: 'apps/client'
- Create a new file .moon/toolchains.yml and add this content:
javascript:
packageManager: 'pnpm'
node: {}
pnpm: {}
What does this configuration do?
The workspace.yml maps each project name to its folder path. This is how moonrepo discovers projects and builds its dependency graph.
The toolchains.yml tells moonrepo to manage Node.js with pnpm as the package manager. Python remains on the system toolchain since moonrepo's Python support is still unstable.
- Create apps/server/moon.yml with the server's task configuration:
language: 'python'
layer: 'application'
tasks:
test:
command: 'python -m pytest tests/ -v'
inputs:
- 'src/**/*'
- 'tests/**/*'
toolchain: 'system'
What does toolchain system mean?
Setting toolchain: 'system' tells moonrepo to use whatever python binary is already on your PATH instead of trying to manage a Python installation itself.
- Create apps/client/moon.yml with the client's task configuration:
language: 'typescript'
layer: 'application'
dependsOn:
- 'server'
tasks:
test:
command: 'pnpm vitest run'
inputs:
- 'src/**/*'
deps:
- '~:build'
build:
command: 'pnpm tsc --noEmit'
inputs:
- 'src/**/*'
- 'tsconfig.json'
What does this client config do?
The dependsOn: ['server'] declaration means the client project depends on the server project at the workspace level.
The deps: ['~:build'] on the test task means "run my own build task first before running tests." The ~: prefix is moonrepo's syntax for referencing a task in the same project.
- Save all four files.
- Confirm the config files exist by running:
ls .moon/workspace.yml .moon/toolchains.yml apps/server/moon.yml apps/client/moon.yml
You should see all four file paths listed with no errors.
File not found?
Double-check you created toolchains.yml (plural) inside the .moon folder, not at the project root. Also confirm the server and client moon.yml files are directly inside apps/server/ and apps/client/, not nested in src/.
Help me find where my moon config files should go.
✔️ Awesome, I've got everything!
Great. Make sure all four files are saved before continuing.
ⓧ I'd like to double check the full code
projects:
server: 'apps/server'
client: 'apps/client'
javascript:
packageManager: 'pnpm'
node: {}
pnpm: {}
language: 'python'
layer: 'application'
tasks:
test:
command: 'python -m pytest tests/ -v'
inputs:
- 'src/**/*'
- 'tests/**/*'
toolchain: 'system'
language: 'typescript'
layer: 'application'
dependsOn:
- 'server'
tasks:
test:
command: 'pnpm vitest run'
inputs:
- 'src/**/*'
deps:
- '~:build'
build:
command: 'pnpm tsc --noEmit'
inputs:
- 'src/**/*'
- 'tsconfig.json'
Initialize both projects
With the orchestration layer in place, each project needs its own language environment and dependencies.
- Set up the Python virtual environment for the backend by running these commands from the workspace root:
cd apps/server
python3 -m venv .venv
source .venv/bin/activate
Why a virtual environment?
A virtual environment isolates this project's Python packages from your system Python. It ensures the exact versions you install here don't conflict with packages in other projects on your machine.
- Create apps/server/requirements.txt with the backend dependencies:
grpcio==1.71.0
grpcio-tools==1.71.0
sqlalchemy==2.0.40
pytest==8.3.5
What are these packages?
- grpcio and grpcio-tools provide the gRPC framework for inter-service communication.
- sqlalchemy is the Python ORM for database access.
- pytest is the test runner.
- Install the dependencies by running:
pip install -r requirements.txt
You should see each package download and install successfully, ending with a "Successfully installed" summary line.
Pip install failing?
Make sure your virtual environment is active. You should see (.venv) at the beginning of your terminal prompt. If not, run source .venv/bin/activate again.
Help me fix my pip install errors.
- Navigate back to the workspace root and then into the client directory:
cd ../../apps/client
- Create apps/client/package.json with the frontend configuration:
{
"name": "audit-drill-client",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run",
"build": "tsc --noEmit"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@testing-library/react": "^16.3.0",
"@types/react": "^19.1.0",
"jsdom": "^26.1.0",
"typescript": "^5.8.0",
"vitest": "^3.1.0"
}
}
What does this package.json define?
The scripts section defines the commands moonrepo will call. The dependencies include React for the UI component. The devDependencies include TypeScript for type checking, Vitest for testing, and jsdom for simulating a browser environment in tests.
- Create apps/client/tsconfig.json for TypeScript compilation settings:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src"]
}
- Create apps/client/vitest.config.ts for the test runner configuration:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
},
});
Why jsdom?
React components run in a browser, but your tests run in Node.js. The jsdom environment simulates a browser's DOM so Vitest can render and test React components without opening an actual browser window.
- Save all three files, then install the frontend dependencies by running:
pnpm install
You should see pnpm download and link all packages, ending with a summary showing the number of packages added.
Pnpm install failing?
Make sure you're inside the apps/client directory (run pwd to check). Also confirm that package.json is valid JSON with no trailing commas or syntax errors.
Help me troubleshoot my pnpm install error.
- Navigate back to the workspace root:
cd ../..
✔️ Awesome, I've got everything!
Great. You should now be back in the audit-drill root directory with both projects fully initialized.
ⓧ I'd like to double check the full code
grpcio==1.71.0
grpcio-tools==1.71.0
sqlalchemy==2.0.40
pytest==8.3.5
{
"name": "audit-drill-client",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run",
"build": "tsc --noEmit"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@testing-library/react": "^16.3.0",
"@types/react": "^19.1.0",
"jsdom": "^26.1.0",
"typescript": "^5.8.0",
"vitest": "^3.1.0"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src"]
}
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
},
});
Your workspace is fully scaffolded. Both projects have their dependencies installed and task runners configured. Next up, you'll add the actual application code that contains the hidden bugs.
Add the Buggy Codebase
Your workspace structure is ready. Now you'll populate it with application code that looks production-quality on the surface.
The backend is a Python gRPC service with SQLAlchemy models. The frontend is a React TypeScript dashboard. Both contain subtle production bugs that pass their test suites but would cause real problems at scale.
In this step, get ready to:
- Add the Python backend models, service, and tests.
- Add the React frontend component and tests.
- Create the audit constraints file that will guide Claude Code.
Add the backend code
The backend has two source files: a models module defining the database schema, and a service module implementing the gRPC task management logic. Both files contain bugs marked with comments so you can verify Claude Code's findings later.
- Create the empty __init__.py files that make Python treat these directories as packages, by running:
touch apps/server/src/__init__.py apps/server/tests/__init__.py
- Confirm both files exist by running:
ls apps/server/src/__init__.py apps/server/tests/__init__.py
You should see both file paths echoed back.
- Create apps/server/src/models.py with the following content:
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
email = Column(String(255), unique=True, nullable=False)
tasks = relationship("Task", back_populates="owner")
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
status = Column(String(50), default="pending")
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
owner = relationship("User", back_populates="tasks")
def get_engine(db_url="sqlite:///tasks.db"):
return create_engine(db_url)
def get_session(engine):
Session = sessionmaker(bind=engine)
return Session()
What does this code do?
This module defines two SQLAlchemy ORM models that map to database tables.
- User stores user info (name, email) and has a one-to-many relationship with tasks.
- Task stores a title, status, and a foreign key linking it to its owner.
- get_engine() and get_session() are helpers that create database connections.
- Save models.py.
- Create apps/server/src/service.py with the following content:
import grpc
from concurrent import futures
from sqlalchemy import text
from sqlalchemy.orm import Session
from .models import Base, Task, User, get_engine, get_session
_engine = get_engine("sqlite:///tasks.db")
Base.metadata.create_all(_engine)
class TaskService:
"""gRPC service implementation for task management."""
def __init__(self, session: Session = None):
self.session = session or get_session(_engine)
def create_task(self, title: str, owner_id: int) -> dict:
"""Create a new task assigned to an owner."""
task = Task(title=title, status="pending", owner_id=owner_id)
self.session.add(task)
self.session.commit()
self.session.refresh(task)
return {"id": task.id, "title": task.title, "status": task.status}
def get_task(self, task_id: int) -> dict | None:
"""Get a single task by ID."""
task = self.session.query(Task).filter(Task.id == task_id).first()
if task is None:
return None
return {
"id": task.id,
"title": task.title,
"status": task.status,
"owner": task.owner.name,
}
def list_tasks(self, status_filter: str = None) -> list[dict]:
"""List tasks, optionally filtered by status."""
if status_filter:
# BUG 1 (Security): SQL injection via string interpolation in raw SQL
results = self.session.execute(
text(f"SELECT * FROM tasks WHERE status = '{status_filter}'")
)
tasks = [
self.session.query(Task).get(row.id) for row in results
]
else:
tasks = self.session.query(Task).all()
# BUG 2 (Performance): N+1 query - each task.owner triggers a lazy load
output = []
for task in tasks:
output.append({
"id": task.id,
"title": task.title,
"status": task.status,
"owner": task.owner.name,
})
return output
def update_task_status(self, task_id: int, new_status: str) -> dict | None:
"""Update task status. Called by other internal services via gRPC."""
# BUG 3 (Reliability): No timeout/deadline handling.
task = self.session.query(Task).filter(Task.id == task_id).first()
if task is None:
return None
task.status = new_status
self.session.commit()
return {"id": task.id, "title": task.title, "status": task.status}
def serve():
"""Start the gRPC server."""
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
What does this code do?
This is the gRPC service layer that handles task CRUD operations.
- create_task() inserts a new task into the database.
- get_task() fetches a single task by ID.
- list_tasks() returns all tasks with optional status filtering.
- update_task_status() changes a task's status (meant to be called via gRPC by other services).
Notice the BUG comments. These mark the seeded issues you'll diagnose during the audit. The code looks correct at a glance, and the tests pass, but each marked section has a production-grade flaw.
- Save service.py.
- Create apps/server/tests/test_service.py with the following content:
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.models import Base, User, Task
from src.service import TaskService
@pytest.fixture
def db_session():
"""Create an in-memory SQLite database for testing."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
# Seed test data
user = User(name="Alice", email="alice@example.com")
session.add(user)
session.commit()
yield session
session.close()
@pytest.fixture
def service(db_session):
return TaskService(session=db_session)
def test_create_task(service, db_session):
user = db_session.query(User).first()
result = service.create_task("Write tests", user.id)
assert result["title"] == "Write tests"
assert result["status"] == "pending"
def test_get_task(service, db_session):
user = db_session.query(User).first()
created = service.create_task("Review PR", user.id)
result = service.get_task(created["id"])
assert result["title"] == "Review PR"
assert result["owner"] == "Alice"
def test_list_tasks_no_filter(service, db_session):
user = db_session.query(User).first()
service.create_task("Task 1", user.id)
service.create_task("Task 2", user.id)
results = service.list_tasks()
assert len(results) == 2
def test_update_task_status(service, db_session):
user = db_session.query(User).first()
created = service.create_task("Deploy", user.id)
result = service.update_task_status(created["id"], "completed")
assert result["status"] == "completed"
Why do these tests pass if the code has bugs?
The tests exercise the happy path only. They never pass a malicious string to list_tasks(), they use in-memory SQLite with 1-2 records (hiding the N+1 performance issue), and they never simulate a slow database or component unmounting.
This is exactly what makes these bugs dangerous in production. Code review and CI both show green, but the issues surface under real load or adversarial input.
- Save test_service.py.
File not saving to the right location?
Make sure models.py and service.py are inside apps/server/src/, and test_service.py is inside apps/server/tests/. The imports in the test file use from src.models which relies on this exact directory structure.
Help me figure out why my Python imports aren't resolving correctly.
Add the frontend code
The frontend is a single React component that displays tasks from the backend and streams live updates over a WebSocket. It also contains seeded bugs that the test suite doesn't catch.
- Create apps/client/src/TaskDashboard.tsx with the following content:
import { useState, useEffect, useCallback } from 'react';
interface Task {
id: number;
title: string;
status: string;
owner: string;
}
export function TaskDashboard() {
const [tasks, setTasks] = useState<Task[]>([]);
const [filter, setFilter] = useState<string>('all');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchTasks();
// BUG 4 (Reliability): WebSocket opened but never closed on unmount
const ws = new WebSocket('ws://localhost:50052/tasks/stream');
ws.onmessage = (event) => {
const updatedTask = JSON.parse(event.data) as Task;
setTasks((prev) =>
prev.map((t) => (t.id === updatedTask.id ? updatedTask : t))
);
};
// Missing: return () => ws.close();
}, []);
const fetchTasks = async () => {
setLoading(true);
try {
const response = await fetch('/api/tasks');
const data = await response.json();
setTasks(data);
} catch (error) {
console.error('Failed to fetch tasks:', error);
} finally {
setLoading(false);
}
};
// BUG 6 (Secret Mission): Stale closure - empty deps array but uses `tasks` directly
const handleStatusChange = useCallback((taskId: number, newStatus: string) => {
const task = tasks.find((t) => t.id === taskId);
if (!task) return;
fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus }),
}).then(() => {
setTasks(tasks.map((t) =>
t.id === taskId ? { ...t, status: newStatus } : t
));
});
}, []);
const filteredTasks = filter === 'all'
? tasks
: tasks.filter((t) => t.status === filter);
if (loading) return <div>Loading tasks...</div>;
return (
<div className="task-dashboard">
<h1>Task Dashboard</h1>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
</select>
<ul>
{filteredTasks.map((task) => (
<li key={task.id}>
<span>{task.title}</span>
<span className="status">{task.status}</span>
<span className="owner">{task.owner}</span>
<button onClick={() => handleStatusChange(task.id, 'completed')}>
Complete
</button>
</li>
))}
</ul>
</div>
);
}
What does this component do?
- Fetches tasks from /api/tasks on mount and displays them in a filterable list.
- Opens a WebSocket to stream real-time task updates from the server.
- handleStatusChange() sends a PATCH request to update a task's status and updates local state.
BUG 4 is a reliability issue (resource leak). BUG 6 is hidden in the Secret Mission. You'll hunt for both during the audit.
- Save TaskDashboard.tsx.
- Create apps/client/src/TaskDashboard.test.tsx with the following content:
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { TaskDashboard } from './TaskDashboard';
vi.stubGlobal('WebSocket', vi.fn(() => ({
onmessage: null,
close: vi.fn(),
})));
const mockTasks = [
{ id: 1, title: 'Write tests', status: 'pending', owner: 'Alice' },
{ id: 2, title: 'Review PR', status: 'completed', owner: 'Bob' },
];
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockTasks),
})
));
});
describe('TaskDashboard', () => {
it('renders loading state initially', () => {
render(<TaskDashboard />);
expect(screen.getByText('Loading tasks...')).toBeDefined();
});
it('renders tasks after loading', async () => {
render(<TaskDashboard />);
await waitFor(() => {
expect(screen.getByText('Write tests')).toBeDefined();
expect(screen.getByText('Review PR')).toBeDefined();
});
});
it('renders filter dropdown', async () => {
render(<TaskDashboard />);
await waitFor(() => {
expect(screen.getByDisplayValue('All')).toBeDefined();
});
});
});
What do these tests cover?
The tests mock both fetch and WebSocket globally, then verify basic rendering: loading state, task display, and the filter dropdown. They never test unmount cleanup or rapid state changes, which is why BUG 4 and BUG 6 go undetected.
- Save TaskDashboard.test.tsx.
TypeScript errors in your editor?
If your editor shows red squiggles under the React imports or JSX, make sure you ran pnpm install inside apps/client/ in the previous step. The type definitions need to be in node_modules for the editor to resolve them.
Help me fix TypeScript errors in my React component file.
Create the audit constraints file
Before you start auditing, you need a CLAUDE.md file at the repo root. Claude Code reads this file at the start of every session to understand the project's rules and constraints.
- Create CLAUDE.md in the audit-drill root directory with the following content:
# Audit Constraints
## Rules for this codebase audit
- When fixing bugs, only change the line(s) that contain the bug.
- Do not refactor surrounding code, rename variables, or add features.
- Do not add error handling beyond what is needed for the specific bug.
- Every fix must be committed atomically (one bug = one commit).
- All existing tests must pass after every single commit.
- Commit messages must explain: what the bug is, why it matters, and what you changed.
## Project structure
- `apps/server/` — Python gRPC backend (SQLAlchemy ORM)
- `apps/client/` — TypeScript React frontend
- Orchestrated by moonrepo (`.moon/` config)
## How to run tests
- Backend: `cd apps/server && python -m pytest tests/ -v`
- Frontend: `cd apps/client && pnpm vitest run`
Why set constraints in CLAUDE.md?
Without explicit constraints, Claude Code tends to refactor aggressively. It might rename variables for clarity, add extra error handling, or reorganize imports. In an audit assessment, this is a red flag. Reviewers want to see that you can make the minimum change needed to fix the bug without disturbing surrounding code.
The CLAUDE.md file enforces this discipline from the start of every session.
- Save CLAUDE.md.
- Confirm your file structure looks correct by running:
ls CLAUDE.md apps/server/src/models.py apps/server/src/service.py apps/server/tests/test_service.py apps/client/src/TaskDashboard.tsx apps/client/src/TaskDashboard.test.tsx
You should see all six file paths echoed back.
Missing a file?
If any path shows an error, check that you created the file in the correct subdirectory. CLAUDE.md goes in the repo root (audit-drill/), not inside apps/.
Help me find where my files ended up.
✔️ Awesome, I've got everything!
Great. Double check you've saved all your files before moving on.
ⓧ I'd like to double check the full code
Here is each file for comparison.
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
email = Column(String(255), unique=True, nullable=False)
tasks = relationship("Task", back_populates="owner")
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
status = Column(String(50), default="pending")
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
owner = relationship("User", back_populates="tasks")
def get_engine(db_url="sqlite:///tasks.db"):
return create_engine(db_url)
def get_session(engine):
Session = sessionmaker(bind=engine)
return Session()
import grpc
from concurrent import futures
from sqlalchemy import text
from sqlalchemy.orm import Session
from .models import Base, Task, User, get_engine, get_session
_engine = get_engine("sqlite:///tasks.db")
Base.metadata.create_all(_engine)
class TaskService:
"""gRPC service implementation for task management."""
def __init__(self, session: Session = None):
self.session = session or get_session(_engine)
def create_task(self, title: str, owner_id: int) -> dict:
"""Create a new task assigned to an owner."""
task = Task(title=title, status="pending", owner_id=owner_id)
self.session.add(task)
self.session.commit()
self.session.refresh(task)
return {"id": task.id, "title": task.title, "status": task.status}
def get_task(self, task_id: int) -> dict | None:
"""Get a single task by ID."""
task = self.session.query(Task).filter(Task.id == task_id).first()
if task is None:
return None
return {
"id": task.id,
"title": task.title,
"status": task.status,
"owner": task.owner.name,
}
def list_tasks(self, status_filter: str = None) -> list[dict]:
"""List tasks, optionally filtered by status."""
if status_filter:
# BUG 1 (Security): SQL injection via string interpolation in raw SQL
results = self.session.execute(
text(f"SELECT * FROM tasks WHERE status = '{status_filter}'")
)
tasks = [
self.session.query(Task).get(row.id) for row in results
]
else:
tasks = self.session.query(Task).all()
# BUG 2 (Performance): N+1 query - each task.owner triggers a lazy load
output = []
for task in tasks:
output.append({
"id": task.id,
"title": task.title,
"status": task.status,
"owner": task.owner.name,
})
return output
def update_task_status(self, task_id: int, new_status: str) -> dict | None:
"""Update task status. Called by other internal services via gRPC."""
# BUG 3 (Reliability): No timeout/deadline handling.
task = self.session.query(Task).filter(Task.id == task_id).first()
if task is None:
return None
task.status = new_status
self.session.commit()
return {"id": task.id, "title": task.title, "status": task.status}
def serve():
"""Start the gRPC server."""
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.models import Base, User, Task
from src.service import TaskService
@pytest.fixture
def db_session():
"""Create an in-memory SQLite database for testing."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
# Seed test data
user = User(name="Alice", email="alice@example.com")
session.add(user)
session.commit()
yield session
session.close()
@pytest.fixture
def service(db_session):
return TaskService(session=db_session)
def test_create_task(service, db_session):
user = db_session.query(User).first()
result = service.create_task("Write tests", user.id)
assert result["title"] == "Write tests"
assert result["status"] == "pending"
def test_get_task(service, db_session):
user = db_session.query(User).first()
created = service.create_task("Review PR", user.id)
result = service.get_task(created["id"])
assert result["title"] == "Review PR"
assert result["owner"] == "Alice"
def test_list_tasks_no_filter(service, db_session):
user = db_session.query(User).first()
service.create_task("Task 1", user.id)
service.create_task("Task 2", user.id)
results = service.list_tasks()
assert len(results) == 2
def test_update_task_status(service, db_session):
user = db_session.query(User).first()
created = service.create_task("Deploy", user.id)
result = service.update_task_status(created["id"], "completed")
assert result["status"] == "completed"
import { useState, useEffect, useCallback } from 'react';
interface Task {
id: number;
title: string;
status: string;
owner: string;
}
export function TaskDashboard() {
const [tasks, setTasks] = useState<Task[]>([]);
const [filter, setFilter] = useState<string>('all');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchTasks();
// BUG 4 (Reliability): WebSocket opened but never closed on unmount
const ws = new WebSocket('ws://localhost:50052/tasks/stream');
ws.onmessage = (event) => {
const updatedTask = JSON.parse(event.data) as Task;
setTasks((prev) =>
prev.map((t) => (t.id === updatedTask.id ? updatedTask : t))
);
};
// Missing: return () => ws.close();
}, []);
const fetchTasks = async () => {
setLoading(true);
try {
const response = await fetch('/api/tasks');
const data = await response.json();
setTasks(data);
} catch (error) {
console.error('Failed to fetch tasks:', error);
} finally {
setLoading(false);
}
};
// BUG 6 (Secret Mission): Stale closure - empty deps array but uses `tasks` directly
const handleStatusChange = useCallback((taskId: number, newStatus: string) => {
const task = tasks.find((t) => t.id === taskId);
if (!task) return;
fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus }),
}).then(() => {
setTasks(tasks.map((t) =>
t.id === taskId ? { ...t, status: newStatus } : t
));
});
}, []);
const filteredTasks = filter === 'all'
? tasks
: tasks.filter((t) => t.status === filter);
if (loading) return <div>Loading tasks...</div>;
return (
<div className="task-dashboard">
<h1>Task Dashboard</h1>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
</select>
<ul>
{filteredTasks.map((task) => (
<li key={task.id}>
<span>{task.title}</span>
<span className="status">{task.status}</span>
<span className="owner">{task.owner}</span>
<button onClick={() => handleStatusChange(task.id, 'completed')}>
Complete
</button>
</li>
))}
</ul>
</div>
);
}
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { TaskDashboard } from './TaskDashboard';
vi.stubGlobal('WebSocket', vi.fn(() => ({
onmessage: null,
close: vi.fn(),
})));
const mockTasks = [
{ id: 1, title: 'Write tests', status: 'pending', owner: 'Alice' },
{ id: 2, title: 'Review PR', status: 'completed', owner: 'Bob' },
];
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockTasks),
})
));
});
describe('TaskDashboard', () => {
it('renders loading state initially', () => {
render(<TaskDashboard />);
expect(screen.getByText('Loading tasks...')).toBeDefined();
});
it('renders tasks after loading', async () => {
render(<TaskDashboard />);
await waitFor(() => {
expect(screen.getByText('Write tests')).toBeDefined();
expect(screen.getByText('Review PR')).toBeDefined();
});
});
it('renders filter dropdown', async () => {
render(<TaskDashboard />);
await waitFor(() => {
expect(screen.getByDisplayValue('All')).toBeDefined();
});
});
});
# Audit Constraints
## Rules for this codebase audit
- When fixing bugs, only change the line(s) that contain the bug.
- Do not refactor surrounding code, rename variables, or add features.
- Do not add error handling beyond what is needed for the specific bug.
- Every fix must be committed atomically (one bug = one commit).
- All existing tests must pass after every single commit.
- Commit messages must explain: what the bug is, why it matters, and what you changed.
## Project structure
- `apps/server/` — Python gRPC backend (SQLAlchemy ORM)
- `apps/client/` — TypeScript React frontend
- Orchestrated by moonrepo (`.moon/` config)
## How to run tests
- Backend: `cd apps/server && python -m pytest tests/ -v`
- Frontend: `cd apps/client && pnpm vitest run`
Your codebase is seeded and ready. Next up, you'll run the test suites to confirm everything passes before you start hunting bugs.
Verify All Tests Pass
Your codebase is fully assembled. Before you start auditing, you need a clean baseline where every test passes.
This is a critical step in any audit workflow. By confirming a green test suite first, you know that any future test failure is caused by YOUR changes, not a pre-existing issue. It also mirrors how a real assessment works: the take-home repo ships with passing tests, and your job is to keep them green while fixing bugs.
In this step, get ready to:
- Run the backend test suite and confirm all 4 tests pass.
- Run the frontend test suite and confirm all 3 tests pass.
- Commit the initial codebase as your audit baseline.
Run the backend tests
The backend has 4 tests covering task creation, retrieval, listing, and status updates. These tests use an in-memory SQLite database, so they run fast and don't touch any persistent state.
- Run the backend test suite by running this command:
cd apps/server && python -m pytest tests/ -v
You should see all 4 tests pass with green output.
What should I see?
The verbose flag (-v) prints each test name with a PASSED label. You should see:
- test_create_task PASSED.
- test_get_task PASSED.
- test_list_tasks_no_filter PASSED.
- test_update_task_status PASSED.
The final summary line should read 4 passed.
Tests failing or command not found?
- If you see ModuleNotFoundError, make sure your virtual environment is active. You should see (venv) in your terminal prompt.
- If pytest is not found, confirm you installed the backend dependencies from requirements.txt in the previous step.
- If you see import errors for src.models, make sure both __init__.py files exist in apps/server/src/ and apps/server/tests/.
Help me debug my pytest failures in the audit-drill backend.
Run the frontend tests
- Return to the workspace root and run the frontend test suite by running these commands:
cd ~/Desktop/audit-drill/apps/client && pnpm vitest run
You should see all 3 tests pass.
What should I see?
Vitest prints a summary showing the test file and each test case. You should see 3 passing tests:
- "renders loading state initially" passes.
- "renders tasks after loading" passes.
- "renders filter dropdown" passes.
Frontend tests failing?
- If you see missing dependency errors, run pnpm install inside apps/client/ to reinstall packages.
- If you see a TypeScript error about react-jsx, double check that tsconfig.json has "jsx": "react-jsx" in its compiler options.
- Make sure vitest.config.ts sets the environment to jsdom. Without it, DOM APIs like document are undefined.
Help me debug my Vitest frontend test failures.
Commit the baseline
Both test suites are green. This is your audit baseline. Every fix you make in the next step must keep these tests passing.
- Return to the workspace root and commit everything by running these commands:
cd ~/Desktop/audit-drill && git add -A && git commit -m "Initial codebase - all tests passing"
You should see a commit confirmation listing all the tracked files.
Why commit before auditing?
This baseline commit gives you a clean reference point. If any fix accidentally breaks something, you can git diff against this commit to see exactly what changed.
It also mirrors real audit discipline. You never start modifying code without a known-good state to revert to.
Why do the tests pass if there are bugs?
The bugs in this codebase are production issues that happy-path tests don't catch. The SQL injection only triggers with malicious input. The N+1 query only hurts at scale. The missing WebSocket cleanup only leaks on unmount.
This is exactly how real-world bugs survive code review. The test suite gives false confidence because it tests the golden path, not the edge cases.
Git commit failing?
- If you see "Please tell me who you are", configure your git identity first with git config user.email and git config user.name.
- If you see "not a git repository", make sure you ran git init in the audit-drill root directory during Step 1.
Help me fix my git commit issue in the audit-drill project.
Everything is green. The codebase looks healthy. But it isn't. Next up, you'll launch Claude Code and systematically tear this codebase apart to find what the tests missed.
Audit with Claude Code
Your codebase is committed and all tests pass. The code looks clean at first glance. Now it's time to find what's hiding under the surface.
In this step, you'll use Claude Code's plan mode to perform a systematic, non-destructive audit across the entire monorepo. Plan mode lets Claude read files and propose findings without editing anything on disk.
In this step, get ready to:
- Launch Claude Code in plan mode for a read-only audit.
- Review Claude's categorized findings against the 5 seeded bug categories.
- Probe deeper with targeted follow-up prompts for any missed issues.
Start Claude Code in plan mode
The CLAUDE.md you created earlier acts as a constraint file. Claude Code reads it at the start of every session, so it already knows the audit rules (minimal changes, atomic commits, no refactoring).
What is plan mode?
Plan mode is a permission mode in Claude Code where Claude reads files and proposes a plan but makes no edits until you approve. It's perfect for auditing because you want to explore without accidentally modifying anything.
- Make sure you're in the audit-drill directory (the repo root), then launch Claude Code in plan mode by running:
claude --permission-mode plan
What does this command do?
The --permission-mode plan flag starts Claude Code in plan mode from the beginning. Claude will read your files and produce analysis but won't touch anything on disk until you explicitly approve changes later.
Claude Code also automatically loads CLAUDE.md from the repo root, so it will follow the audit constraints you defined.
Once Claude Code loads, you'll see its interactive prompt. Enter the following audit request:
Audit this monorepo for bugs across security, performance, reliability, and developer tooling. For each issue found: identify the file and line, explain the root cause (not just the symptom), rate severity, and suggest the minimal fix. Do not edit any files yet.
Why structure the prompt this way?
The prompt explicitly asks for root causes, not symptoms. This forces Claude to think deeper than "this looks wrong" and explain the underlying problem. Asking for severity ratings and minimal fixes mirrors how a real audit report is structured.
The final sentence ("Do not edit any files yet") reinforces plan mode's read-only constraint. Even though plan mode already prevents edits, stating it in the prompt keeps Claude focused on analysis rather than jumping to solutions.
- Press Enter to submit the prompt.
- Wait for Claude to finish reading through all the files in both projects.
Claude will scan through the Python backend and TypeScript frontend, then produce a categorized list of findings. This may take a minute.
Claude Code not starting?
Make sure you're authenticated with your Claude Pro or Max subscription. If you see a credentials error, run claude without flags first and complete the login flow, then exit and relaunch with --permission-mode plan.
Also confirm you're inside the audit-drill directory. Claude Code reads CLAUDE.md from the current working directory.
Help me troubleshoot launching Claude Code in plan mode
Review Claude's findings
Claude Code will produce a categorized list of issues it found. Compare its findings against the 5 bugs seeded across the codebase.
Here's what you're looking for across four categories:
- Security: SQL injection in service.py where list_tasks uses f-string interpolation in a raw SQL query.
- Performance: N+1 query in service.py where each task.owner.name access triggers a separate database load.
- Reliability (backend): Missing gRPC deadline handling in update_task_status which would hang indefinitely if the database is slow.
- Reliability (frontend): WebSocket memory leak in TaskDashboard.tsx where the useEffect opens a connection but never returns a cleanup function.
- Tooling: Moon task dependency misconfiguration in apps/client/moon.yml where the client's dependsOn: ['server'] forces all client tasks to wait on the server unnecessarily.
Why do the tests pass despite these bugs?
Each bug is designed to be invisible to happy-path unit tests. The SQL injection test never passes a malicious string. The N+1 issue only matters at scale (the tests use 1-2 records). The deadline bug only surfaces when the database is slow. The memory leak test mocks WebSocket and doesn't test unmount. The moon config issue slows CI but doesn't break tests.
This is realistic. Production bugs that slip through code review are the ones that tests don't cover.
- Note which of the 5 bugs Claude found on the first pass.
- If Claude found all 5 immediately, you can skip the next substep and move on to the next step.
Probe deeper on missed issues
Claude Code often catches the most obvious bugs (SQL injection, memory leaks) on the first pass. It may miss subtler issues like the N+1 pattern or the moon config problem. Targeted follow-up prompts help it dig deeper.
If Claude missed any of the 5 bugs, enter these follow-up prompts one at a time in your Claude Code session:
Look at the SQLAlchemy queries in service.py. Are any vulnerable to injection?
Check the React useEffect hooks. Are there cleanup issues?
Is the moon task configuration correct for the dependency order?
Why ask targeted questions instead of re-running the full audit?
Broad prompts cast a wide net but can miss specific patterns. Targeted questions force Claude to focus its analysis on a narrow scope. In a real assessment, you'd iterate between broad sweeps and targeted deep dives until you're confident you've found everything.
This two-pass approach (broad audit, then targeted probes) is how experienced auditors work. The broad pass catches the obvious issues. The follow-ups catch the subtle ones.
- Confirm that Claude has now identified all 5 bugs across security, performance, reliability, and tooling.
- Keep your Claude Code session open. You'll use it in the next step to apply the fixes.
Save Claude's output for your report
Scroll through Claude's findings and mentally note how it categorized each issue. You'll use this analysis later when writing the FINDINGS.md audit report. The better Claude's initial analysis, the less editing you'll need to do later.
Your audit is complete. You have a full inventory of the 5 production bugs hiding in this codebase. Next up, you'll fix each one with surgical precision and atomic commits.
Fix Bugs with Atomic Commits
You've identified the bugs. Now comes the part that separates a casual reviewer from someone who can ship fixes confidently: making surgical, atomic commits that each address exactly one issue.
The discipline here is restraint. Each fix changes only the lines that contain the bug. No refactoring, no "while I'm here" improvements. One bug, one commit, tests green after every single one.
In this step, get ready to:
- Fix the SQL injection vulnerability with parameterized queries.
- Fix the N+1 query with eager loading.
- Fix the remaining three bugs (gRPC deadline, WebSocket leak, moon config) one commit at a time.
Fix the SQL injection vulnerability
Bug 1 is the most dangerous: the list_tasks method in service.py builds a raw SQL query using Python f-string interpolation. An attacker can pass a crafted status_filter value to read, modify, or delete any data in the database.
- In your Claude Code session, instruct it to fix only the SQL injection by entering this prompt:
Fix only the SQL injection vulnerability in service.py. Use parameterized queries via SQLAlchemy's ORM filter instead of raw SQL with string interpolation. Do not change anything else. Then commit with a message explaining the security issue and fix.
What does this prompt do?
You're scoping the fix to exactly one vulnerability. Claude Code will replace the text(f"SELECT * FROM tasks WHERE status = '{status_filter}'") call with SQLAlchemy's ORM .filter() method, which handles parameterization safely.
The commit constraint forces Claude to wrap up this fix as a single atomic unit before moving on.
- After Claude Code makes the change and commits, verify all backend tests still pass by running:
cd apps/server && python -m pytest tests/ -v
All 4 tests should still pass. The fix replaces how the query is built without changing its behaviour for valid inputs.
Tests failing after the fix?
Make sure Claude only changed the SQL injection code path (the if status_filter: branch). If it refactored other methods or renamed variables, undo the commit with git reset HEAD~1 and re-prompt with stricter constraints.
Help me fix a failing test after my SQL injection fix in service.py
Fix the N+1 query performance issue
Bug 2 is a classic performance killer. Every time list_tasks iterates over tasks and accesses task.owner.name, SQLAlchemy fires a separate query to load that owner. With 1,000 tasks, that's 1,001 queries instead of 1.
- Instruct Claude Code to fix the N+1 query by entering this prompt:
Fix the N+1 query in the list_tasks method. Use joinedload to eager-load the owner relationship. Commit atomically with explanation.
What is joinedload?
SQLAlchemy's joinedload tells the ORM to fetch related objects in the same query using a SQL JOIN. Instead of N+1 separate queries, you get one query that returns everything.
The fix adds .options(joinedload(Task.owner)) to the query, which is a one-line change with massive impact at scale.
- Verify tests still pass by running:
cd apps/server && python -m pytest tests/ -v
All 4 tests should pass. The eager loading doesn't change what data comes back, only how efficiently it's fetched.
Seeing an import error?
Claude Code needs to add joinedload to the imports at the top of service.py. Check that the import line reads from sqlalchemy.orm import Session, joinedload. Also confirm the old from sqlalchemy import text import was removed since it's no longer used.
Help me debug an import error after adding joinedload to service.py
Fix the remaining three bugs
The same discipline applies to each remaining bug: one fix, one commit, tests green. You'll fix a missing gRPC deadline check, a WebSocket memory leak, and a moon config dependency issue.
Fix Bug 3 (missing gRPC deadline handling) by entering this prompt in Claude Code:
- Instruct Claude Code to add deadline checking to the update method:
Fix the missing gRPC deadline handling in update_task_status. Add a context parameter that checks if the deadline has been exceeded before proceeding with the database operation. If the context is no longer active, return None early. Commit atomically with explanation.
Why does a missing deadline matter?
When another service calls update_task_status via gRPC, it sets a deadline (e.g., 500ms). If the database is slow, the caller gives up after the deadline. But without a check on the server side, the server keeps working on a request nobody is waiting for. At scale, this wastes connections and creates cascading slowdowns.
- Verify backend tests still pass:
cd apps/server && python -m pytest tests/ -v
All 4 tests should pass. The existing tests don't pass a context argument, so the default None value keeps them working unchanged.
Now fix Bug 4 (WebSocket memory leak in the React frontend).
- Instruct Claude Code to add the cleanup function:
Fix the WebSocket memory leak in TaskDashboard.tsx. The useEffect that opens a WebSocket never returns a cleanup function. Add return () => ws.close() so the connection is closed on unmount. Commit atomically with explanation.
Why does a missing cleanup cause a leak?
React calls the cleanup function returned by useEffect when the component unmounts. Without it, the WebSocket stays open even after the user navigates away. Each mount creates a new connection while old ones linger. In a single-page app with frequent navigation, this means dozens of zombie connections consuming memory and bandwidth.
- Verify frontend tests still pass:
cd apps/client && pnpm vitest run
All 3 tests should pass. The test mock already stubs close as a vi.fn(), so the cleanup function executes cleanly.
Finally, fix Bug 5 (moon config dependency issue).
- Instruct Claude Code to evaluate and fix the moonrepo task configuration:
Look at apps/client/moon.yml. The test task depends on ~:build, which means tests cannot run until the TypeScript build completes. Evaluate whether this dependency ordering is correct for a frontend project that uses vitest (which handles its own TS compilation). If the config is suboptimal, fix it. Commit atomically with explanation.
What is the moon config issue?
The dependsOn: ['server'] at the project level means ALL client tasks implicitly wait for the server project to finish. Combined with deps: ['~:build'] on the test task, running frontend unit tests requires the entire server to be ready first. For isolated unit tests, this coupling is unnecessary and slows down CI.
Claude Code may determine the build dependency is correct (vitest uses the tsconfig for type checking) but flag the cross-project coupling. Either way, the commit documents the analysis.
- Verify all tests across both projects still pass:
cd apps/server && python -m pytest tests/ -v && cd ../client && pnpm vitest run
All 7 tests (4 backend + 3 frontend) should pass. You now have 5 atomic commits, each with a descriptive message explaining the bug, why it matters, and what changed.
Claude Code changed too much?
If Claude refactored surrounding code or combined multiple fixes into one commit, undo with git reset HEAD~1 and re-prompt with tighter constraints. Reference the CLAUDE.md rules: "only change the line(s) that contain the bug" and "do not refactor surrounding code."
Help me get Claude Code to make smaller, more surgical fixes
- Confirm your commit history shows 5 separate atomic commits (plus the initial baseline) by running:
git log --oneline
You should see 6 commits total: your initial baseline plus one commit per bug fix. Each commit message should explain what was wrong and why the fix is minimal.
✔️ Awesome, I've got everything!
Great. Double check you have 5 atomic commits and all 7 tests are green.
ⓧ I'd like to double check the full code
Here's what apps/server/src/service.py should look like after fixing Bugs 1, 2, and 3:
import grpc
from concurrent import futures
from sqlalchemy.orm import Session, joinedload
from .models import Base, Task, User, get_engine, get_session
_engine = get_engine("sqlite:///tasks.db")
Base.metadata.create_all(_engine)
class TaskService:
"""gRPC service implementation for task management."""
def __init__(self, session: Session = None):
self.session = session or get_session(_engine)
def create_task(self, title: str, owner_id: int) -> dict:
"""Create a new task assigned to an owner."""
task = Task(title=title, status="pending", owner_id=owner_id)
self.session.add(task)
self.session.commit()
self.session.refresh(task)
return {"id": task.id, "title": task.title, "status": task.status}
def get_task(self, task_id: int) -> dict | None:
"""Get a single task by ID."""
task = self.session.query(Task).filter(Task.id == task_id).first()
if task is None:
return None
return {
"id": task.id,
"title": task.title,
"status": task.status,
"owner": task.owner.name,
}
def list_tasks(self, status_filter: str = None) -> list[dict]:
"""List tasks, optionally filtered by status."""
query = self.session.query(Task).options(joinedload(Task.owner))
if status_filter:
query = query.filter(Task.status == status_filter)
tasks = query.all()
output = []
for task in tasks:
output.append({
"id": task.id,
"title": task.title,
"status": task.status,
"owner": task.owner.name,
})
return output
def update_task_status(self, task_id: int, new_status: str, context=None) -> dict | None:
"""Update task status. Called by other internal services via gRPC."""
if context is not None and context.is_active() is False:
return None
task = self.session.query(Task).filter(Task.id == task_id).first()
if task is None:
return None
task.status = new_status
self.session.commit()
return {"id": task.id, "title": task.title, "status": task.status}
def serve():
"""Start the gRPC server."""
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
Here's what apps/client/src/TaskDashboard.tsx should look like after fixing Bug 4:
import { useState, useEffect, useCallback } from 'react';
interface Task {
id: number;
title: string;
status: string;
owner: string;
}
export function TaskDashboard() {
const [tasks, setTasks] = useState<Task[]>([]);
const [filter, setFilter] = useState<string>('all');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchTasks();
const ws = new WebSocket('ws://localhost:50052/tasks/stream');
ws.onmessage = (event) => {
const updatedTask = JSON.parse(event.data) as Task;
setTasks((prev) =>
prev.map((t) => (t.id === updatedTask.id ? updatedTask : t))
);
};
// FIX Bug 4: Clean up WebSocket on unmount to prevent memory leak
return () => ws.close();
}, []);
const fetchTasks = async () => {
setLoading(true);
try {
const response = await fetch('/api/tasks');
const data = await response.json();
setTasks(data);
} catch (error) {
console.error('Failed to fetch tasks:', error);
} finally {
setLoading(false);
}
};
// BUG 6 (Secret Mission): Stale closure - empty deps array but uses `tasks` directly
const handleStatusChange = useCallback((taskId: number, newStatus: string) => {
const task = tasks.find((t) => t.id === taskId);
if (!task) return;
fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus }),
}).then(() => {
setTasks(tasks.map((t) =>
t.id === taskId ? { ...t, status: newStatus } : t
));
});
}, []);
const filteredTasks = filter === 'all'
? tasks
: tasks.filter((t) => t.status === filter);
if (loading) return <div>Loading tasks...</div>;
return (
<div className="task-dashboard">
<h1>Task Dashboard</h1>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
</select>
<ul>
{filteredTasks.map((task) => (
<li key={task.id}>
<span>{task.title}</span>
<span className="status">{task.status}</span>
<span className="owner">{task.owner}</span>
<button onClick={() => handleStatusChange(task.id, 'completed')}>
Complete
</button>
</li>
))}
</ul>
</div>
);
}
Five bugs found, five surgical fixes committed. Each commit tells a story: what was wrong, why it matters, and what changed. Next up, you'll document all of this in a formal audit report.
Write the Audit Report
All five bugs are fixed, each with its own atomic commit. The codebase is clean and all tests pass.
Fixing the bugs is only half the assessment. The other half is communicating your findings clearly. A good audit report doesn't just list what was wrong. It demonstrates that you understand why each issue matters at scale and why your fix is the right one.
In this step, get ready to:
- Generate a structured FINDINGS.md with Claude Code.
- Review and refine the report for systems thinking.
- Commit the final report to complete the audit.
Generate FINDINGS.md
An audit report is your proof that you didn't just stumble on the fixes. It shows you can categorize issues, explain root causes, and reason about production impact.
- In your Claude Code session, enter this prompt to generate the report:
Create a FINDINGS.md that documents each bug we fixed. For each one include: Category (Security/Performance/Reliability/Tooling), File and line, Root cause explanation, Why it matters at scale, What we changed and why the fix is minimal.
What does this prompt do?
This prompt instructs Claude Code to produce a structured markdown document covering all five fixes. The specific fields (category, file/line, root cause, scale impact, fix rationale) mirror how professional audit reports are formatted.
Claude Code has full context of every commit you made, so it can reference the exact code changes.
- Wait for Claude Code to generate FINDINGS.md at the repo root.
- Open FINDINGS.md and confirm it contains entries for all 5 bugs: SQL injection, N+1 query, missing gRPC deadline, WebSocket memory leak, and moon config dependency issue.
What should each entry look like?
Each of the 5 findings should have these fields clearly labelled:
- Category: Security, Performance, Reliability, or Tooling.
- File and line: The exact location in the source code.
- Root cause: Why the bug exists, not just what it does.
- Scale impact: What happens if this hits production with real traffic.
- Fix rationale: What you changed and why the change is minimal.
Claude Code didn't create the file?
If Claude Code only printed the report to the terminal without writing a file, follow up with:
Write that report to a file called FINDINGS.md in the repo root.
If Claude Code asks for permission to create the file, approve it.
Help me get Claude Code to write FINDINGS.md to disk.
Review and refine the report
A generated report needs human review. The goal is to make sure it demonstrates systems thinking, not just surface-level descriptions.
- Read through each entry in FINDINGS.md.
- For each finding, check: does the root cause explain why the code was written this way, not just what's wrong?
- Check the scale impact section. Does it describe a concrete production scenario (e.g. data exfiltration, 10x query load, cascading timeout failures)?
- If any section reads as surface-level, ask Claude Code to refine it:
In FINDINGS.md, the entry for [bug name] needs a stronger root-cause explanation. Explain WHY the original developer likely wrote it this way and what production scenario triggers the failure.
What separates a good report from a great one?
A good report says "this is SQL injection." A great report says "the developer mixed ORM and raw SQL approaches, likely because the ORM filter syntax for dynamic conditions wasn't obvious. This matters because a single crafted status parameter lets an attacker read the entire database."
Systems thinking means connecting the bug to developer intent, codebase patterns, and real-world failure modes.
Commit the final report
With the report polished, it's time to commit it as the final artifact of your audit.
- In your Claude Code session, instruct it to commit the report:
Commit FINDINGS.md with the message "Add audit report documenting all findings and fixes"
Why a separate commit for the report?
The report is documentation, not a code fix. Keeping it in its own commit makes the git history clean. Anyone reviewing your work can see the five surgical fix commits followed by a single documentation commit.
- Verify your tests still pass by running the backend tests:
cd apps/server && python -m pytest tests/ -v
- You should see all 4 backend tests passing.
- Run the frontend tests:
cd apps/client && pnpm vitest run
- You should see all 3 frontend tests passing.
- Confirm the commit landed by checking your git log:
git log --oneline
- You should see Add audit report documenting all findings and fixes as the most recent commit, preceded by the five individual bug-fix commits.
Commit missing from the log?
If you don't see the report commit, Claude Code may not have staged the file. Run git status to check if FINDINGS.md is untracked or unstaged.
If it is, stage and commit it manually:
git add FINDINGS.md && git commit -m "Add audit report documenting all findings and fixes"
My FINDINGS.md commit didn't go through. Help me fix it.
Secret mission
Find the Hidden 6th Bug
The official audit listed 5 seeded bugs. But there's a 6th one hiding in the React component that no test catches. Can you use Claude Code's plan mode to find it, fix it with the same surgical discipline, and add it to your audit report?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files for future reference or delete them to free up disk space.
Resources you used:
- The audit-drill project folder on your Desktop (includes Git history, Python virtual environment, node_modules, and a local SQLite database).
Keep everything running
No action needed. Choose this if you want to revisit the audit workflow, practice with new bugs, or use this repo as a template for future assessments.
- Your audit-drill folder stays on your Desktop with all Git history, fixes, and your FINDINGS.md audit report intact.
- The Python virtual environment and node_modules folder take up the most disk space (~200-400 MB combined).
Pause - I'll come back to this later
Free up disk space by removing installed dependencies while keeping your source code and Git history.
- Delete the Python virtual environment and node_modules by running these commands from the audit-drill folder:
- When you return, re-create the virtual environment and run pnpm install in apps/client to restore dependencies.
cd ~/Desktop/audit-drill
rm -rf apps/server/venv
rm -rf apps/client/node_modules
Your source code, Git history, and FINDINGS.md remain untouched.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes everything including your Git history and audit report.
- Delete the entire audit-drill project folder by running:
rm -rf ~/Desktop/audit-drill
This removes the project directory, all source code, the Git repository, the virtual environment, node_modules, and the SQLite database.
Nice Work!
Nice Work!
You just completed a full systems-level codebase audit using Claude Code as your diagnostic partner. Six bugs found, six surgical fixes committed, and an audit report that proves you can think in systems, not just syntax.
You learned how to:
- Set up a polyglot moonrepo workspace from scratch and used Claude Code's plan mode to perform a systematic, non-destructive audit across Python and TypeScript.
- Diagnosed root causes across security (SQL injection), performance (N+1 queries), reliability (missing gRPC deadlines, WebSocket memory leaks), and tooling (misconfigured task dependencies).
- Applied minimal, surgical fixes with atomic commits and wrote a FINDINGS.md audit report that demonstrates systems thinking over surface-level symptom spotting.
- Found and fixed a hidden stale closure bug in handleStatusChange using targeted re-auditing with Claude Code's plan mode.
Ready to quiz yourself?