Claude Code Workflows: Subagents, Code Review, and Debugging
You've mastered the basics. Your CLAUDE.md is dialed in, your hooks fire perfectly, your MCP servers give Claude direct access to your tools. But you're still working with one Claude at a time. One conversation, one task, one thread of execution.
What if you could run five?
Setups with explicit workflow patterns and agent definitions score 7.0/10 on average in our analysis. The gap between casual users and power users isn't configuration — it's how they orchestrate their work. This guide covers the patterns that produce the best results.
Subagents: delegate like a manager, not a micromanager
A subagent is a separate Claude Code session that handles a specific task. Your main session stays focused while subagents work on isolated pieces in parallel. Think of it like handing tasks to teammates — each one gets a clear brief and delivers a specific result.
When subagents make sense (and when they don't)
Good subagent tasks:
- Independent pieces that don't need shared context
- Multiple files that need changes but don't depend on each other
- Exploration tasks (researching an approach) while you continue building
- Repetitive work across similar files (updating 10 API routes the same way)
Bad subagent tasks:
- Anything requiring the full context of your ongoing conversation
- Changes where file A depends on what you decided in file B
- Quick edits that are faster to do inline
Here's the key insight: if you could hand the task to a colleague with just a written brief — no verbal context needed — it's a good subagent candidate.
How to scope a subagent task
Vague subagent tasks produce vague results. "Improve the user API" gives Claude nothing to anchor on. Here's what a well-scoped subagent task looks like:
## Task: Add input validation to user endpoints
### Files to modify
- src/api/users/route.ts
- src/api/users/[id]/route.ts
### Files to read (for context)
- src/lib/schemas.ts
- src/types/user.ts
### Requirements
- Add zod validation to all POST/PUT request bodies
- Return 400 with structured error messages for invalid input
- Do NOT modify GET or DELETE endpoints
- Do NOT change the database layer
### Done when
- Modified route files with validation
- New or updated zod schemas in src/lib/schemas.ts
Notice the explicit file scope, clear requirements, and a definition of "done." The subagent knows exactly what to touch, what to leave alone, and when to stop.
The Director pattern: running parallel subagents
For larger features, you can use Claude as a director — planning the work, then dispatching subagents for each piece:
You: "I need to add a notification system. Plan the work and use subagents."
Claude (Director):
1. Planning phase: notification schema, API endpoints, UI components
2. Subagent 1: Create database migration and Prisma schema
3. Subagent 2: Implement API endpoints (GET /notifications, POST /mark-read)
4. Subagent 3: Build the notification bell component and dropdown
5. Integration: wire everything together and test
Each subagent gets a bounded task. The director session handles integration and makes sure the pieces fit. This pattern works particularly well because subagents start clean — no accumulated context weight from a long conversation.
I use this pattern for any feature that touches more than 3 files. Five parallel terminals, each with a subagent handling one piece, while the main session keeps the big picture.
Code review: the highest-value daily workflow
Code review is one of Claude Code's strongest use cases and one of the most underused. Instead of reviewing your own code (which your brain rationalizes as correct) or waiting for a teammate, Claude catches issues in seconds.
The Fix-First method
Most review tools give you a list of problems. You then have to fix each one manually. That's backwards.
The Fix-First method: Claude fixes the mechanical issues directly and only asks you about the ambiguous ones:
## How to review code (put this in a skill or CLAUDE.md)
1. Read the FULL diff first — understand the complete change
2. AUTO-FIX mechanical issues:
- Missing error handling → add try/catch
- Obvious type errors → fix the type
- Style violations → fix them
3. Batch ambiguous decisions into ONE question:
- "I found 3 design decisions to confirm: [A], [B], [C]"
4. Never flag issues already addressed in the diff
5. Output: CRITICAL (must fix) and INFORMATIONAL (consider fixing)
You don't want a list of 15 nitpicks when 12 could have been auto-fixed. The Fix-First method respects your time.
Pre-commit review in practice
Set up a shell alias for daily use:
alias ccreview='claude -p "Review staged changes (git diff --staged). Fix mechanical issues. Flag anything critical."'
Or as a skill for richer behavior:
<!-- .claude/skills/pre-commit-review.md -->
---
name: pre-commit-review
description: Review staged changes before committing
---
1. Run `git diff --staged`
2. For each file:
- Error handling: async operations wrapped in try/catch?
- Security: hardcoded secrets, SQL injection, XSS?
- Types: any `any` that should be specific?
- Tests: modified functions have test updates?
3. AUTO-FIX mechanical issues
4. Report CRITICAL (blocks commit) and INFORMATIONAL (nice to have)
5. If no critical issues: suggest a commit message
Type /pre-commit-review before every commit. Claude checks your work, fixes the small stuff, and flags the real problems. It takes 15 seconds and catches things you'd miss at 6 PM on a Friday.
Test-Driven Development: write the spec, let Claude implement
TDD with Claude Code works differently than TDD alone. You describe what a function should do. Claude writes the test. You verify the test captures your intent. Then Claude writes the code to pass it.
It works because a test is an unambiguous specification. When you describe a feature in words, Claude might interpret it differently than you meant. When you express it as a test, expected behavior is explicit — inputs, outputs, edge cases.
The loop
1. You describe what the function should do
2. Claude writes the test
3. You verify the test matches your intent
4. Claude writes the implementation to pass the test
5. Claude runs the test to confirm
6. Repeat
A concrete example: building a rate limiter
You: "I need a rate limiter. 100 requests per minute per IP. Return 429 when
exceeded, include Retry-After header. Write the test first."
Claude writes:
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createRateLimiter } from './rate-limiter';
describe('Rate Limiter', () => {
beforeEach(() => {
vi.useFakeTimers();
});
it('should allow requests under the limit', () => {
const limiter = createRateLimiter({ maxRequests: 100, windowMs: 60000 });
const result = limiter.check('192.168.1.1');
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(99);
});
it('should block requests over the limit', () => {
const limiter = createRateLimiter({ maxRequests: 2, windowMs: 60000 });
limiter.check('192.168.1.1');
limiter.check('192.168.1.1');
const result = limiter.check('192.168.1.1');
expect(result.allowed).toBe(false);
expect(result.retryAfter).toBeGreaterThan(0);
});
it('should reset after the window expires', () => {
const limiter = createRateLimiter({ maxRequests: 1, windowMs: 60000 });
limiter.check('192.168.1.1');
vi.advanceTimersByTime(61000);
const result = limiter.check('192.168.1.1');
expect(result.allowed).toBe(true);
});
it('should track IPs independently', () => {
const limiter = createRateLimiter({ maxRequests: 1, windowMs: 60000 });
limiter.check('192.168.1.1');
const result = limiter.check('192.168.1.2');
expect(result.allowed).toBe(true);
});
});
You read the test. Four cases: under limit, over limit, window reset, IP isolation. Looks right. You tell Claude to proceed, and it writes the implementation to pass all four. The test acted as a contract you both agreed on before any production code was written.
Debugging: the systematic approach that actually works
Debugging with Claude is powerful, but it can also go in circles if you let it. Claude holds more context and explores more hypotheses than you would — but it needs structure to avoid thrashing.
The 7-step debugging workflow
Put this in your CLAUDE.md or a debugging skill:
When debugging:
1. REPRODUCE — run the failing case, capture the exact error
2. HYPOTHESIZE — list 3 possible causes, ranked by likelihood
3. INVESTIGATE — check the most likely cause first
4. LOG — add logging at every async boundary if you can't see where execution stops
5. IDENTIFY — pinpoint the exact line or function
6. FIX — apply the minimal fix
7. VERIFY — run the original failing case to confirm the fix works
The critical rule: never skip step 7. Our data shows that 40% of debugging sessions that go sideways do so because a fix was claimed but never verified. "Should work now" is not evidence. Run it.
The three-strike rule
This is the pattern that separates productive debugging from time-burning:
If Claude hasn't found the root cause after three attempts, stop. Don't let it keep guessing — increasingly wild hypotheses make the problem harder to diagnose. Instead, choose one of three paths:
Option A: New hypothesis. Step back entirely. Restate the problem from scratch. Sometimes the third attempt failed because the first attempt poisoned the reasoning.
Option B: Human review. Claude shows you everything it's found so far and you add context. Maybe you know something about the production environment or a recent deploy that Claude doesn't.
Option C: Instrument and wait. Add comprehensive logging and reproduce the issue. Let the logs tell you where execution stops instead of guessing.
When in doubt: add logging first
The most productive debugging sessions start by adding observability before trying to fix anything:
// Before: silent failure — you have no idea where it breaks
async function processOrder(orderId: string) {
const order = await db.orders.findUnique({ where: { id: orderId } });
const payment = await chargePayment(order.amount);
await db.orders.update({ where: { id: orderId }, data: { status: 'paid' } });
}
// After: every step is visible
async function processOrder(orderId: string) {
console.log('[processOrder] start', { orderId });
const order = await db.orders.findUnique({ where: { id: orderId } });
console.log('[processOrder] fetched order', { found: !!order, amount: order?.amount });
if (!order) {
console.error('[processOrder] order not found', { orderId });
throw new Error(`Order ${orderId} not found`);
}
const payment = await chargePayment(order.amount);
console.log('[processOrder] payment result', { paymentId: payment.id, status: payment.status });
await db.orders.update({ where: { id: orderId }, data: { status: 'paid' } });
console.log('[processOrder] done', { orderId });
}
When something hangs or fails, the last log line tells you exactly where execution stopped. This technique saves more debugging time than any clever analysis.
Prompting patterns that get better results
How you phrase instructions matters more than you'd think. Here are the patterns that consistently produce better code.
Define what "done" looks like
Bad: "Improve the authentication."
Good: "Add rate limiting to the login endpoint. 5 attempts per email per 15 minutes. Return 429 with Retry-After header. Write tests for the limit and the reset. Don't touch the registration endpoint."
The second prompt has no ambiguity. Claude can execute it without asking a single clarifying question.
State constraints explicitly
Claude follows the rules it knows about. If you don't mention a constraint, it won't consider it:
- Do NOT modify any files in src/legacy/ — that code is frozen
- The build must pass with Node 18 (no Node 20+ APIs)
- Keep bundle size under 200KB — check with `npm run analyze`
Ask for a plan on anything touching 3+ files
For larger tasks, ask Claude to plan before executing:
Plan how you'd implement a caching layer for API responses. Show me:
1. Which files you'd create or modify
2. The cache invalidation strategy
3. How it integrates with existing middleware
Don't write code yet. Just the plan.
Review the plan, adjust it, then let Claude execute. This catches structural mistakes before they cascade across multiple files. Changing a plan costs nothing. Undoing a half-implemented wrong approach costs hours.
How it all connects
Your setup gives Claude project knowledge. Your automation enforces standards. Your integrations give Claude access to your infrastructure. Workflows determine how Claude operates day to day.
Start with pre-commit review — it's the easiest win and the one you'll use every single day. Then try subagents next time you have a feature that touches 4+ files. Score your setup to see where your workflow orchestration stands.
Frequently Asked Questions
How many subagents can run at the same time?
It depends on your machine (RAM, CPU) and API rate limits. On a typical dev machine, 3-5 concurrent subagents work well. Each one is a separate session consuming tokens, so watch your usage if you're on API pricing.
Should I use subagents for everything?
No. They add overhead — context switching, session creation, and the risk that a subagent misses context the main session has. A good test: if you could hand the task to a colleague with just a written brief (no verbal explanation needed), it's a subagent candidate. If you'd need a 10-minute conversation to explain the context, keep it in the main session.
What do I do when Claude's fix doesn't work?
First, make sure Claude actually verified the fix by running the failing test. If the fix genuinely doesn't work, don't let Claude try the same approach again. Say "that approach didn't work — propose a different root cause." After three failed attempts, escalate: new hypothesis, human review, or add logging and gather data.
Can Claude Code do pair programming?
Yes, and it works best with a specific split: you drive architecture decisions, Claude handles implementation. Describe what you want at a high level, let Claude propose the code, review it, iterate. The anti-pattern is letting Claude make architecture decisions without your input — those compound, and by the time you notice a wrong turn, Claude has built three layers on top of it.
How do I handle large refactors?
Break them into phases, one subagent per phase. Phase 1: "Rename all occurrences of OldService to NewService." Phase 2: "Update all callers to use the new API signature." Phase 3: "Remove deprecated methods and update tests." Each phase is bounded and testable. The main session tracks progress and handles integration between phases.