CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Claude Code
Claude Code/2026-05-01Intermediate

Eliminating Flaky Tests Systematically with Claude Code

Move past the 'just hit retry' habit. This guide walks through a structured workflow for using Claude Code to classify, reproduce, and fix flaky tests at the root cause level — without papering over them with sleeps or skips.

Claude Code196test automationflaky testsCI/CD18debugging8

CI turns red, Slack pings you, and you open the log to find a familiar test failing. You hit Retry, it goes green, and you close the tab. Sound familiar?

Flaky tests quietly erode trust in your CI pipeline. The moment a team starts assuming "it'll pass on retry," real regressions get filed under the same shrug. I once shipped a release where a genuinely broken assertion was waved through as "probably flaky" — and the bug surfaced in production through a customer report. Since then, I've stopped treating flaky tests as a "fix later" problem and started treating them as the highest-priority diagnostic work in my queue.

This article walks through a practical workflow for using Claude Code to dismantle flaky tests methodically. There is no magical one-shot command, but if you classify the cause correctly, Claude Code becomes a remarkably capable partner.

Flaky tests are almost always one of three failure modes

The first move is to stop treating "intermittent test failure" as a single thing. The actual cause almost always falls into one of three buckets.

Order dependency: Test A passing leaves global state that breaks test B. Database rows, singletons, environment variables, module-level caches — anything that persists across tests is a candidate.

Timing dependency: A setTimeout(500) waiting for an animation, or a fixed sleep(1) waiting for a job to finish. CI environments fluctuate, and any wait that's "long enough on my laptop" will eventually fail somewhere with higher load.

External state dependency: Wall-clock time, randomness, network calls to third-party APIs, file system state. Anything that varies between runs is a fault line.

Each category demands a completely different fix. Diagnosing the category before asking Claude Code to repair anything is what separates a real fix from a sleep-and-pray.

The first request to Claude Code is "classify, don't fix"

When I encounter a flaky test, I never start with "fix this." I start with this:

Read this test file and classify the most likely cause from the three categories below:
 
1. Order dependency (global state pollution)
2. Timing dependency (async or wait-based)
3. External state dependency (time, randomness, APIs, files)
 
Cite specific lines and explain why. If multiple categories apply, rank them by likelihood.
 
Target: tests/integration/payment.test.ts

The reason matters. If you ask for a fix up front, Claude Code patches the most visible suspect — which might not be the root cause. The biggest risk in flaky-test work is landing a "symptom mask" that makes the next regression even harder to diagnose.

Step 1: Get a 100% reproduction script before fixing anything

Once you have a hypothesis, the next priority is reproducibility. A flaky test you can reproduce on demand is no longer flaky — it's a regular bug.

The reproduction strategy depends on the category.

For order dependency: shuffle the test order and run many iterations.

# Vitest
npx vitest run --sequence.shuffle --sequence.seed=12345
 
# Jest
jest --testSequencer=./scripts/random-sequencer.js

I often ask Claude Code: "Write a bash script that runs Vitest with random order 50 times and collects the seeds and test names that fail." It returns something like:

#!/usr/bin/env bash
# scripts/find-flaky.sh — surface order-dependent flakiness
set -uo pipefail
 
ITERATIONS="${1:-50}"
LOG_DIR="./.flaky-logs"
mkdir -p "$LOG_DIR"
 
> "$LOG_DIR/failures.tsv"
echo -e "iter\tseed\tfailed_test" > "$LOG_DIR/failures.tsv"
 
for i in $(seq 1 "$ITERATIONS"); do
  SEED=$RANDOM
  echo "▶ iteration $i (seed=$SEED)"
 
  if ! OUTPUT=$(npx vitest run --sequence.shuffle --sequence.seed="$SEED" --reporter=json 2>&1); then
    FAILED=$(echo "$OUTPUT" | jq -r '.testResults[].assertionResults[] | select(.status=="failed") | .fullName' 2>/dev/null | tr '\n' ',')
    echo -e "${i}\t${SEED}\t${FAILED}" >> "$LOG_DIR/failures.tsv"
  fi
done
 
echo ""
echo "=== Failure log ==="
cat "$LOG_DIR/failures.tsv"

A typical output:

=== Failure log ===
iter  seed   failed_test
3     8421   integration > payment > should reject expired card,
12    19284  integration > payment > should reject expired card,
27    4477   integration > payment > should reject expired card,

The same test failing at deterministic seeds is almost always order-dependent. If different tests fail at random, you likely have a shared resource being mutated across cases.

For timing dependency: skip the shuffle. Just run the same test repeatedly (--repeat=100 or a bash loop) and measure the failure rate.

For external state dependency: vary the external — change the system clock, drop the network, swap the random seed — and watch which axis triggers the failure.

