Skip to content

AI-assisted debugging patterns are five evidence-first recipes for Claude Code, Codex and Cursor: instrument and correlate intermittent failures, turn a stack trace into a targeted fix, find memory leaks from heap snapshots, reproduce race conditions with a failing test, and rebuild cross-service timelines by request ID. Each recipe ends with a check that fails before the fix and passes after.

This page is for developers who hand bugs to a coding agent, and for tech leads who decide what evidence a bug-fix pull request must carry. The situation: about one login in three fails in production, the stack trace points at a line that looks fine, and the failure disappears when you attach a debugger. Asked to “fix the auth bug”, the agent returns a confident guess and a diff that makes the symptom go quiet. The patterns below make the agent collect evidence first and prove the fix with a check you can rerun.

  • A symptom-to-pattern table that tells you which evidence to collect before the agent edits anything
  • Seven copy-paste prompts: instrumentation, log correlation, stack-trace diagnosis, heap-diff analysis, race reproduction, cross-service timelines, and a review of the fix
  • The exact commands to pipe logs into Claude Code and Codex without an interactive session
  • An evidence checklist that lets a reviewer approve a bug fix without reading every line of the diff
  • The ways agent debugging goes wrong, with a recovery for each

Start from the symptom. Every row names the evidence the agent needs and the check that proves the fix, because a fix with no check that failed first is still a guess.

SymptomPatternEvidence to give the agentProof that the fix works
Fails some of the time, no clear trigger1. Instrument and correlateStructured logs from 100 or more reproduced runsThe same loop shows zero failures
One exception with a stack trace2. Stack trace to targeted fixThe full trace, the event payload, the releaseA test built from the event fails, then passes
Memory climbs until the process restarts3. Heap-snapshot diffTwo snapshots under the same load, top retainersA soak script (the leaking operation repeated thousands of times under a heap budget) passes
Duplicates or lost updates under load4. Failing concurrency testThe operation, the shared state, the observed duplicateThe concurrent test fails before and passes after
Slow or failing only across services5. Request-ID timelineLogs or spans from every hop, keyed by request IDThe gap is gone in a fresh timeline or trace

For a deterministic bug with a clear reproduction, you need none of this: write the failing test and let the agent fix it, as in test-driven development with AI.

Two additions make every pattern below cheaper. A debugging skill gives the agent a method (evidence first, one hypothesis at a time, a failing test before the fix), so you stop repeating it in every prompt. A read-only error-tracking MCP server lets the agent pull the real stack trace and event instead of your paraphrase of it. Both installs are covered in depth, with their traps, on testing and debugging skills and observability MCP servers.

Terminal window
# Terminal, repository root
npx skills add obra/superpowers --skill systematic-debugging -a claude-code -y
claude mcp add -s project --transport http sentry https://mcp.sentry.dev/mcp
claude mcp login sentry

On the Sentry OAuth screen, leave Triage Issues and Manage Projects & Teams unchecked so the agent can read issues and events but not resolve or reassign them. If you use another error tracker, the patterns work the same with a pasted trace.

Pattern 1: Instrument and correlate an intermittent failure

Section titled “Pattern 1: Instrument and correlate an intermittent failure”

An intermittent failure needs data from many runs, not a longer stare at one. The agent adds narrow, structured logging, you reproduce the failure many times, and the agent correlates failing runs against passing ones before it proposes anything.

  1. Commit or stash your work first, so git diff shows only what the agent adds. The instrumentation is temporary; you revert it at the end.

  2. Ask for logging only, scoped to the suspect path. Give the symptom, the rate, and what you have ruled out.

  3. Reproduce many times and capture the output. A single passing run proves nothing about an intermittent bug. Run the narrowest test that exercises the path in a loop:

    Terminal window
    # Terminal. 100 runs of one test file; every run is bracketed in the log
    for i in $(seq 1 100); do
    echo "RUN $i START" >> debug.log
    npx vitest run tests/auth/validate.test.ts >> debug.log 2>&1 \
    && echo "RUN $i PASSED" >> debug.log || echo "RUN $i FAILED" >> debug.log
    done
    grep -c "^RUN [0-9]* FAILED" debug.log

    If the test never fails locally, the trigger is environmental (clock skew between hosts, load, a shared cache). Capture the [AUTH-DEBUG] lines from staging or production for the same window instead.

  4. Ask for correlation, not a fix. Pipe the log to the agent in a read-only run, so it cannot edit code while it reasons. The flags below were checked against Claude Code 2.1.283 and Codex 0.157.1.

    Terminal window
    # -p runs non-interactively and prints the answer; plan mode keeps it read-only
    grep -E "AUTH-DEBUG|^RUN " debug.log | claude -p --permission-mode plan \
    "Correlate the failing [AUTH-DEBUG] events against the passing ones. What do the failures share that the passes do not? Give confidence and the log lines that support it. No fix yet."

    A useful answer names a testable cause, for example: “every failure has exp less than 2 s ahead of the validating host’s clock, and all of them come from host B, whose clock runs 4 s ahead.” A vague answer (“possibly a race condition”) means the logging missed the signal; add the field it asks for and run the loop again.

  5. Turn the cause into a failing test, then fix. The test pins the condition (a token validated by a host whose clock is 4 s ahead) and must fail on the current code. Then the agent adds the fix, for example a small clock-skew tolerance, and the loop from step 3 runs again. Zero failures in 100 runs, plus the new test, is the evidence.

  6. Remove the instrumentation. Ask the agent to revert every [AUTH-DEBUG] line, then confirm with git grep AUTH-DEBUG returning nothing. Durable logging belongs in logging patterns, not in leftovers from a hunt.

Pattern 2: Turn a stack trace into a targeted fix

Section titled “Pattern 2: Turn a stack trace into a targeted fix”

A stack trace pins the file, line and call chain, which makes it the richest single input you can give an agent. The risk is the fix: the fastest way to make Cannot read properties of undefined disappear is optional chaining everywhere, which hides the real bug. Make the agent classify the missing value before it touches code.

A good fix keeps the two cases apart:

async createOrder(userId: string, input: OrderInput) {
// "Should never happen": the controller validated userId, so this is a bug upstream
const user = await this.users.find(userId);
if (!user) throw new NotFoundError(`User ${userId} not found`);
// "Expected sometimes": users created before billing launched have no customer record
if (!user.billingCustomer) {
logger.warn('user without billing customer; creating one', { userId });
user.billingCustomer = await this.billing.createCustomer(user);
}
return this.orders.create(user, input);
}

The test that matters is the one built from the real event. If the agent’s test passes on the unfixed code, it reproduced something else; send it back to step 1.

This prompt, and the ones in patterns 3 and 4, work unchanged in Claude Code, Codex and Cursor. Only the headless piping in patterns 1 and 5 differs by tool, which is why only those steps carry tabs.

Pattern 3: Find a memory leak from heap snapshots

Section titled “Pattern 3: Find a memory leak from heap snapshots”

Heap usage that climbs until the process restarts is a retention problem, and retention is visible in two heap snapshots taken under the same load. Collect the evidence with Node’s own tooling, then hand the agent the retainers, not the phrase “it leaks”.

  1. Start the service with snapshots on a signal. --heapsnapshot-signal writes a .heapsnapshot file to the working directory each time the process receives the signal:

    Terminal window
    # Terminal
    node --heapsnapshot-signal=SIGUSR2 dist/server.js &
    # run warm-up load until heap usage is steady, then:
    kill -USR2 $! # snapshot 1
    # run the same load for 30 minutes, then:
    kill -USR2 $! # snapshot 2

    Writing a snapshot blocks the event loop and needs roughly as much extra memory as the heap itself, so on a service already near its memory limit it can trigger the very restart you are chasing. Take the snapshots on one instance drained from the load balancer, or in staging under the same load, and leave memory headroom.

  2. Compare the snapshots. Load both into Chrome DevTools (Memory tab), select the second, and switch the view to Comparison. Sort by size delta and copy the top 10 constructors with their retainer paths.

  3. Give the agent the diff and ask for the retention path.

  4. Run the soak script before and after the fix. It needs --expose-gc for global.gc(): node --expose-gc scripts/soak-leak.mjs. A plain Node script avoids the question of whether your test runner’s worker processes inherit the flag. The usual cause is an unbounded collection: listeners, timers or cache entries that are added per request and never removed. The fix is a release path that callers actually invoke, and the soak script, run in CI, is what shows they keep doing it.

Pattern 4: Reproduce a race condition with a failing test

Section titled “Pattern 4: Reproduce a race condition with a failing test”

A race fixed without a failing test is a race you hope you fixed. Make the agent write a concurrent test that fails on the current code, then implement the smallest guard that turns it green.

The last two sentences of that prompt carry the lesson. Without them, the obvious guard is an in-memory Map keyed on the user ID, which passes a single-process test and still double-charges once two instances receive the two clicks. A guard that survives several instances looks like this:

async processPayment(key: string, payment: Payment) {
// INSERT ... ON CONFLICT (idempotency_key) DO UPDATE SET status = 'pending'
// WHERE payment_attempts.status = 'failed'
// against a unique index on idempotency_key:
// only one instance can claim the key, however many receive the click
const claimed = await db.paymentAttempts.insertIfAbsent({ idempotencyKey: key, status: 'pending' });
if (!claimed) throw new ConflictError('Payment already processing');
try {
const charge = await gateway.charge(payment, { idempotencyKey: key });
await db.paymentAttempts.update(key, { status: 'done', chargeId: charge.id });
return charge;
} catch (err) {
// release the claim, or a gateway error leaves it 'pending' and every retry gets ConflictError
await db.paymentAttempts.update(key, { status: 'failed' });
throw err;
}
}

