Repeat-failure guards for coding agents. Most "the model is stuck in a loop" reports are a command waiting for input, a result the agent can't see, or a missing rule that says stop. A five-minute diagnosis, the environment fixes, and a tested hook.

A loop needs an exit, and the model won't always find one by itself.
TL;DR
- When an agent runs the same command over and over, the usual cause isn't the model. It's a command that waits for input nobody will type, output the agent never sees in full, or no rule that says when to stop. I ran 10 everyday commands two ways. In a terminal nobody types into, 9 of them hang until something kills them. Without a terminal, most fail fast with confusing errors instead, which invites a retry.
- Every repeat re-sends the whole conversation. One Cursor user found 4.2 million cache-read tokens in a single implementation run, and the arithmetic in the replies explains it.
- Fix it in three layers: environment variables that make tools non-interactive, a stop rule in AGENTS.md, and a 142-line hook that warns the agent on the third identical result and blocks the command after the fourth. It works with Claude Code, Codex and Cursor. Diagnosis, settings and the hook are below.
Someone posted in r/opencodeCLI this week that OpenCode keeps getting stuck in an infinite loop, "repeatedly generating or executing shell commands without making any progress." Most replies blamed the model. But the poster had already ruled that out: it happened with three different models from three providers, mostly in fresh sessions.
When the same symptom shows up across models and in short conversations, look at what the agent is running, not at who is running it.
The same day, in r/cursor, someone asked about suspiciously large token usage. One implementation run used 4.5 million tokens, and 4.2 million of them were cache reads. That one wasn't a loop, but a reply explained why it cost so much, and the explanation is also why loops are expensive: "every tool call in an agent run ... resends the whole conversation so far." With a conversation of about 80,000 tokens and about 50 tool calls, "that's about 4M cache-read tokens even if each step did almost nothing new."
A loop is the worst case of that arithmetic. Every step re-sends the whole conversation, and none of them gets anywhere.
Three things that look like a model loop

Each cause has a fix that doesn't depend on which model you use.
A command that's waiting for someone. git log opens a pager. git commit without -m opens an editor. npm init asks questions. A dev server never exits by design. An agent can't type q into a pager or answer a prompt. It gets a timeout or an error, and from where it sits, retrying looks reasonable.
A result the agent can't see. Long output gets cut. Codex keeps a per-call output budget (10,000 tokens by default) and marks cut output with "Warning: truncated output". Claude Code saves long output to a file and shows a preview. If the error that matters fell outside the part the model read, it will keep "fixing" the wrong thing. Timeouts can push the same way. When an OpenCode shell command times out, the tool result says: "Retry with a larger timeout if the command is expected to take longer." For an editor waiting on input, a larger timeout just means a longer wait.
No stop rule. Nothing in most setups says "if this failed the same way three times, stop." A model that's been told to finish the task will keep trying to finish it.
If the loop happens with every model, in fresh sessions, the model isn't the cause.
I tested which commands hang
Whether a command hangs depends on how the agent's shell runs it. Some tools run commands with plain pipes and no input. OpenCode's V2 shell, for example, starts commands with stdin set to "ignore" and writes their output to a file. Other tools can give a command a pseudo-terminal: Codex's exec_command takes a tty flag that "allocates a PTY for the command." With a terminal, programs act as if a person is there.
So I ran 10 everyday commands both ways on a Linux box, plus the variants with each fix applied (17 runs per mode), with an 8-second limit and a clean environment:
| Command | Pipes, no input | Terminal, nobody typing | Fix |
|---|---|---|---|
git log |
exits | hangs (pager) |
GIT_PAGER=cat or git --no-pager
|
less file |
exits | hangs | PAGER=cat |
git commit (no -m) |
fails fast | hangs (editor) |
-m "msg", or GIT_EDITOR=true to fail fast |
python3 -c 'input()' |
fails (EOFError) | hangs | pass input as arguments or a file |
npm init |
fails | hangs (questions) | npm init -y |
git clone from a server that wants a login |
fails | hangs (Username prompt) | GIT_TERMINAL_PROMPT=0 |
vi file |
hangs | hangs | never in an agent; edit with the tool |
python3 -m http.server, tail -f
|
hangs | hangs | run in the background, then wait for it |
The full run, including the variants with the fixes applied, is in the test output that ships with the hook. Three things stand out.
First, without a terminal most interactive commands don't hang. They fail fast, but with confusing errors such as "problem with the editor 'editor'", "EOFError" or "could not read Username". A model that doesn't recognise the cause tries again. That's a loop with a fast turnaround.
Second, with a terminal, the same commands sit until the tool's timeout. Then the agent sees a timeout, and sometimes a hint to retry with a longer one.
Third, servers and watchers never exit in either mode. They need to run in the background, with one blocking wait for whatever they're supposed to produce. I wrote about that in Stop Paying Your Agent to Wait.
The five-minute diagnosis
When an agent seems stuck, check these in order before you switch models.

