Debugging Patterns
PLAI-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.
What these debugging patterns give you
Section titled “What these debugging patterns give you”- 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
Which debugging pattern fits your bug?
Section titled “Which debugging pattern fits your bug?”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.
| Symptom | Pattern | Evidence to give the agent | Proof that the fix works |
|---|---|---|---|
| Fails some of the time, no clear trigger | 1. Instrument and correlate | Structured logs from 100 or more reproduced runs | The same loop shows zero failures |
| One exception with a stack trace | 2. Stack trace to targeted fix | The full trace, the event payload, the release | A test built from the event fails, then passes |
| Memory climbs until the process restarts | 3. Heap-snapshot diff | Two snapshots under the same load, top retainers | A soak script (the leaking operation repeated thousands of times under a heap budget) passes |
| Duplicates or lost updates under load | 4. Failing concurrency test | The operation, the shared state, the observed duplicate | The concurrent test fails before and passes after |
| Slow or failing only across services | 5. Request-ID timeline | Logs or spans from every hop, keyed by request ID | The 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.
Set up the agent before the first bug
Section titled “Set up the agent before the first bug”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, repository rootnpx skills add obra/superpowers --skill systematic-debugging -a claude-code -yclaude mcp add -s project --transport http sentry https://mcp.sentry.dev/mcpclaude mcp login sentry# Terminal, repository rootnpx skills add obra/superpowers --skill systematic-debugging -a codex -ycodex mcp add sentry --url https://mcp.sentry.dev/mcpcodex mcp login sentry# Terminal, repository rootnpx skills add obra/superpowers --skill systematic-debugging -a cursor -yThen add the server to .cursor/mcp.json and complete the OAuth sign-in when Cursor asks:
{ "mcpServers": { "sentry": { "url": "https://mcp.sentry.dev/mcp" } } }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.
-
Commit or stash your work first, so
git diffshows only what the agent adds. The instrumentation is temporary; you revert it at the end. -
Ask for logging only, scoped to the suspect path. Give the symptom, the rate, and what you have ruled out.
-
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 logfor i in $(seq 1 100); doecho "RUN $i START" >> debug.lognpx vitest run tests/auth/validate.test.ts >> debug.log 2>&1 \&& echo "RUN $i PASSED" >> debug.log || echo "RUN $i FAILED" >> debug.logdonegrep -c "^RUN [0-9]* FAILED" debug.logIf 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. -
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-onlygrep -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."Terminal window # Piped stdin is appended to the prompt; read-only sandbox, no editsgrep -E "AUTH-DEBUG|^RUN " debug.log | codex exec -s read-only \"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."Switch to Plan Mode, attach
debug.logto the chat with@, and paste the correlation prompt below. Plan Mode produces a plan instead of edits, which keeps this step read-only.A useful answer names a testable cause, for example: “every failure has
expless 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. -
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.
-
Remove the instrumentation. Ask the agent to revert every
[AUTH-DEBUG]line, then confirm withgit grep AUTH-DEBUGreturning 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”.
-
Start the service with snapshots on a signal.
--heapsnapshot-signalwrites a.heapsnapshotfile to the working directory each time the process receives the signal:Terminal window # Terminalnode --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 2Writing 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.
-
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.
-
Give the agent the diff and ask for the retention path.
-
Run the soak script before and after the fix. It needs
--expose-gcforglobal.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. 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.logdonegrep -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:
| Evidence | What it proves | Who produces it |
|---|---|---|
| The reproduction (test or loop) and its output before the fix | The agent reproduced the reported bug, not a neighbour of it | The agent, checked by the developer |
| The same reproduction after the fix, including the run count | The cause is gone, not just less frequent | The agent |
| Full test suite, type check and lint green in CI | Nothing else broke | CI |
| The diagnosis in two sentences: cause, and the evidence for it | The fix addresses the cause, not the symptom | The developer |
| A second-agent review of the diff against that diagnosis | Side effects a tired author misses | A separate review run |
| The error rate or metric after deploy, for one traffic peak | The fix held in production | The 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?”Where to go next with debugging
Section titled “Where to go next with debugging”- Start from the section overview: error handling recipes.
- Tool-specific debugging loops: Claude Code in the terminal, Codex across its surfaces, and Cursor’s Plan Mode and Agent mode.
- Make every production error a test first: error-driven development.
- Next in this section: logging patterns for the structured logs these prompts need, then recovery patterns for retries and graceful degradation once you know the cause.
Did this guide help you?
One tap, no sign-in.
What will you do with it?
How did it read?
What got in the way? Pick any that apply
Your role?
Thanks. I read every answer. Michał Jaskólski
Did this guide help?
One tap each. Close whenever you like.
no sign-in