The catch branch matters as much as the claim: without it, one gateway timeout locks that key for good. insertIfAbsent must treat a failed row as claimable again. Releasing the claim is only safe because the key also reaches the gateway: a timeout does not tell you whether the charge went through, and a gateway that deduplicates on the same key turns the retry into a lookup instead of a second charge. If your gateway has no idempotency key, leave the row pending and reconcile it against the gateway before allowing a retry. The test suite needs a second case where the gateway mock throws once and the retry succeeds.

Run the test at least 20 times in a loop, as in pattern 1. A concurrency test that passes once proves little.

Pattern 5: Rebuild a cross-service timeline by request ID

Section titled “Pattern 5: Rebuild a cross-service timeline by request ID”

When a request crosses services, no single log explains the failure; the explanation is in the gaps between hops. Stamp a request ID at the edge, collect every hop’s logs, and have the agent rebuild the timeline.

Terminal window
# Terminal. Collect one hour of logs from each hop (adjust to your platform)
for app in api-gateway order-service payment-service; do
kubectl logs -l app=$app --since=1h --timestamps --tail=-1 | sed "s/ / [$app] /" > $app.log
done
grep -h "req_7f3a2c" *.log | sort | claude -p --permission-mode plan \
"Rebuild the timeline for request req_7f3a2c across these services. Flag the longest gap."

The sed tags each line with its service right after the timestamp, so sort still orders by time and the agent can see which hop emitted each line. --tail=-1 matters: with a label selector, kubectl logs returns only the last 10 lines per pod unless you set --tail, and the agent would rebuild the timeline from missing data without any warning.

Production logs carry user data and user-controlled strings: redact emails, tokens and IDs first (for example, pipe them through a sed filter), and keep the run read-only, as here, because log lines can contain text that reads like instructions. The same applies to [AUTH-DEBUG] lines captured from staging or production in pattern 1.

For Codex, replace the last command with codex exec -s read-only and the same prompt. In Cursor, attach the three files with @ in Plan Mode.

A timeline turns “checkout is slow sometimes” into “payment-service waited 5 s for a database connection because the pool was exhausted by a batch job”. If your services export OpenTelemetry spans, give the agent the trace instead of text logs: the prompt works unchanged, and the parent-child structure removes the guesswork about ordering. Wiring the traces and alerts themselves is covered in monitoring and logging patterns.

How do you prove a bug fix without reading every line?

Section titled “How do you prove a bug fix without reading every line?”

A reviewer should approve a bug-fix pull request on evidence, and read code only where the evidence is missing or surprising. Require these items in the pull request description:

EvidenceWhat it provesWho produces it
The reproduction (test or loop) and its output before the fixThe agent reproduced the reported bug, not a neighbour of itThe agent, checked by the developer
The same reproduction after the fix, including the run countThe cause is gone, not just less frequentThe agent
Full test suite, type check and lint green in CINothing else brokeCI
The diagnosis in two sentences: cause, and the evidence for itThe fix addresses the cause, not the symptomThe developer
A second-agent review of the diff against that diagnosisSide effects a tired author missesA separate review run
The error rate or metric after deploy, for one traffic peakThe fix held in productionThe developer who shipped it

The built-in reviewers give you a general pass with no diagnosis: /code-review in a Claude Code session, codex exec review --uncommitted in the terminal, or Bugbot on the pull request in Cursor. In Codex, custom instructions and a diff target are mutually exclusive: codex exec review "<prompt>" with no target flag takes your instructions, but codex exec review --uncommitted "<prompt>" fails with “the argument ‘—uncommitted’ cannot be used with ‘[PROMPT]’” (checked against codex-cli 0.157.1).

To review the fix against the diagnosis, pipe the diff into a fresh read-only headless session with the prompt below: git diff | claude -p --permission-mode plan "<review prompt>" or git diff | codex exec -s read-only "<review prompt>" (or codex exec review "<review prompt>" without a target flag). In Cursor, paste the prompt into Plan Mode.

Sign-off: the developer who ran the hunt owns the evidence; the reviewer, usually the tech lead for an incident fix, approves when every row is present. The agent must not edit the reproduction test to make it pass. Protecting the oracle shows how to make that mechanically impossible rather than a matter of trust.

What goes wrong when an agent debugs for you?

Section titled “What goes wrong when an agent debugs for you?”

Did this guide help you?

One tap, no sign-in.

not at all a lot