Most loops are explained by step 2 or step 3.
- Look at the last three tool calls. Same command, same result? That's the loop. Same command, different results? It's probably making slow progress, or something flaky is going on.
- Look at the command. Does it open a pager, an editor or a prompt? Does it start something that never exits? Check it against the table above.
- Look at the output. Was it truncated? Did it time out? Is the real error above the part the agent read? Run the command yourself and compare.
- Ask another session. One reply in the OpenCode thread suggested this, and it works: "open another session and ask what the looping session is up to." A fresh session isn't anchored on the same wrong assumption. Ask it to read the stuck session's recent steps and say what it thinks is wrong, without changing anything.
- Look at the session. A very long or recently compacted session may have lost the instruction that mattered. In that case, write the current state to a file and start fresh, as I described in Compact, Clear or Hand Off?
Fix the environment
The cheapest fix is to make the common tools non-interactive before the agent ever runs them. Git documents all of these:
-
GIT_PAGER=cat: "If it is set to an empty string or to the value "cat", Git will not launch a pager." -
PAGER=cat: the same for other tools that use$PAGER. -
GIT_TERMINAL_PROMPT=0: "git will not prompt on the terminal (e.g., when asking for HTTP authentication)." -
GIT_EDITOR=true: commands that open an editor get one that exits at once. In my test,git commitwithout-mthen stopped with "Aborting commit due to empty commit message" instead of hanging.
npm has one more. Its docs say that for npm exec and npx, "When standard input is not a TTY or a CI environment is detected, --yes is assumed." Other npm commands have their own flags, such as npm init -y.
Where you set them depends on the tool:
| Tool | Where to set environment variables | What it already sets |
|---|---|---|
| Codex |
[shell_environment_policy] set = { ... } in config.toml
|
PAGER=cat, GIT_PAGER=cat, GH_PAGER=cat, NO_COLOR=1, TERM=dumb
|
| Claude Code |
"env": { ... } in .claude/settings.json
|
nothing pager-related that I found documented |
| Cursor | your shell profile, inside if [ -n "$CURSOR_AGENT" ]
|
CURSOR_AGENT (and sandbox variables when sandboxed) |
| OpenCode | the server's environment: opencode service set env NAME VALUE, then opencode service start
|
AGENT=1, OPENCODE=1, TERM=xterm-256color
|
Codex already covers the pager cases, which is one reason pager hangs are rarer there. Its source sets those variables for every command its shell tool runs. For the others, here is the Claude Code version:
{
"env": {
"GIT_PAGER": "cat",
"PAGER": "cat",
"GIT_TERMINAL_PROMPT": "0",
"GIT_EDITOR": "true"
}
}
Add a stop rule to AGENTS.md
AGENTS.md is the instructions file Codex, Cursor and OpenCode all read, and Claude Code reads it too when there's no CLAUDE.md. (If you keep a CLAUDE.md, put @AGENTS.md at its top.) A stop rule is three lines:
## When something fails
- If a command fails the same way twice, stop and say why before trying anything else.
- Never retry the same command a third time without changing something (the command, the code, or the environment).
- Never run pagers, editors or prompts (less, vi, git commit without -m, npm init without -y). Never run servers or watchers in the foreground.
A rule in a file is a request, not a guarantee. A long session can lose it, and a determined model can talk itself past it. That's why the last layer is a hook. I made the same argument in How Much Should You Invest in Your AGENTS.md?: move what must always happen out of prose and into something that runs.
A hook that counts
OpenCode's V1 documentation describes a built-in doom_loop permission, triggered "when the same tool call repeats 3 times with identical input." The current V2 docs say plainly that "doom_loop and lsp are not current V2 Core permission actions." I didn't find a built-in repeat guard in Claude Code or Codex either. Cursor's loop_limit caps automatic follow-ups from its stop hooks, which is a different thing.
So here's one you can add. loop-guard.py is a single Python file with no dependencies. It runs on Claude Code, Codex and Cursor, using hook events all three document.

