AI Agent Loops Without Making Progress

Agent keeps trying the same fix or oscillates between two states. Break the loop fast.

You watch your Claude Code or Cursor agent run npm test for the twelfth time, edit the same expect(...).toBe(...) line, run again, fail again, revert. Or Aider ping-pongs between git diff and git restore. Tokens burn, the progress bar doesn’t move. 95% of the time the model isn’t “incapable of fixing this” — it’s stuck in a broken feedback loop: a flaky test, an under-constrained prompt, or context that no longer matches the code on disk. This article shows how to spot a loop in 30 seconds, kill it with one prompt, and prevent it from starting next time.

Common causes

Ordered by hit rate, highest first.

1. The test it keeps running is flaky

The single most common trigger. A test uses Date.now(), network calls, randomness, or snapshot timestamps, so the same code passes one run and fails the next. Agent sees red, edits, green, next run red — it thinks it broke its own fix, reverts, green again, edits again, red again.

Run 1: ✓ pass
Run 2: ✗ fail (snapshot mismatch — timestamp drift)
Run 3: ✓ pass (after agent "reverted")
Run 4: ✗ fail

How to spot it: Stop the agent and run the same command three times yourself without touching code. If you can reproduce green-red-green, the test is the problem, not the agent.

2. Ambiguous prompt — agent oscillates between interpretations

You said “fix the login bug,” but the codebase has both OAuth and magic-link paths. Agent fixes OAuth, magic-link test fails, agent switches to magic-link, OAuth test fails — it bounces between two mutually exclusive interpretations of the goal.

How to spot it: Look at the diffs of the last 5 actions. If they touch two different files in different functions and each new edit reverts the previous one, the prompt is under-constrained.

3. Plan is wrong but agent refuses to replan

Agent committed to “change schema → change API → change UI.” Step 1 is impossible (table already has data), but agent stays in step 1 rewriting migrations forever instead of revisiting the plan.

How to spot it: Ask the agent to print its current plan and which step it’s on. If it says “still on step 2” for more than 5 iterations, force a replan.

4. Context window saturated with old errors

In a long Claude Code or Codex session, 80% of the context is stale “previous failure” logs. The agent is fitting to those old errors and can’t see that the code has changed since.

How to spot it: Open a fresh session, paste the current code, restate the goal. If it succeeds on the first try, context pollution was the cause.

5. Agent ping-pongs between mock and real implementation

It writes a mock to make a test pass, next iteration decides the mock is unrealistic, edits the real implementation, original test breaks, goes back to the mock — loop.

How to spot it: Search the diff trail for jest.mock, vi.mock, MagicMock, or sinon.stub being added and removed across runs.

6. Tool call failed but agent didn’t notice

The Bash tool returned a non-zero exit code, but the agent parsed it as stdout and treated the command as “successful with weird output,” then ran it again.

How to spot it: Check the exit codes of the last 3 tool calls. If they’re non-zero but the agent never said “the command failed” in chat, it missed the failure.

Shortest path to fix

Ordered by ROI. Steps 1 and 2 break most loops in under a minute.

Step 1: Stop the agent, read the last 10 actions

Hit Esc (Claude Code) or click Stop (Cursor / Codex). Don’t let it keep burning tokens. Then ask in chat:

List the last 10 actions you took (file path + edit summary + command + result).
No commentary, just a table.

90% of the time you’ll see it bouncing between two files or editing the same line over and over.

Step 2: Break the loop with a one-sentence constraint

The most effective anti-loop prompt template:

Stop. Do not edit X anymore.
The real situation is Y.
Next, do only one thing: Z.
Do not touch any other file.

Concrete examples:

Loop typeConstraint prompt
Edits test expectations repeatedly”Don’t touch the test. The test is right — change the implementation to match.”
Adds/removes mocks repeatedly”Keep the real implementation. Delete all mocks and run the integration test to see the real error.”
Rewrites migrations repeatedly”Schema is frozen. Restart the plan from step 1 but only touch the UI layer.”
Bouncing between two files”You may only edit src/auth/login.ts. All other files are read-only.”

Step 3: Pin the test or success criterion

If the agent is chasing a flaky test, stabilize the test first:

# Run the same test 5 times — is it stable?
for i in {1..5}; do npm test -- --testNamePattern="login flow"; done

If you see 2+ failures, mock time / network / randomness before resuming:

// vitest setup
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-01'));

Once the test is stable, let the agent resume.

Step 4: Open a fresh session with clean context

If none of the above works, the last lever is to reset context:

  1. Copy the full current file contents (not the diff).
  2. Open a new session.
  3. Restart with this template:
Goal: [one-sentence goal]
Current code (already edited N rounds):
[paste full file]
Current failing test / error:
[paste full error]
Constraints: don't touch tests, don't touch config, only edit src/X.ts.
Output your plan first. Wait for my approval before editing.

A fresh context isn’t polluted by old failures and usually succeeds on the first try.

Step 5: Set a hard iteration cap

Prevent the next loop from happening. In Claude Code, add to CLAUDE.md:

## Agent behavior constraints
- Maximum 5 build/test iterations per task
- If still failing after 3 iterations, stop and report status for human decision
- Do not edit the same block of code in the same file more than 3 times

Cursor uses .cursorrules, Aider uses .aiderrules, Codex uses AGENTS.md — same idea.

Prevention

  • Set iteration caps in CLAUDE.md / AGENTS.md / .cursorrules — e.g., “stop and ask after 3 consecutive failures of the same test”
  • Inspect the agent’s reasoning / action trace, not just final output — loop patterns only show in the trace
  • Isolate flaky tests into a flaky.test.ts file so agents never iterate against unstable feedback
  • On long tasks, force the agent to re-print “current plan + current progress” every 5 iterations
  • Give the agent an explicit completion criterion — not “fix login” but “npm test -- auth all green”
  • When a loop appears, open a fresh session instead of trying to recover the polluted one

Tags: #AI coding #Debug #Troubleshooting