Part 3. Preventing an AI Agent Runaway — Lessons From a Heartbeat Design Mistake
![]()
Series: Can AI Agents Actually Run Business Operations? — Part 3
OpenClaw has been getting a lot of attention as an AI agent platform since late 2025. In this series, I share what I learned from using OpenClaw on an actual client engagement — is it usable for real business work, and what should you watch out for from a security and reliability standpoint? I hope it helps people evaluating OpenClaw or similar AI agent platforms.
In this post (Part 3), I cover an incident where an AI agent started acting on its own in the middle of the night and spent $1.29 in 2 hours 36 minutes. The trigger was an empty config file — an instruction that means “do nothing” to a human, but reads as “decide for yourself and act” to an AI. This post covers that interpretation gap, how feedback loops can form, and what you need to design for if you plan to run an agent 24/7 unattended.
The previous post is Part 2: Comparing Three Models for Code Generation.
1. Incident Summary
| Metric | Value |
|---|---|
| Date/time | 2026/3/29 02:47 - 05:24 (night) |
| Session | heartbeat (30-minute alive-check) |
| Messages | 23 (user 2, assistant 21) |
| Token usage | 4.3M prompt, 2.4M total |
| Avg tokens/msg | 102.8K |
| Tool calls | 19 (exec 7, read 6, session_status 2, edit 2, memory_search 1, write 1) |
| Model | Gemini 3 Flash |
| Cost | $1.29 (about 43x the expected $0.03/day) |
In the middle of the night, the agent was reading files, running commands, and editing files on its own.
2. Runaway Mechanism — Why “Empty” Triggers a Runaway
Root cause
HEARTBEAT.md was empty (only a template comment), so the agent defaulted to finding work to do on its own.
Three design mistakes, in sequence
Design mistake 1: HEARTBEAT.md was empty
<!-- HEARTBEAT.md -->
<!-- Template comment only, no instructions -->
For humans, “empty” means “do nothing.” For an AI agent, it can read as “no explicit instruction — decide for yourself and act.”
Design mistake 2: AGENTS.md said “be proactive”
# AGENTS.md (at the time)
heartbeat:
- Do not just return HEARTBEAT_OK
- Proactively find tasks and execute them
- Check email/calendar/weather
Combined with the empty HEARTBEAT.md, the agent started doing this:
- Read HEARTBEAT.md → empty
- Follow “be proactive,” start looking for work
- Read SOUL.md, USER.md, MEMORY.md, daily memory (read x6)
- Run weather checks, git status, etc. (exec x7)
- Do memory maintenance (edit x2, write x1)
Design mistake 3: SOUL.md auto-triggered session_status
# SOUL.md (at the time)
- Run session_status after every response
- If over 200K tokens, run archive
This created a feedback loop.
Run session_status → context grows → archive process → grows more → session_status → ...
Result: 21 assistant responses, 2.4M tokens, $1.29.
3. Why Heartbeat Runs Inside the Main Session
Understanding OpenClaw’s session structure helps explain why this got expensive.
agent:main
├── :main ← persistent session (web UI / direct)
├── :cron:xxxxx ← isolated (disposable)
├── :telegram:xxxxx ← independent session
└── heartbeat ← runs inside the main session
Heartbeat is not an independent session — it runs inside the main session. That means every heartbeat tick re-loads the full main context (about 138K at the time) before handling the heartbeat task.
Every 30 minutes, a task that only needs to “return HEARTBEAT_OK” pays for 138K tokens of API cost.
| Channel | Cost |
|---|---|
| heartbeat (before stop) | $2.17 (over a few hours) |
| heartbeat (after stop) | $0.04 (no change afterward) |
4. Fixes
Immediate fixes
1. HEARTBEAT.md: empty → explicit “HEARTBEAT_OK”
HEARTBEAT_OK
I wrote the instruction explicitly as “return HEARTBEAT_OK.” The agent now reads the instruction, returns HEARTBEAT_OK, and exits.
2. AGENTS.md: removed all proactive instructions
# Fixed
heartbeat:
- Only do what HEARTBEAT.md says
- Never do anything else
3. SOUL.md: removed the auto session_status rule
I removed the “run session_status after every response” rule that was creating the feedback loop.
Structural fix: isolate the heartbeat agent
Even with these fixes, the core cost problem of heartbeat remains. As long as it runs inside the main session, it keeps re-sending the full context.
The structural fix was to move heartbeat into an independent agent running on a local LLM (Gemma3 4B).
{
"id": "heartbeat",
"model": "ollama/gemma3:4b",
"heartbeat": {
"every": "30m",
"lightContext": true,
"isolatedSession": true
}
}
lightContext: true: only reads HEARTBEAT.md (skips SOUL.md, etc.)isolatedSession: true: fresh session every time (no context accumulation)- Gemma3 4B: local LLM, API cost $0
Idle test
After stopping heartbeat and all crons and leaving the server idle for over 30 minutes:
Before idle: $8.97
After idle: $8.97 (no change)
Background API consumption was confirmed to be zero.
5. Control Challenges Specific to OpenClaw
The risk in --accept-risk
OpenClaw asks for --accept-risk for a reason: it runs with full host access. The agent can read/write files and run commands freely. If you leave this running 24/7 without understanding that, incidents like this become more likely.
Compared to Claude Code
| Aspect | Claude Code | OpenClaw |
|---|---|---|
| Sessions | Always one | Multiple concurrent |
| Context management | Auto-compaction | Manual or rule-based |
| Memory carry-over | MEMORY.md | MEMORY.md + vector DB |
| Scheduled execution | None | Cron + HEARTBEAT (two to watch) |
| Runaway risk | Low (human always present) | Higher (24/7 unattended) |
No built-in runaway detection
OpenClaw does not currently have a feature that detects when an agent is consuming an abnormal amount of tokens and stops it. You have to guard against this indirectly by setting a monthly spend cap on the API provider side.
6. Takeaways
- “Empty” is ambiguous as an instruction. An empty config does not mean “do nothing” to an AI. You need to write “do nothing” or “only return HEARTBEAT_OK” explicitly.
- Avoid feedback loops. Chains like session_status → archive → more tokens → session_status are easy to miss at design time, and often only surface in production.
- Design for nighttime safety first. This incident happened between 02:47 and 05:24, when no human was watching. That is exactly when the design needs to hold.
- Keep heartbeat as alive-check only. Move periodic business tasks to cron jobs. Heartbeat should do nothing beyond confirming the agent is alive.
- Be skeptical of “cost zero” claims. Even if OpenClaw says “isolated cron is cost-free,” if it runs on Gemini Flash, it is not really zero. To actually get to $0, you need a local LLM.
7. Runaway Prevention Checklist
- Write explicit instructions in HEARTBEAT.md (never leave it empty)
- Check AGENTS.md for any “proactive” or “find your own work” instructions
- Check SOUL.md for rules that could form an auto-loop
- Move heartbeat to a local LLM (Gemma3 or similar)
- Set
lightContext: true+isolatedSession: true - Set a monthly spend cap on the API provider
- Run an idle test (30+ minutes) and confirm no background cost growth
In Part 4, I plan to cover why giving one agent all the responsibilities creates problems, and the design and implementation of a 5-agent setup.