Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 5 min read

My coding agent burned tokens in a loop. So I built it a brake.

My coding agent burned tokens in a loop. So I built it a brake. Here's a failure mode every heavy user of coding agents has seen: the agent gets stuck. It re-reads the same file. It retries the same failing test with c

My coding agent burned tokens in a loop. So I built it a brake.

Here's a failure mode every heavy user of coding agents has seen: the agent gets stuck. It re-reads the same file. It retries the same failing test with cosmetically different commands. It makes call after call, each one plausible, none of them progress β€” and every call costs tokens.

When I went looking for tooling, I found plenty of cost dashboards: ccusage, CodeBurn, and friends. They're good tools. But they all share one property β€” they tell you what a session cost after it ended. That's an autopsy, not a safety system.

I wanted something that intervenes live, inside the session, while there's still something to save. So I built DejaLoop, an open-source Claude Code plugin. This post is about the interesting engineering problem underneath it: how do you tell a stuck agent from a busy one, in real time, cheaply enough to run on every session?

Why fingerprints aren't enough

The naive approach is fingerprinting: hash each tool call (tool name + inputs), and flag repeats. It works β€” for the dumbest loops. An agent reading the same file six times in a row is trivially detectable.

But the expensive loops are smarter than that. Consider this trajectory:

pytest -q            β†’ FAILED
pytest tests/        β†’ FAILED
python -m pytest     β†’ FAILED
pytest -q -x         β†’ FAILED
pytest tests/ -q     β†’ FAILED

Six calls, six different fingerprints, zero edits in between. A fingerprint detector sees variety and concludes the agent is exploring. A human sees an agent that has retried the same failing strategy six ways and changed nothing. The difference between those two readings is semantics: you have to judge whether the trajectory constitutes progress, not whether the calls are identical.

The opposite failure matters just as much. A TDD session β€” edit, test fails, edit, test fails, edit, test passes β€” looks like a failing loop to naive detection. Flag that, and your guard becomes the most annoying tool on someone's machine. Any detector in this space lives or dies on its false-alarm rate.

The design: three layers

DejaLoop runs inside Claude Code via its hooks system (PreToolUse, PostToolUse, Stop). Every tool call flows through three layers:

Layer 1 β€” Deterministic checks (free, instant)

Fingerprint every call. Same call 3Γ— consecutively, or Bash failing 3Γ— in a row, is a strong local signal that costs nothing to compute. This layer also maintains the trajectory features the judge needs: repetition streaks, failure streaks, call-similarity, steps since the last edit, whether any verification (test/build/lint) has run.

Layer 2 β€” A semantic judge

Every few tool calls β€” or immediately when layer 1 smells trouble β€” the recent trajectory is scored by a judge answering typed questions: Is the agent stuck? Is it repeating a failed strategy? How would you rate its progress?

The judge is pluggable, and there are two real options:

  • A zero-cost local judge that ships built in. No API key, no network, works offline. It's a purpose-built scorer for this one narrow judgment β€” not a general model.
  • TypeSafe Jev (bring your own key). Jev is a dedicated decision model that returns probabilities for typed questions, which is exactly the shape this problem wants. It costs about $0.0002 per typical session at current pricing β€” a $5 credit covers tens of thousands of sessions.

A general lesson here: I did not need a general-purpose decision model. I needed one judgment, made cheaply and often. Narrow judges are dramatically easier to build, test, and afford.

Layer 3 β€” Policy: escalate, don't amputate

A "chance it's stuck" score isn't an action. The policy layer maps it to escalating interventions:

Signal Action
< 0.50 Continue silently
0.50–0.75 Warn β€” inject a message into the session
0.75–0.90 Replan β€” deny the next tool call, feed the reason back to the agent
> 0.90 Halt & ask β€” stop and let the human decide

There's also a separate check on the Stop event: if the agent edited files but never ran a test, build, or lint, its "done" claim gets blocked and it's sent back to verify. Anyone who has reviewed agent output knows why that one exists.

Two safety properties that are not optional

Fail-open. If the judge times out, if the plugin throws, if anything at all goes wrong in the guard β€” the session proceeds, untouched. A safety tool that can brick your session is a liability, not a safety tool. Every exception path in the hook handler resolves to "allow."

Shadow mode by default. New installs only log what the guard would have done. Blocking is opt-in, after you've read the logs and trust it. This is how security tooling earns adoption, and it's also how I get real-world false-alarm data without hurting anyone's work.

The benchmark (and its limits)

I built 8 labeled scenarios β€” 4 stuck, 4 healthy β€” with the healthy set deliberately including the hard negatives: a TDD red-green cycle, varied exploration across files, and legitimate repeated test runs. A scenario counts as "caught" if the guard intervenes; a healthy scenario counts as a false alarm if it intervenes at all.

Judge Stuck caught False alarms
Deterministic rules only 1/4 0/4
Local judge (zero-cost) 4/4 0/4

Rules alone miss exactly the cases I built this for β€” the varied-command failing loop, the alternating two-strategy loop, the near-identical search thrash. The local judge caught all of them and left every healthy pattern alone.

Now the honesty paragraph, because this audience deserves it: 8 scenarios is a small set, and they're simulated. Claude Code hooks are just JSON on stdin, which makes the whole pipeline testable offline with scripted payloads β€” that's how these numbers were produced β€” but real sessions are longer, messier, and weirder than anything I scripted. The benchmark code is in the repo; the real validation will be shadow-mode logs from actual use, and I'd rather tell you that up front than dress up a small number as a big one.

What I'd do differently / what's next

  • Calibrate against real Jev. The Jev client is implemented against the documented API, but its thresholds (0.50 / 0.75 / 0.90) are uncalibrated against real Jev probabilities. That's the first job once it's running with a live key on real sessions.
  • More agents. v1 is Claude Code only, because that's where the hooks are documented and where I work. A Codex adapter is the obvious next step.
  • Learn from the ledger. Every intervention is logged locally (~/.dejaloop/ledger.jsonl in the current build). Aggregate patterns β€” with consent, never by default β€” are how the judge gets smarter than my 8 scenarios.

Try it

DejaLoop is MIT-licensed, Python standard library only, nothing hosted, no telemetry, bring-your-own-key only if you opt into Jev.

Repo: https://github.com/Syed-Amaan-ullah/dejaloop

If you've watched an agent loop your money away, I want to hear the story β€” and if you run it in shadow mode, I want to hear what it would have caught. That's the data that makes v2 real.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.