Step 2: Distinguish a real fix from a coverup

Once reproduction is solid, ask for the fix. By default, Claude Code will often suggest one of:

  • A retry wrapper (expect(...).toEventuallyEqual(...))
  • A larger setTimeout or sleep
  • An it.skip to silence the test

These hide the symptom. They don't fix the cause. I send Claude Code an explicit constraint list:

Fix the root cause of this flake. The following approaches are forbidden:
 
- Increasing wait times (sleep, setTimeout, larger waitFor timeouts)
- Adding retries
- Skipping the test
- Swallowing failures with try/catch
 
Instead, use one of:
 
A. Reset shared state in beforeEach/afterEach so tests don't leak into each other
B. Wait on events or observable state, not on time
C. Make external dependencies deterministic (frozen clock, mocks, seeded randomness)

That single constraint block changes the quality of the proposed fix dramatically. The "wait on state, not time" rule alone solves a huge fraction of Playwright and Testing Library flakes.

Common before/after patterns

Before — fragile timing:

// Wait for animation by sleeping
await page.click("#open-modal");
await new Promise((r) => setTimeout(r, 500)); // 500ms isn't guaranteed
expect(await page.isVisible("#modal-content")).toBe(true);

After — wait on the actual state:

// Wait until the modal is genuinely visible
await page.click("#open-modal");
await page.waitForSelector("#modal-content[data-state='open']", {
  state: "visible",
  timeout: 5000, // upper-bound guard, not the actual wait
});
expect(await page.isVisible("#modal-content")).toBe(true);

The timeout here is an upper bound, not a fixed delay. The wait returns as soon as the condition is satisfied, which is what makes it robust on slow CI days.

Before — testing randomness with random data:

test("IDs are unique", () => {
  const id1 = generateId();
  const id2 = generateId();
  expect(id1).not.toBe(id2); // can collide by chance
});

After — seed the randomness, test the property:

import { mockRandom } from "./helpers/mock-random";
 
test("100 generated IDs do not collide (seeded)", () => {
  const restore = mockRandom([0.1, 0.2, 0.3]); // deterministic outputs
  try {
    const ids = Array.from({ length: 100 }, generateId);
    expect(new Set(ids).size).toBe(100);
  } finally {
    restore();
  }
});

If you want to assert "no collisions," extract the randomness as a separate concern and inject deterministic values into the test. The property under test stops depending on luck.

Step 3: Build a system that catches the next one

Patching individual flakes one at a time is whack-a-mole. To stop the same patterns from breeding, I keep three habits in any team I work with.

1. Run randomized order in CI by default. A test suite that only passes in declared order is a stack of dominoes waiting for a refactor to topple it. Yes, you'll spend the first week fixing tests that "always passed." That's the debt you didn't know you had.

2. Wrap all wait helpers. Have Claude Code sweep the test directory for ad-hoc waits:

Scan all files under tests/ and list every occurrence of:
 
- new Promise(resolve => setTimeout(resolve, ...))
- await sleep(...)
- await new Promise((r) => setTimeout(r, ...))
 
Output a table of file path, line number, and wait duration in ms.

You'll get a clear migration target — replace each with a project-wide waitForCondition helper. Claude Code is very good at this kind of broad mechanical sweep.

3. Track flake history. A test that flaked, was "fixed," and starts flaking again is signaling something different. At minimum, surface "tests retried most often in the last 30 days" so you can see regressions early.

Related reading

What to do next

Next time CI fails and you catch yourself thinking "that test again," resist the Retry button. Open Claude Code, ask it to classify the failure into one of the three categories, and stop there. You haven't fixed anything yet — but naming the failure as "this is timing-dependent" is the actual breakthrough. From there, the constraint block above (no longer waits, no retries, no skips) keeps the fix honest, and the systemic habits keep the next flake from sneaking in unnoticed.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-06-18
Moving Cleanup and Logging into a SessionEnd Hook
How to use Claude Code's new post-session hook to automate temp-file cleanup and log writing after a session ends, with real examples from a pipeline that processes several repositories in sequence.
Claude Code2026-06-14
Before Per-PR CI Burns Through Your Monthly Credits: A Three-Layer Guard for Claude Code GitHub Actions
From June 15, Claude Code GitHub Actions bills against non-rolling monthly credits. Run a review on every PR and you can drain the month in the first week. Here is a three-layer guard — when to run, how heavy one run can get, and making spend visible — with working workflows.
Claude Code2026-05-28
Claude Code × Xcode Cloud — A One-Week Migration of ci_scripts and TestFlight Auto-Delivery
Notes from migrating a long-running indie iOS CI from Fastlane to Xcode Cloud in one week, with the three ci_scripts/ hook scripts in full, TestFlight automation, and dSYM upload — all paired with Claude Code.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →