Detect when agents are stuck in repetitive action loops using a rolling 20-action window with escalating nudges — replan after 3 similar actions, explore after 5, force-done after 8.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add oimiragieo/agent-studio --skill behavioral-loop-detection --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Behavioral Loop Detection?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/oimiragieo-behavioral-loop-detection)More formats (shields.io, HTML) on the badges page.
---
name: behavioral-loop-detection
description: Detect when agents are stuck in repetitive action loops using a rolling 20-action window with escalating nudges — replan after 3 similar actions, explore after 5, force-done after 8.
version: 1.0.0
model: sonnet
invoked_by: both
user_invocable: false
tools: [Read, Write, Bash, TaskUpdate]
agents: [developer, qa, master-orchestrator, planner]
category: 'Validation & Quality'
tags: [loop-detection, agent-safety, escalation, monitoring, behavioral]
error_handling: strict
source: builtin
trust_score: 100
provenance_sha: 6c03c65351067b21
---
# Behavioral Loop Detection
## Overview
Detects when an agent is stuck in a repetitive action loop by maintaining a rolling 20-action
history window. Compares recent actions using normalized similarity scoring and applies
escalating interventions before the agent wastes tokens or blocks progress.
Designed for any agentic context — browser automation, file editing, API calls, or
multi-step orchestration — not tied to any specific tool type.
## When to Use
Invoke this skill when:
- An agent task has been running for more than 10 steps without clear progress
- The same tool is being called repeatedly with similar arguments
- A task appears stalled with no `TaskUpdate(completed)` after a long sequence
- You want proactive loop-prevention in a custom orchestrator
```javascript
Skill({ skill: 'behavioral-loop-detection' });
```
## The Iron Law
```
SAME ACTION × 3 = REPLAN
SAME ACTION × 5 = EXPLORE
SAME ACTION × 8 = FORCE-DONE
```
Never let an agent silently loop forever. Each repetition consumes tokens and produces no value.
## Action History Window
### Maintenance Rules
- Keep a rolling buffer of the **last 20 actions** (FIFO — drop oldest when full)
- Each entry in the buffer: `{ toolName, normalizedArgs, timestamp, stepIndex }`
- Reset the buffer only when a **fundamentally different** tool/action type appears
- Never reset on argument variation alone (e.g., same tool, different file path = still similar)
### Normalized Action Comparison
Two actions are **similar** when:
1. The `toolName` is identical, AND
2. The normalized argument string similarity score ≥ 0.75 (Jaccard over word tokens)
**Normalization rules (apply in order):**
1. Convert all values to lowercase
2. Strip file paths down to basename only (e.g., `/a/b/file.ts` → `file.ts`)
3. Remove timestamps, UUIDs (8-4-4-4-12 hex pattern), and numeric IDs
4. Sort object keys alphabetically before stringifying
5. Truncate the result to 200 characters
**Similarity scoring:**
```javascript
function jaccardSimilarity(a, b) {
const setA = new Set(a.split(/\s+/));
const setB = new Set(b.split(/\s+/));
const intersection = new Set([...setA].filter(x => setB.has(x)));
const union = new Set([...setA, ...setB]);
return intersection.size / union.size;
}
```
## Escalation Ladder
| Threshold | Trigger Condition | Intervention | Message to Agent |
| --------- | -------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Level 1 | 3 similar actions in a row | REPLAN nudge | "You have repeated a similar action 3 times. Stop and produce a revised plan before continuing." |
| Level 2 | 5 similar actions in a row | EXPLORE nudge | "You have repeated a similar action 5 times. The current approach is failing. Try a completely different tool or method." |
| Level 3 | 8 similar actions in a row | FORCE-DONE signal | "Loop limit reached (8 repetitions). Mark this task complete with partial results and explain what was not accomplished." |
- Escalation levels are cumulative: Level 2 fires at step 5 regardless of whether Level 1 fired
- After FORCE-DONE signal, call `TaskUpdate({ status: 'completed', metadata: { partial: true, loopDetected: true } })`
## Workflow
### Step 1: Initialize the Action Buffer
At the start of task execution or when this skill is loaded, create the buffer:
**Command:**
```javascript
const actionBuffer = {
window: [], // Array of last 20 action entries
maxSize: 20,
similarRunLength: 0, // Current streak of similar actions
lastNormalized: null, // Normalized args of last action
};
```
**Expected output:** In-memory buffer ready; no file I/O required unless persisting across sessions.
**Verify:** `actionBuffer.window.length === 0` on initialization.
### Step 2: Record Each Action
Before executing any tool call, append to the buffer:
**Command:**
```javascript
function recordAction(buffer, toolName, rawArgs) {
const normalized = normalizeArgs(toolName, rawArgs);
const entry = {
toolName,
normalizedArgs: normalized,
timestamp: Date.now(),
stepIndex: buffer.window.length,
};
if (buffer.window.length >= buffer.maxSize) {
buffer.window.shift(); // drop oldest
}
buffer.window.push(entry);
return entry;
}
```
**Expected output:** Buffer contains the new entry; length never exceeds 20.
**Verify:** `buffer.window.length <= 20` after each call.
### Step 3: Check Similarity Against Last Action
After recording, compute similarity and update the run-length counter:
**Command:**
```javascript
function checkSimilarity(buffer, currentEntry) {
if (!buffer.lastNormalized) {
buffer.lastNormalized = currentEntry.normalizedArgs;
buffer.similarRunLength = 1;
return { similar: false, runLength: 1 };
}
const score = jaccardSimilarity(buffer.lastNormalized, currentEntry.normalizedArgs);
if (score >= 0.75) {
buffer.similarRunLength += 1;
} else {
buffer.similarRunLength = 1;
buffer.lastNormalized = currentEntry.normalizedArgs;
}
return { similar: score >= 0.75, runLength: buffer.similarRunLength, score };
}
```
**Expected output:** `{ similar: boolean, runLength: number, score: number }`
**Verify:** `runLength` increments only when `score >= 0.75`.
### Step 4: Apply Escalation Rule
Evaluate the run-length and emit the appropriate intervention:
**Command:**
```javascript
function applyEscalation(runLength, taskId) {
if (runLength >= 8) {
console.error(`[loop-detection] FORCE-DONE: ${runLength} similar actions. Task ${taskId}`);
return {
level: 3,
action: 'force-done',
message:
'Loop limit reached (8 repetitions). Mark this task complete with partial results and explain what was not accomplished.',
};
}
if (runLength >= 5) {
console.error(`[loop-detection] EXPLORE: ${runLength} similar actions. Task ${taskId}`);
return {
level: 2,
action: 'explore',
message:
'You have repeated a similar action 5 times. The current approach is failing. Try a completely different tool or method.',
};
}
if (runLength >= 3) {
console.error(`[loop-detection] REPLAN: ${runLength} similar actions. Task ${taskId}`);
return {
level: 1,
action: 'replan',
message:
'You have repeated a similar action 3 times. Stop and produce a revised plan before continuing.',
};
}
return { level: 0, action: 'continue', message: null };
}
```
**Expected output:** `{ level: 0|1|2|3, action: string, message: string|null }`
**Verify:** `level === 3` triggers `TaskUpdate({ status: 'completed', metadata: { partial: true, loopDetected: true } })`.
### Step 5: Handle FORCE-DONE
When `level === 3`, the agent MUST stop and complete the task:
**Command:**
```javascript
// In the agent's task loop, when applyEscalation returns level 3:
TaskUpdate({
taskId: context.taskId,
status: 'completed',
metadata: {
summary:
'Task partially completed. Loop detected after 8 similar actions. ' + partialResultsSummary,
partial: true,
loopDetected: true,
loopDetails: {
runLength: runLength,
lastAction: buffer.lastNormalized,
bufferSnapshot: buffer.window.slice(-5),
},
},
});
```
**Expected output:** Task marked completed with `partial: true` metadata.
**Verify:** `TaskList()` shows the task as `completed`, not `in_progress`.
## Enforcement Hooks
Input validated against `schemas/input.schema.json` before execution.
Output contract defined in `schemas/output.schema.json`.
Pre-execute hook at `hooks/pre-execute.cjs` validates that `taskId` is provided.
Post-execute hook at `hooks/post-execute.cjs` emits a loop-detection event to `tool-events.jsonl`.
## Integration Points
- **master-orchestrator**: Integrate after each wave of agent spawns; check for stalled tasks
- **developer**: Active during file-editing loops (same file edited 3+ times without test progress)
- **qa**: Active during test-fix loops (same test file modified 3+ times without green)
- **planner**: Active during re-planning loops (same plan section rewritten 3+ times)
## Anti-Patterns
- Never reset the buffer on every tool call — that defeats detection
- Never apply force-done for `level < 3` — nudges are sufficient at levels 1 and 2
- Never compare raw unormalized args — path differences will mask real loops
- Never skip the similarity check when `toolName` differs — tool variety alone ≠ not looping
## Memory Protocol (MANDATORY)
**Before starting:** Read `.claude/context/memory/learnings.md` for previously detected loop patterns.
**After completing:** If a loop was detected and force-done triggered, append to `.claude/context/memory/learnings.md`:
```
## Behavioral Loop Detection — [date]
- Task [taskId]: [toolName] looped [N] times. Final normalized args: [args].
- Root cause hypothesis: [explain why the agent got stuck]
- Recommendation: [what to change in the task prompt or approach]
```
**After issues:** Append to `.claude/context/memory/issues.md` if force-done fires more than twice in a session.
## Related Skills
- `verification-before-completion` — Pre-completion gates that prevent false success
- `judge-verification` — Independent LLM judge that verifies task completion
- `error-recovery-escalation` — 5-level error recovery before force-done
- `context-compressor` — Compress context when loop detection fires repeatedly
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!