Same command plus same result counts as a repeat. Any change in either resets the count.
How it decides:
- After each shell command, it takes the command and its result and strips what changes between runs even when nothing else does: colour codes, timestamps, durations, memory addresses. If the same command produces the same result again, the count goes up. A different result resets it, so a test run that fails with fewer errors each time never trips it.
- On the third identical result, it adds a short message to the agent's context: stop, explain why it keeps failing, and change something or ask.
- After the fourth, the next attempt at that command is blocked before it runs. The agent gets the reason.
- When you send a new message, everything resets. "Try again" from you always works.
Each tool reports results differently, and the script handles each one. Claude Code sends successful Bash output as stdout and stderr, and failures to a separate PostToolUseFailure event whose error starts with "Exit code N". Codex sends the output as one string, and its PostToolUse fires for non-zero exits too. Cursor sends a JSON string with exitCode and stdout, and timeouts go to postToolUseFailure. The script tells the three apart by fields only one of them sends: Cursor's cursor_version, Codex's turn_id.
Install it by putting the file at .agents/hooks/loop-guard.py in your repo and adding the hook config for your tool.
Claude Code (.claude/settings.json):
{
"hooks": {
"PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "python3 \"${CLAUDE_PROJECT_DIR}/.agents/hooks/loop-guard.py\"" }] }],
"PostToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "python3 \"${CLAUDE_PROJECT_DIR}/.agents/hooks/loop-guard.py\"" }] }],
"PostToolUseFailure": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "python3 \"${CLAUDE_PROJECT_DIR}/.agents/hooks/loop-guard.py\"" }] }],
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "python3 \"${CLAUDE_PROJECT_DIR}/.agents/hooks/loop-guard.py\"" }] }]
}
}
Codex (.codex/hooks.json; review and trust it with /hooks before it runs):
{
"hooks": {
"PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.agents/hooks/loop-guard.py\"" }] }],
"PostToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.agents/hooks/loop-guard.py\"" }] }]
}
}
Cursor (.cursor/hooks.json):
{
"version": 1,
"hooks": {
"preToolUse": [{ "command": "python3 .agents/hooks/loop-guard.py", "matcher": "Shell" }],
"postToolUse": [{ "command": "python3 .agents/hooks/loop-guard.py", "matcher": "Shell" }],
"postToolUseFailure": [{ "command": "python3 .agents/hooks/loop-guard.py", "matcher": "Shell" }]
}
}
Codex resets on a new turn_id and Cursor on a new generation_id, which changes "with every user message", so they need no prompt hook. In Codex, .agents is one of the paths its workspace sandbox keeps read-only, so under that profile the agent can't edit its own guard.
Here's the script:
Two settings: LOOP_GUARD_WARN (default 3) and LOOP_GUARD_BLOCK (default 4). The thresholds are a judgment call. Lower catches loops sooner. Higher leaves room for genuinely flaky commands.
A few design choices are deliberate. Cursor treats invalid output from a permission hook as a block, so the script always answers its preToolUse with valid JSON. On UserPromptSubmit it prints nothing, because Claude Code adds plain output from that event to the conversation. And if anything inside the script breaks, it allows the call and exits. A broken guard shouldn't stop your agent.
I tested it with 20 cases: payloads built in the shapes each tool documents, run through the real script. Repeats are caught even when timestamps and durations differ, changing output never trips it, other commands aren't affected, a new message resets the count, and garbage input is allowed through. I didn't run it inside the three apps, so check the first warning yourself when you install it.
OpenCode isn't covered. Its V2 plugin system is different, and I didn't want to ship a plugin I couldn't test. For OpenCode, the environment fixes and the AGENTS.md rule do most of the work.
Checklist
- When a loop happens, diagnose before you switch models. Last three calls, the command, the output, a second opinion, the session length.
-
Set the non-interactive variables for your tool:
GIT_PAGER=cat,PAGER=cat,GIT_TERMINAL_PROMPT=0,GIT_EDITOR=true. -
Use the flags that skip questions, such as
-y,--yesand-m. - Run servers and watchers in the background, with one blocking wait.
- Add the three-line stop rule to AGENTS.md.
- Install the hook for Claude Code, Codex or Cursor, and watch for its first warning.
- Keep sessions short for long implementation runs: plan in one session, write the plan to a file, implement in a fresh one.
Threads that prompted this
- r/opencodeCLI: "OpenCode keeps getting stuck in an infinite loop". It happens across three models, mostly in new sessions, and one reply suggests asking a second session what the first one is doing.
- r/cursor: "Suspiciously large token usage", where 4.2 million of 4.5 million tokens were cache reads, and the replies explain why: every tool call re-sends the whole chat, so plan in one chat and implement in a fresh one.
Sources
- Git: environment variables (
GIT_PAGER,GIT_EDITOR,GIT_TERMINAL_PROMPT,--no-pager), read 21 Oct 2026. npm: npm exec. - Claude Code: Hooks reference (PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit), settings (
env), tools reference (output limits). - Codex: Hooks, configuration reference (
shell_environment_policy), permissions (protected paths); source at openai/codex a3b6853 (command environment,ttyflag, output budget, hook payloads). - Cursor: Hooks, Terminal (
CURSOR_AGENT). - OpenCode: Permissions (V2), Network (
opencode service set env), V1 permissions (doom_loop);shellsource at anomalyco/opencode 2fde77c. - My hang test: 10 commands plus fixed variants, pipes versus pseudo-terminal, 8-second limit, Debian 13, git 2.47, npm 11 (21 Oct 2026).
- Previous articles: Stop Paying Your Agent to Wait, Compact, Clear or Hand Off?, How Much Should You Invest in Your AGENTS.md?
Tags: AI Coding, Claude Code, Cursor, Developer Tools, Software Development
If this was useful, subscribe to Agents in Practice for one practical note on AI coding agents a week. Helping your team adopt Claude Code or Codex safely and without the surprise bill? I consult on exactly that: andre-burgstahler-9n5.pages.dev.
This article was written with AI assistance and reviewed by the author.
Top comments (0)