<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ralph Loop | Ralph Loop Blog</title><description>A long-running AI agent loop that codes for days.</description><link>https://ralphloop.sh/</link><language>en</language><item><title>How to Inspect and Debug Inside an AI Agent Sandbox</title><link>https://ralphloop.sh/blog/debug-inside-an-agent-sandbox/</link><guid isPermaLink="true">https://ralphloop.sh/blog/debug-inside-an-agent-sandbox/</guid><description>A sandbox is not a black box. Here is how to find the running microVM, shell into it with sbx exec, reproduce the failure by hand, and read the network log.

</description><pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When an autonomous agent gets stuck, do not guess from the outside. Open a shell in the running sandbox and look. A Ralph sandbox is a normal container once you are inside it, so you find it with &lt;code dir=&quot;auto&quot;&gt;sbx ls&lt;/code&gt;, shell in with &lt;code dir=&quot;auto&quot;&gt;sbx exec -it &amp;#x3C;name&gt; bash&lt;/code&gt;, and debug the agent the same way you debug any other process. This is the field guide to inspecting and debugging an AI agent sandbox: locating the microVM, getting an interactive shell, reproducing the failure by hand, and reading the network log.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;when-to-get-inside-the-sandbox&quot;&gt;When to get inside the sandbox&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A loop driven by &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Ralph&lt;/a&gt; does not fail silently. The agent emits an explicit signal when it cannot proceed: &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; when it needs human help (exit code &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt;) or &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt; when it needs a decision (exit code &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt;). A run that hits the iteration cap exits with &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;, and a clean finish exits with &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;. The promise text tells you what the agent thinks went wrong. It does not always tell you why.&lt;/p&gt;
&lt;p&gt;That gap is what a shell closes. You get inside when a task keeps failing the same way, when an install hangs, when tests pass for you but not for the agent, or when you just want to see the state the agent left behind. Because the agent runs in bypass-permissions mode (see &lt;a href=&quot;https://ralphloop.sh/blog/bypass-permissions-yolo-mode-safely/&quot;&gt;running agents in YOLO mode safely&lt;/a&gt; for why that is fine inside a microVM), the inside of the sandbox holds the real evidence: the working tree, the logs, the installed packages, and the exact environment the agent saw.&lt;/p&gt;
&lt;p&gt;The sandbox is the same boundary described in the pillar guide, &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-agents-in-docker-sandboxes-safely/&quot;&gt;how to run AI coding agents in Docker Sandboxes safely&lt;/a&gt;. Debugging it does not weaken that boundary. You are stepping into the contained space on purpose, doing your work, and stepping back out.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;find-the-sandbox-you-need-to-debug&quot;&gt;Find the sandbox you need to debug&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;You cannot shell into a sandbox you cannot name. Ralph names every sandbox deterministically, so the name is predictable, but you still want to confirm what is actually running.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;list-everything-with-sbx-ls&quot;&gt;List everything with sbx ls&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Start with the list:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This prints every sandbox on the machine. The one you want follows the Ralph format:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;current-dir&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A project at &lt;code dir=&quot;auto&quot;&gt;/Users/me/Work/My App&lt;/code&gt; running Claude shows up as &lt;code dir=&quot;auto&quot;&gt;ralph-claude-my-app-a1b2c3d4&lt;/code&gt;. If you ran more than one agent against the same project, you will see one sandbox per agent, because the agent slug is part of the name. That is deliberate, so a Claude run and a Codex run never stomp on each other’s state.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;ask-ralph-for-the-exact-name&quot;&gt;Ask Ralph for the exact name&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;You do not have to read the name off a list. Ralph prints it on the &lt;code dir=&quot;auto&quot;&gt;Starting Ralph&lt;/code&gt; line at launch, and you can ask for it any time:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;--agent&lt;/code&gt; flag targets a specific agent’s sandbox for the same project. Capture the value once and reuse it for every &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt; command below, so you are not retyping a hash.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;decode-the-deterministic-name&quot;&gt;Decode the deterministic name&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The name carries information, which helps when you have several sandboxes open. The pieces are documented in the repo’s sandbox naming notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;agent&gt;&lt;/code&gt; is the agent slug, lowercased: &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;current-dir&gt;&lt;/code&gt; is the basename of the project directory, sanitized to &lt;code dir=&quot;auto&quot;&gt;[a-z0-9-]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;hash8&gt;&lt;/code&gt; is the first eight hex characters of &lt;code dir=&quot;auto&quot;&gt;sha256&lt;/code&gt; of the absolute project path.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The path hash is the part that prevents collisions. Two directories both named &lt;code dir=&quot;auto&quot;&gt;app&lt;/code&gt;, one at &lt;code dir=&quot;auto&quot;&gt;~/Work/app&lt;/code&gt; and one at &lt;code dir=&quot;auto&quot;&gt;/tmp/app&lt;/code&gt;, get different hashes and therefore different sandboxes. So when &lt;code dir=&quot;auto&quot;&gt;sbx ls&lt;/code&gt; shows two &lt;code dir=&quot;auto&quot;&gt;ralph-claude-app-...&lt;/code&gt; entries, the trailing hash is how you tell them apart.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;shell-into-the-sandbox-with-sbx-exec&quot;&gt;Shell into the sandbox with sbx exec&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is the command you came for. Open an interactive shell:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;-it&lt;/code&gt; flags allocate an interactive terminal. Now you are inside the microVM with full control, the same as any container. You can install packages, run the test suite, read files, and poke at the environment the agent ran in.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;navigate-to-the-project-path&quot;&gt;Navigate to the project path&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The project is mounted at the same absolute path it has on your host. If your project lives at &lt;code dir=&quot;auto&quot;&gt;/Users/you/Work/my-app&lt;/code&gt; on the host, it lives at &lt;code dir=&quot;auto&quot;&gt;/Users/you/Work/my-app&lt;/code&gt; inside the sandbox too. That is on purpose, so tooling, config files, and lockfiles resolve identically and the agent never trips over a path difference.&lt;/p&gt;
&lt;p&gt;So the first move after shelling in is usually:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/Users/you/Work/my-app&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now your working directory matches the agent’s. Any relative path the agent used resolves the same way for you.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;run-the-agent-cli-by-hand&quot;&gt;Run the agent CLI by hand&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The most useful trick is driving the agent yourself, interactively, from inside the sandbox. You see exactly what it sees, including the permission mode and the network reality:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/Users/you/Work/my-app&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;claude&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Swap &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt; for &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt; depending on which agent that sandbox was built for. Running the CLI by hand is how you tell apart a prompt problem from an environment problem. If the agent stalls the same way interactively, the issue is the task, the prompt, or the repo state. If it works by hand but fails in the loop, the issue is in how the loop set things up, and you can compare the two.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;reattach-a-stopped-sandbox-with-sbx-run&quot;&gt;Reattach a stopped sandbox with sbx run&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;sbx exec&lt;/code&gt; works while the sandbox is running. To reattach the exact sandbox Ralph uses, for a manual login or a longer debugging session, use the attach form of &lt;code dir=&quot;auto&quot;&gt;sbx run&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;There is one sharp edge here, and it trips people up, so state it plainly. The create form, &lt;code dir=&quot;auto&quot;&gt;sbx run --name &amp;#x3C;name&gt; &amp;#x3C;agent&gt; .&lt;/code&gt;, is create-only. Passing &lt;code dir=&quot;auto&quot;&gt;--name&lt;/code&gt; for a sandbox that already exists fails with an error that says &lt;code dir=&quot;auto&quot;&gt;--name can only be used when creating a new sandbox&lt;/code&gt;. So you use the &lt;code dir=&quot;auto&quot;&gt;--name&lt;/code&gt; create form exactly once, before the sandbox exists, and the bare &lt;code dir=&quot;auto&quot;&gt;sbx run &amp;#x3C;name&gt;&lt;/code&gt; attach form every time after.&lt;/p&gt;
&lt;p&gt;Ralph handles this branching for you. It probes &lt;code dir=&quot;auto&quot;&gt;sbx ls&lt;/code&gt; before every iteration and emits the create form when the sandbox is missing and the attach form when it exists. That probe is also why a loop self-heals: if you &lt;code dir=&quot;auto&quot;&gt;sbx rm&lt;/code&gt; a sandbox mid-run while debugging, the next iteration simply creates it again. Re-running &lt;code dir=&quot;auto&quot;&gt;./ralph.sh&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --login&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --ports&lt;/code&gt; is therefore safe at any point.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;debug-inside-logs-reproduction-and-missing-tools&quot;&gt;Debug inside: logs, reproduction, and missing tools&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Once you have a shell, the sandbox is just an environment. Three moves cover most debugging sessions.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;read-the-agents-own-memory-layer&quot;&gt;Read the agent’s own memory layer&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Ralph keeps its state on disk, not in chat history, because the filesystem and git history are the memory layer. That means everything the agent knows is sitting in the project, ready to read:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.agent/logs/LOG.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.agent/history/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.agent/tasks.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--oneline&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt; is the running log. &lt;code dir=&quot;auto&quot;&gt;.agent/history/&lt;/code&gt; holds per-iteration logs, so you can see what happened on the iteration that broke. &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt; is the task lookup table, and the git log shows what the agent actually committed. Reading these in order usually tells you whether the agent thrashed on one task, committed something broken, or stalled waiting on a decision. For the wider picture of making a run auditable from the outside, including live output and screenshots, see &lt;a href=&quot;https://ralphloop.sh/blog/observability-for-ai-coding-agents/&quot;&gt;observability for autonomous coding agents&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;reproduce-the-failing-command&quot;&gt;Reproduce the failing command&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The fastest way to understand a failure is to run the thing that failed. Inside the sandbox, at the project path, run the verification stack the loop assumes:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tsc&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--noEmit&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;eslint&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;playwright&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If the agent reported a failing test, run that test directly and read the full output without the loop’s framing. The repo mantra is “if you didn’t test it, it doesn’t work,” and the same applies to your debugging: reproduce the exact failure before you theorize about it. Because you are inside the same microVM the agent used, a failure you reproduce here is the real failure, not a near miss caused by a different machine.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;install-missing-tooling&quot;&gt;Install missing tooling&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Sometimes the agent is blocked because a binary it needs is not installed. You have full root-style control inside the sandbox, so install it and confirm the theory:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;apt-get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span&gt;apt-get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-y&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;package&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If an install itself fails, that is almost never a package manager bug. It is the network gate, which is the next section.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;debug-the-network-with-sbx-policy-log&quot;&gt;Debug the network with sbx policy log&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Docker Sandboxes are deny-by-default on outbound HTTP and HTTPS. So the single most common “the agent is stuck” cause is a blocked connection: &lt;code dir=&quot;auto&quot;&gt;npm install&lt;/code&gt; fails, an API call is refused, or a package download hangs. The gate is doing its job, but you need to see what it blocked.&lt;/p&gt;
&lt;p&gt;Read the connection history:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This shows which outbound connections the sandbox attempted and which the policy allowed or denied. List the active rules to see what is currently permitted:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When the log shows a denied host the task legitimately needs, allow that specific domain rather than opening everything:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;registry.npmjs.org&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Allow several at once with a comma-separated list, which is the usual case when an install is blocked:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;*.npmjs.org,*.pypi.org,files.pythonhosted.org,github.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Changes take effect immediately and persist across sandbox restarts. Resist the urge to debug with the full-open &lt;code dir=&quot;auto&quot;&gt;&quot;**&quot;&lt;/code&gt; rule and leave it there, because that throws away half the boundary. The disciplined way to build an allowlist that lets installs through while keeping exfiltration out is covered in &lt;a href=&quot;https://ralphloop.sh/blog/ai-agent-sandbox-network-policies/&quot;&gt;network policies for AI agent sandboxes&lt;/a&gt;. The primary reference for the policy engine is the &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/&quot;&gt;Docker Sandboxes documentation&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-map-of-debug-entry-points&quot;&gt;A map of debug entry points&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;There are only a handful of ways into and out of a sandbox, and they compose cleanly. This is the whole surface you work with.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart LR
  Host[&quot;Your machine (host)&quot;]
  subgraph Entry[&quot;Debug entry points&quot;]
    List[&quot;sbx ls: find the name&quot;]
    Shell[&quot;sbx exec -it name bash: interactive shell&quot;]
    Attach[&quot;sbx run name: reattach the loop sandbox&quot;]
    NetLog[&quot;sbx policy log: connection history&quot;]
    Stop[&quot;sbx stop name: end the session&quot;]
  end
  subgraph Sandbox[&quot;Sandbox: ralph-claude-my-app-a1b2c3d4&quot;]
    Proj[&quot;Project at the same path as the host&quot;]
    Logs[&quot;.agent/logs, .agent/history, git log&quot;]
    CLI[&quot;Agent CLI you can run by hand&quot;]
    Net[&quot;Network gate: deny-by-default&quot;]
  end
  Host --&gt; List
  Host --&gt; Shell
  Host --&gt; Attach
  Host --&gt; NetLog
  Host --&gt; Stop
  Shell --&gt; Proj
  Shell --&gt; Logs
  Shell --&gt; CLI
  Attach --&gt; CLI
  NetLog --&gt; Net&lt;/pre&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;sbx ls&lt;/code&gt; gives you the name. &lt;code dir=&quot;auto&quot;&gt;sbx exec&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;sbx run&lt;/code&gt; put you inside. &lt;code dir=&quot;auto&quot;&gt;sbx policy log&lt;/code&gt; explains network failures from the outside. &lt;code dir=&quot;auto&quot;&gt;sbx stop&lt;/code&gt; ends the session. You rarely need anything else.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;clean-up-with-sbx-stop&quot;&gt;Clean up with sbx stop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When you finish a manual debugging session, stop the sandbox you were poking at:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;stop&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Stopping targets only that one name. Sandboxes you started for other agents in the same project are left alone. Ralph runs this same command for you through its exit trap when a loop ends, by normal exit, by a double Ctrl+C, or by any path that fires the trap, and the cleanup is guarded so it runs at most once.&lt;/p&gt;
&lt;p&gt;Stopping is not deleting. A stopped sandbox can be reattached later with &lt;code dir=&quot;auto&quot;&gt;sbx run &amp;#x3C;name&gt;&lt;/code&gt;, which is handy when you want to come back to the same state. When you are truly done and want the microVM gone, remove it explicitly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;rm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt; is not installed at all, Ralph fails fast before any of this with a clear pointer to the &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/get-started/&quot;&gt;Docker Sandboxes getting-started guide&lt;/a&gt; rather than a confusing downstream error.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-debugging-session-end-to-end&quot;&gt;A debugging session, end to end&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Putting the moves in order, here is what a real session looks like when a loop reports &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;First, get the name and confirm the sandbox is there:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Shell in and go to the project path:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/Users/you/Work/my-app&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Read what the agent recorded, then reproduce the failure it hit:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.agent/logs/LOG.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--oneline&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If the failure is a blocked install, check the network log from your host in another terminal, allow the domain, and retry the install inside the sandbox:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;registry.npmjs.org&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If the failure is a task or prompt problem, run the agent CLI by hand and watch it work the task interactively. When you understand the cause, exit the shell, fix the task spec or the prompt on the host, and let the loop pick up where it left off. Ralph re-probes the sandbox on the next iteration, so your fix runs in the same contained environment.&lt;/p&gt;
&lt;p&gt;This is the Ralph technique applied to its own failures. The loop, in the tradition of Geoffrey Huntley’s original &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;Ralph writeup&lt;/a&gt;, keeps its state on disk so you can always step in, read the evidence, reproduce the problem, and step back out. The sandbox is contained, so debugging it costs you nothing on the host. The worst case stays cheap, and the agent gets back to work.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;How do I open a shell inside a running AI agent sandbox?&lt;/h3&gt; &lt;p&gt;Find the sandbox name with sbx ls or with ./ralph.sh --print-name, then run sbx exec with the interactive flags and bash, for example sbx exec -it ralph-claude-my-app-a1b2c3d4 bash. You now have full control inside the microVM, so you can navigate to the project path, run tests, install packages, and inspect files like any container.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What is the difference between sbx exec and sbx run for debugging?&lt;/h3&gt; &lt;p&gt;Use sbx exec with -it and bash to open an interactive shell in a sandbox that is already running. Use sbx run with the name to reattach the sandbox Ralph uses for a manual login or a longer session. The create form sbx run --name name agent dot is create-only and only works the first time, before the sandbox exists.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why does an install or API call fail inside the sandbox?&lt;/h3&gt; &lt;p&gt;Docker Sandboxes block outbound network by default. Run sbx policy log to see which connections were denied, then allow the specific domain with sbx policy allow network using the sandbox name. Allow only the hosts the task needs, such as registry.npmjs.org or the comma-separated npm, PyPI, and GitHub domains, rather than opening all traffic.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Where does the agent store the logs I should read when it gets stuck?&lt;/h3&gt; &lt;p&gt;Ralph keeps its state on disk inside the project, not in chat history. Read .agent/logs/LOG.md for the running log, list .agent/history for per-iteration logs, check .agent/tasks.json for the task table, and run git log to see what was committed. Together these usually show whether the agent thrashed on a task or stalled waiting on a decision.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I clean up a sandbox after debugging?&lt;/h3&gt; &lt;p&gt;Run sbx stop with the sandbox name to stop only that sandbox; sandboxes for other agents are untouched. Stopping is not deleting, so you can reattach later with sbx run and the name. When you want the microVM gone for good, remove it with sbx rm. Ralph also stops its own sandbox automatically through its exit trap when a loop ends.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>sandboxing</category></item><item><title>Network Policies for AI Agent Sandboxes</title><link>https://ralphloop.sh/blog/ai-agent-sandbox-network-policies/</link><guid isPermaLink="true">https://ralphloop.sh/blog/ai-agent-sandbox-network-policies/</guid><description>A sandboxed agent should start with no outbound network and earn each domain it reaches. Here is how to allowlist exactly what a task needs with sbx policy, so package installs work and exfiltration does not.

</description><pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An agent needs some network and not all of it. A Docker Sandbox blocks outbound HTTP and HTTPS by default, so the agent starts with zero reach and you grant exactly the domains a task needs with &lt;code dir=&quot;auto&quot;&gt;sbx policy allow network&lt;/code&gt;. That is the whole &lt;code dir=&quot;auto&quot;&gt;ai agent sandbox network policy&lt;/code&gt; model: deny everything, then allowlist the package registries, code hosts, and APIs the work requires, and nothing else. This guide shows the commands, the matching rules, and a practical allowlist you can paste for a normal project. It is the network half of the &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-agents-in-docker-sandboxes-safely/&quot;&gt;Docker Sandboxes pillar guide&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-a-sandbox-blocks-the-network-by-default&quot;&gt;Why a sandbox blocks the network by default&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Filesystem isolation is only half of a sandbox boundary. The other half is the network. An agent with unrestricted outbound access can fetch arbitrary code, run a &lt;code dir=&quot;auto&quot;&gt;curl | bash&lt;/code&gt; it found in a README, and in the worst case send your source or secrets to a host you never approved. That last risk is the one that should keep you honest. The agent does not need to be malicious. It is a probabilistic system running shell commands, and a shell command that uploads a file looks exactly like a shell command that downloads one.&lt;/p&gt;
&lt;p&gt;So Docker Sandboxes ship with a deny-by-default network posture. All outbound HTTP and HTTPS to domains not on the allowlist is blocked, and all non-HTTP protocols (raw TCP, UDP including DNS, and ICMP) are blocked at the network layer, as documented in the &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/security/defaults/&quot;&gt;default security posture reference&lt;/a&gt;. Traffic to private IP ranges, loopback, and link-local addresses is blocked too, so the agent cannot reach back into your LAN or a cloud metadata endpoint.&lt;/p&gt;
&lt;p&gt;The practical symptom is that &lt;code dir=&quot;auto&quot;&gt;npm install&lt;/code&gt; fails, a &lt;code dir=&quot;auto&quot;&gt;pip install&lt;/code&gt; hangs, or an API call is refused inside the sandbox. That is not a bug. That is the gate doing its job. You fix it by allowlisting the specific domains the task needs, not by opening everything. The point of the sandbox was to make the agent fearless inside a contained blast radius, and a wide-open network puts a hole in the wall you built.&lt;/p&gt;
&lt;p&gt;Docker offers three starting policies, described in the &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/security/policy/&quot;&gt;policies documentation&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open: all outbound traffic allowed, equivalent to a global allow-all rule. No restrictions.&lt;/li&gt;
&lt;li&gt;Balanced: default deny with a baseline allowlist covering common AI provider APIs, package managers, code hosts, container registries, and cloud services. You extend it with &lt;code dir=&quot;auto&quot;&gt;sbx policy allow&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Locked Down: everything blocked, including model provider APIs like &lt;code dir=&quot;auto&quot;&gt;api.anthropic.com&lt;/code&gt;. You allow each host explicitly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most Ralph users sit on Balanced or stricter. If you chose Balanced, package installs from the big registries often work out of the box and you only add what is missing. If you chose Locked Down, you allow even the model API the agent talks to. Check which one you are on before debugging a blocked request.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-a-request-flows-through-the-network-gate&quot;&gt;How a request flows through the network gate&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Every outbound connection the agent makes hits the policy gate before it leaves the sandbox. The gate compares the destination against your allow and deny rules, and if nothing matches, the default policy decides. On a deny-by-default install, nothing matching means blocked.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
  Agent[&quot;Agent in YOLO mode&quot;] --&gt;|&quot;outbound request to a host&quot;| Gate{&quot;Network policy gate&quot;}
  Gate --&gt;|&quot;matches a deny rule&quot;| Block[&quot;Blocked: connection refused&quot;]
  Gate --&gt;|&quot;matches an allow rule&quot;| Pass[&quot;Allowed: leaves the sandbox&quot;]
  Gate --&gt;|&quot;matches nothing (deny-by-default)&quot;| Block
  Pass --&gt; Net[&quot;Internet: only allowlisted hosts&quot;]
  Block --&gt; Log[&quot;Recorded in sbx policy log&quot;]&lt;/pre&gt;
&lt;p&gt;The diagram is the mental model. The agent never talks to the internet directly. It talks to a gate, and the gate enforces a list you control from the host. You set that list once for a project and stop thinking about individual requests.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-to-allow-a-domain-with-sbx-policy&quot;&gt;How to allow a domain with sbx policy&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Grant access with &lt;code dir=&quot;auto&quot;&gt;sbx policy&lt;/code&gt;. Changes take effect immediately and persist across sandbox restarts. You target a single sandbox by passing its name, or every sandbox on the machine with the global flag &lt;code dir=&quot;auto&quot;&gt;-g&lt;/code&gt;. Get the name from &lt;code dir=&quot;auto&quot;&gt;sbx ls&lt;/code&gt; or from Ralph directly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Ralph builds a deterministic sandbox name in the form &lt;code dir=&quot;auto&quot;&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;current-dir&gt;-&amp;#x3C;hash8&gt;&lt;/code&gt;, so a project at &lt;code dir=&quot;auto&quot;&gt;/Users/me/Work/my-app&lt;/code&gt; running Claude becomes &lt;code dir=&quot;auto&quot;&gt;ralph-claude-my-app-a1b2c3d4&lt;/code&gt;. Use that string wherever a command below shows a name.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;allow-one-domain-for-one-sandbox&quot;&gt;Allow one domain for one sandbox&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The base command takes a sandbox name and a domain:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;api.example.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That sandbox can now reach &lt;code dir=&quot;auto&quot;&gt;api.example.com&lt;/code&gt;. Nothing else changed, and no other sandbox was affected.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;allow-several-domains-at-once&quot;&gt;Allow several domains at once&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Pass a comma-separated list instead of a single host. This is the common case when a package install is blocked and you want to fix it in one command:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;*.npmjs.org,*.pypi.org,files.pythonhosted.org,github.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Quote the list so your shell does not try to glob the asterisks. Each entry is matched independently against outbound requests.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;apply-a-rule-to-every-sandbox-with--g&quot;&gt;Apply a rule to every sandbox with -g&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;When a domain is something every project on your machine needs (a model provider API, your internal package mirror), set it globally:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-g&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;api.anthropic.com,*.npmjs.org,*.pypi.org&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Global rules are convenient and also a bigger commitment, because they widen the network for sandboxes you have not thought about yet. Prefer per-sandbox rules for anything task-specific and reserve &lt;code dir=&quot;auto&quot;&gt;-g&lt;/code&gt; for genuinely universal hosts.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-allow-all-escape-hatch&quot;&gt;The allow-all escape hatch&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;You can open a single sandbox completely with the double-asterisk wildcard, which opts it out of network filtering:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Use &lt;code dir=&quot;auto&quot;&gt;&quot;**&quot;&lt;/code&gt; sparingly and on purpose. It is the right tool when you genuinely cannot enumerate the domains a task needs and you accept the tradeoff for that one sandbox. It is the wrong default, because it throws away the network half of the boundary you set up the sandbox to get. If you find yourself reaching for &lt;code dir=&quot;auto&quot;&gt;&quot;**&quot;&lt;/code&gt; on every run, that is a signal to capture the real allowlist instead, which the practical section below walks through.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;inspect-log-and-revoke&quot;&gt;Inspect, log, and revoke&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;You do not have to guess what the gate is doing. List the active rules:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Watch what the agent actually tried to reach, which is the fastest way to turn a blocked install into a precise allow rule:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;And remove access with the mirror of &lt;code dir=&quot;auto&quot;&gt;allow&lt;/code&gt;. A deny rule blocks a host even inside a broader allow:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;deny&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ads.example.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;deny&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-g&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;telemetry.example.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The workflow that scales is: run the task, read &lt;code dir=&quot;auto&quot;&gt;sbx policy log&lt;/code&gt; for the refused hosts, add a tight allow rule for the ones the task legitimately needs, and re-run. After two iterations you usually have the exact allowlist a project needs, and you never touched &lt;code dir=&quot;auto&quot;&gt;&quot;**&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-domain-matching-works&quot;&gt;How domain matching works&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The matching rules are simple once you have seen them, and getting them wrong is the usual reason an allow rule appears to do nothing. The &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/security/policy/&quot;&gt;policies reference&lt;/a&gt; is the source of truth, and here is the working summary.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;exact-domains-versus-wildcard-subdomains&quot;&gt;Exact domains versus wildcard subdomains&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A bare domain matches only itself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;example.com&lt;/code&gt; matches &lt;code dir=&quot;auto&quot;&gt;example.com&lt;/code&gt; on any port. It does NOT match &lt;code dir=&quot;auto&quot;&gt;api.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;*.example.com&lt;/code&gt; matches subdomains like &lt;code dir=&quot;auto&quot;&gt;api.example.com&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;cdn.example.com&lt;/code&gt;. It does NOT match the bare &lt;code dir=&quot;auto&quot;&gt;example.com&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To cover both the root and its subdomains, list both patterns:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;example.com,*.example.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This trips people up constantly. A registry that serves its index from the apex domain and its tarballs from a CDN subdomain needs both entries, or installs stall halfway through.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;ports&quot;&gt;Ports&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Append a port to scope a rule to one port:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;example.com:443&lt;/code&gt; matches requests to &lt;code dir=&quot;auto&quot;&gt;example.com&lt;/code&gt; on port 443, the default HTTPS port.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;*.example.com:443&lt;/code&gt; matches any subdomain on port 443.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;example.com&lt;/code&gt; with no port matches any port.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Non-HTTP TCP traffic such as SSH is blocked by default and can be allowed by adding a rule for the destination IP address and port, for example &lt;code dir=&quot;auto&quot;&gt;sbx policy allow network -g &quot;10.1.2.3:22&quot;&lt;/code&gt;. UDP and ICMP are blocked at the network layer and cannot be unblocked with a policy rule, so do not expect to ping out or run a custom UDP protocol from inside the sandbox.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;most-specific-wins-and-deny-beats-allow&quot;&gt;Most specific wins, and deny beats allow&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;When several patterns could match one request, the most specific pattern decides. The order runs from an exact hostname with a port, to an exact hostname on any port, to wildcard patterns (longest match first), to catch-all wildcards, and finally the default policy. That specificity is what lets you block a broad pattern while allowing a narrow exception. You can deny &lt;code dir=&quot;auto&quot;&gt;example.com&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;*.example.com&lt;/code&gt;, then allow &lt;code dir=&quot;auto&quot;&gt;api.example.com:443&lt;/code&gt;, and only that one host and port gets through.&lt;/p&gt;
&lt;p&gt;For the case people ask about most: when an allow rule and a deny rule both match a request at the same specificity, the deny rule wins. Deny is the safer default to lose to, and it means you can layer a tight deny over a broad allow without worrying that the allow quietly re-opens the host.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-this-matters-installs-versus-exfiltration&quot;&gt;Why this matters: installs versus exfiltration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The reason to bother with any of this is that the two things you want from the network point in opposite directions.&lt;/p&gt;
&lt;p&gt;You want package installs to work. An autonomous agent that cannot run &lt;code dir=&quot;auto&quot;&gt;npm install&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;pip install&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;cargo fetch&lt;/code&gt; is an agent that stalls on its first dependency and burns iterations doing nothing useful. Installs need the registries, the CDNs that serve the actual artifacts, and usually a code host for git dependencies.&lt;/p&gt;
&lt;p&gt;You do not want exfiltration to work. The same outbound channel that pulls a tarball in can push a file out. An agent that has read your project (the whole point of the shared workspace) and unrestricted network can, in principle, POST your source or a leaked credential to any host. You are not assuming the model is hostile. You are removing the capability so a confused or prompt-injected agent cannot do the damage even by accident.&lt;/p&gt;
&lt;p&gt;A tight allowlist resolves the tension. Installs flow to the registries and code hosts you named, and there is no open path to an arbitrary collection endpoint. The narrower the list, the smaller the surface. This is the same argument the pillar makes for the filesystem, applied to the wire: enforce the boundary from the outside, and keep the grant minimal. For the broader case on why every autonomous run belongs behind a boundary you control rather than a permission prompt the agent can blow past, the &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-agents-in-docker-sandboxes-safely/&quot;&gt;Docker Sandboxes pillar&lt;/a&gt; is the place to start.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-practical-allowlist-for-a-typical-project&quot;&gt;A practical allowlist for a typical project&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here is a concrete starting point for a JavaScript or Python project that pulls dependencies and clones a few git repos. Get the sandbox name, then grant the registries and code host in one command.&lt;/p&gt;
&lt;p&gt;For an npm-based project:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;registry.npmjs.org,*.npmjs.org,github.com,*.githubusercontent.com,codeload.github.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For a Python project add the PyPI hosts:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;pypi.org,*.pypi.org,files.pythonhosted.org&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A few notes on why those specific entries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;registry.npmjs.org&lt;/code&gt; is the index, and &lt;code dir=&quot;auto&quot;&gt;*.npmjs.org&lt;/code&gt; covers the related hosts npm reaches during an install. Listing both the apex and the wildcard avoids the half-finished install problem.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;github.com&lt;/code&gt; handles git operations, &lt;code dir=&quot;auto&quot;&gt;codeload.github.com&lt;/code&gt; serves tarball downloads for git dependencies, and &lt;code dir=&quot;auto&quot;&gt;*.githubusercontent.com&lt;/code&gt; serves raw files and release assets. Git installs that work for the clone but fail on the download usually need &lt;code dir=&quot;auto&quot;&gt;codeload.github.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;pypi.org&lt;/code&gt; is the index and &lt;code dir=&quot;auto&quot;&gt;files.pythonhosted.org&lt;/code&gt; is where the wheels and source distributions actually live, so PyPI needs both.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your agent talks to a model API from inside the sandbox (you are on Locked Down, or you run the CLI by hand in there), allow that host too, for example &lt;code dir=&quot;auto&quot;&gt;api.anthropic.com&lt;/code&gt; for Claude. On a Balanced policy these are often already covered by the baseline allowlist, so check &lt;code dir=&quot;auto&quot;&gt;sbx policy ls&lt;/code&gt; before adding duplicates.&lt;/p&gt;
&lt;p&gt;Then run the loop and walk away. The agent works in bypass-permissions mode, but the network gate is one of the walls that makes that safe:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When the loop finishes, Ralph stops the sandbox through its exit trap. Your allowlist persists with the sandbox, so the next run starts from the same known-good network posture instead of a blank deny.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-network-policy-fits-the-rest-of-the-boundary&quot;&gt;Where network policy fits the rest of the boundary&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A network policy is one wall, not the whole house. The sandbox also keeps the agent out of your home directory and off your host Docker daemon, which is the filesystem side of the same idea. If you are weighing a Docker Sandbox against a container you wire up yourself, the network gate is a good example of the difference, and &lt;a href=&quot;https://ralphloop.sh/blog/docker-sandboxes-vs-containers-for-agents/&quot;&gt;Docker Sandboxes versus plain containers for AI agents&lt;/a&gt; covers where a hand-rolled setup tends to leak.&lt;/p&gt;
&lt;p&gt;When a request is blocked and you are not sure why, get inside and look. The sandbox is a normal container from the inside, so you can reproduce the failing request, read DNS, and watch &lt;code dir=&quot;auto&quot;&gt;sbx policy log&lt;/code&gt; from the host at the same time. The full routine for that lives in &lt;a href=&quot;https://ralphloop.sh/blog/debug-inside-an-agent-sandbox/&quot;&gt;how to inspect and debug inside an AI agent sandbox&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, the network you allow depends on which agent CLI you run and what it phones home to. Each tool reaches a slightly different set of hosts for auth, models, and telemetry, so the allowlist for a Codex run is not identical to a Gemini run. The &lt;a href=&quot;https://ralphloop.sh/blog/agentic-coding-clis/&quot;&gt;agentic coding CLIs&lt;/a&gt; guide is the cross-hub companion for picking a CLI and knowing what it expects from the wire. Ralph itself is a hackable Bash loop in the tradition of Geoffrey Huntley’s original &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;Ralph technique&lt;/a&gt;, so if a policy rule needs to live somewhere repeatable, it is plain shell you can script next to the loop.&lt;/p&gt;
&lt;p&gt;Set the allowlist once, keep it minimal, and let the agent run. Installs go through, exfiltration does not, and the gate is something you can read in one &lt;code dir=&quot;auto&quot;&gt;sbx policy ls&lt;/code&gt;.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What is the default network policy for a Docker Sandbox?&lt;/h3&gt; &lt;p&gt;Deny-by-default for outbound HTTP and HTTPS. Any domain not on the allowlist is blocked, and non-HTTP protocols like raw TCP, UDP, DNS, and ICMP are blocked at the network layer. Docker also offers an Open policy that allows everything and a Locked Down policy that blocks even model provider APIs, so check which one your install uses before debugging a refused request.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I allow a domain for an AI agent sandbox?&lt;/h3&gt; &lt;p&gt;Use sbx policy allow network with the sandbox name and the domain, for example sbx policy allow network ralph-claude-my-app-a1b2c3d4 api.example.com. Pass a comma-separated list to allow several at once, add the -g flag to apply the rule to every sandbox on the machine, and quote any pattern that contains an asterisk so your shell does not expand it.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why does my allow rule for example.com not match api.example.com?&lt;/h3&gt; &lt;p&gt;Because a bare domain matches only the exact host. example.com does not match its subdomains, and the wildcard form star-dot-example.com does not match the bare root domain. To cover both the apex and its subdomains, allow both patterns in one rule, such as example.com and star-dot-example.com.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What happens when an allow rule and a deny rule both match a request?&lt;/h3&gt; &lt;p&gt;The most specific matching pattern wins overall, running from an exact hostname with a port, to an exact hostname on any port, to wildcards, to catch-all rules, and finally the default policy. When an allow and a deny match at the same specificity, the deny rule wins, so you can layer a narrow deny over a broad allow safely.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I see which hosts the agent tried to reach?&lt;/h3&gt; &lt;p&gt;Run sbx policy ls to list the active allow and deny rules, and sbx policy log to see the connection history including blocked attempts. Reading the log is the fastest way to turn a failed install into a precise allow rule, because it tells you exactly which hosts were refused so you can add only those.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>sandboxing</category></item><item><title>Running Agents in YOLO Mode (--dangerously-skip-permissions) Safely</title><link>https://ralphloop.sh/blog/bypass-permissions-yolo-mode-safely/</link><guid isPermaLink="true">https://ralphloop.sh/blog/bypass-permissions-yolo-mode-safely/</guid><description>Bypass-permissions mode hands an agent full shell access with no prompts. That is reckless on your host and a non-event inside a Docker Sandbox. Here is how to run YOLO mode when the blast radius is contained.

</description><pubDate>Fri, 15 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Yes, you can run an agent with &lt;code dir=&quot;auto&quot;&gt;--dangerously-skip-permissions&lt;/code&gt; safely, but only when the agent is locked inside a sandbox. The exact same flag is reckless on your laptop and a non-event inside a Docker Sandbox microVM. The flag never changes. What changes is what the agent can reach when it stops asking permission. This post explains what YOLO mode does, why you want it for unattended loops, and the one rule that makes &lt;code dir=&quot;auto&quot;&gt;dangerously skip permissions safely&lt;/code&gt; an honest phrase instead of a contradiction.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-yolo-mode-actually-means&quot;&gt;What “YOLO mode” actually means&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;YOLO mode is any configuration where the agent stops asking you to approve commands and just executes them. No “can I run this?” prompt, no confirmation step, no human in the loop on each shell call. The agent reads a task, decides what to do, and does it.&lt;/p&gt;
&lt;p&gt;That is the entire appeal and the entire risk in one sentence. The appeal is speed and autonomy. The risk is that you have removed the only gate between a probabilistic model and a real shell.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;claude-code-dangerously-skip-permissions&quot;&gt;Claude Code: —dangerously-skip-permissions&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;In Claude Code the flag is &lt;code dir=&quot;auto&quot;&gt;--dangerously-skip-permissions&lt;/code&gt;. There is an equivalent longer form, &lt;code dir=&quot;auto&quot;&gt;--permission-mode bypassPermissions&lt;/code&gt;, documented in the &lt;a href=&quot;https://code.claude.com/docs&quot;&gt;Claude Code docs&lt;/a&gt;. Both do the same thing: the agent skips the per-action approval prompt for the rest of the session.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;claude&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--dangerously-skip-permissions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;claude&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--permission-mode&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bypassPermissions&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The name is refreshingly honest. The word “dangerously” is in the flag because Anthropic wants you to feel a little sick typing it on a machine that has your credentials on it. That instinct is correct. The fix is not to avoid the flag. The fix is to change where you run it.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-same-idea-in-other-agents&quot;&gt;The same idea in other agents&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Every serious agent CLI has its own version of this switch, because every long autonomous run needs it. The Codex CLI has a full-auto mode. Gemini, Cursor, Copilot, and opencode each expose an auto-accept or yolo style setting that turns off interactive approvals. The spelling differs per tool, and you do not have to memorize each one, because &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Ralph Loop&lt;/a&gt; launches each supported agent in bypass-permissions mode for you. Pass any extra agent-specific flags after a &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; separator:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gpt-5.5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Supported agents are &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt; (the default), &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt;. The common thread across all of them is the same trade. You give up the approval prompt to get an agent that can work without you.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-you-want-yolo-mode-for-unattended-loops&quot;&gt;Why you want YOLO mode for unattended loops&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here is the case for turning the prompts off, because it is easy to read “dangerous” and conclude you should always approve commands by hand. You should not, and the reason is structural.&lt;/p&gt;
&lt;p&gt;An autonomous loop runs the agent many times. A single iteration of a Ralph loop does real work: it finds the highest-priority incomplete task in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, works the steps in the task spec, runs tests, linting, and type checks, commits, and repeats. Across &lt;code dir=&quot;auto&quot;&gt;./ralph.sh -n 50&lt;/code&gt; that is fifty iterations, each one issuing dozens of shell commands. If every command needs your approval, the loop is not autonomous. It is a very slow pair-programming session where you are the bottleneck and you are asleep.&lt;/p&gt;
&lt;p&gt;The whole point of running an agent overnight is that you are not there. A permission prompt at 3am is a permission prompt that blocks forever. The agent stalls on the first &lt;code dir=&quot;auto&quot;&gt;npm install&lt;/code&gt; waiting for a yes that never comes, and you wake up to a loop that did nothing for eight hours.&lt;/p&gt;
&lt;p&gt;So unattended runs and per-command approval are mutually exclusive. You either babysit, or you go YOLO. For a long loop, babysitting defeats the purpose, which means YOLO mode is not an edge case. It is the normal operating mode for any agent meant to run while you are away. If you want the deeper version of this argument applied to a single CLI, the &lt;a href=&quot;https://ralphloop.sh/blog/run-claude-code-in-a-loop/&quot;&gt;run Claude Code in a loop&lt;/a&gt; guide walks through the same trade for autonomous Claude Code runs.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-one-rule-only-go-yolo-when-the-blast-radius-is-contained&quot;&gt;The one rule: only go YOLO when the blast radius is contained&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Now the rule that makes all of this safe. Run YOLO mode only when the blast radius is contained. The blast radius is everything the agent can touch when it stops asking. Your job is to make that set small, disposable, and external to anything you care about.&lt;/p&gt;
&lt;p&gt;A permission prompt is not a security boundary. When the agent asks “can I run this command?” and you click yes, you are the boundary, and you are slow, distracted, and often not awake. When you tell the agent to stop asking, you have not made the agent safe. You have removed the only thing standing between the model and your filesystem. The real fix is to put a wall around the agent that the agent cannot reach through.&lt;/p&gt;
&lt;p&gt;On your host, that wall does not exist. An agent running with your user account can read your SSH keys, your &lt;code dir=&quot;auto&quot;&gt;~/.aws/credentials&lt;/code&gt;, your browser session cookies, and every other git repository you have checked out. It can push to remotes, delete files, and run a &lt;code dir=&quot;auto&quot;&gt;curl | bash&lt;/code&gt; it found in a README. None of that requires malice. It is a probabilistic system executing shell commands, and shell commands do not have an undo. For the longer version of why every autonomous run belongs behind a wall, read &lt;a href=&quot;https://ralphloop.sh/blog/why-sandbox-ai-coding-agents/&quot;&gt;why you should sandbox every autonomous coding agent&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Inside a Docker Sandbox, the wall is real. A Docker Sandbox is not a plain &lt;code dir=&quot;auto&quot;&gt;docker run&lt;/code&gt;. It is a lightweight microVM with its own guest kernel, documented in the &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/&quot;&gt;Docker Sandboxes docs&lt;/a&gt;. The agent sees your project directory and a network gate. It does not see the rest of your machine. Run &lt;code dir=&quot;auto&quot;&gt;--dangerously-skip-permissions&lt;/code&gt; in there and “anything the agent can do” now means “anything it can do inside a microVM that only contains one project directory and a locked-down network.” The dangerous flag becomes a non-event, because the danger had a target on the host and the sandbox removed the target.&lt;/p&gt;
&lt;p&gt;This is the inversion worth internalizing. You are not making the agent trustworthy. You are making trust unnecessary by shrinking the blast radius to something you can reset with git or throw away entirely.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TB
  subgraph Laptop[&quot;YOLO on your laptop (host)&quot;]
    HAgent[&quot;Agent: --dangerously-skip-permissions&quot;]
    HAgent --&gt; Keys[&quot;SSH keys, cloud creds, cookies&quot;]
    HAgent --&gt; Repos[&quot;Every other git repo&quot;]
    HAgent --&gt; Net1[&quot;Open network: fetch any URL&quot;]
  end
  subgraph Box[&quot;YOLO inside a Docker Sandbox&quot;]
    SAgent[&quot;Agent: --dangerously-skip-permissions&quot;]
    SAgent --&gt; Proj[&quot;Shared project directory only&quot;]
    SAgent --&gt; Gate[&quot;Network gate: deny-by-default&quot;]
    SAgent -. &quot;no path to&quot; .-&gt; HostHome[&quot;Host home directory&quot;]
  end&lt;/pre&gt;
&lt;p&gt;Same flag in both boxes. The only difference is the box. On the left, “skip permissions” means skip the last protection your machine had. On the right, it means move fast inside a container that contains nothing precious.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-ralph-runs-yolo-mode-inside-a-named-sandbox&quot;&gt;How Ralph runs YOLO mode inside a named sandbox&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;You can wire this up by hand: create a microVM, launch the agent with the bypass flag, and tear it down when you are done. The point of Ralph is that you do not have to. It computes the sandbox name, checks that the &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt; CLI exists, decides whether to create or attach, launches the agent in bypass-permissions mode, and stops the sandbox on exit. Ralph is a hackable Bash loop in the tradition of Geoffrey Huntley’s original &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;Ralph technique&lt;/a&gt;, and the YOLO plumbing is part of what it automates.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;answering-yes-to-bypass-permissions-inside-the-sandbox&quot;&gt;Answering “Yes” to bypass permissions inside the sandbox&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The first time you launch an agent in bypass mode, the CLI shows a one-time warning that asks you to confirm you really want to skip permission prompts. With Ralph, you hit that confirmation inside the sandbox, not on your host, which is exactly where you want to be saying yes. Authenticate and accept the bypass warning in one step with the login action:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--login&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This opens the selected agent inside its correctly named sandbox. You answer the bypass-permissions prompt once, in the microVM, and from then on the agent runs without prompts inside that contained environment. Saying yes to “skip permissions” feels very different when the worst case is a disposable container instead of your home directory.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-deterministic-sandbox-name-is-the-boundary&quot;&gt;The deterministic sandbox name is the boundary&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Ralph names every sandbox deterministically so the same project and agent pair always reuses the same microVM. The format is:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;current-dir&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;agent&gt;&lt;/code&gt; slug is the lowercased agent name, &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;current-dir&gt;&lt;/code&gt; is the sanitized basename of your project directory, and &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;hash8&gt;&lt;/code&gt; is the first 8 hex characters of a &lt;code dir=&quot;auto&quot;&gt;sha256&lt;/code&gt; of the absolute project path. A project at &lt;code dir=&quot;auto&quot;&gt;/Users/me/Work/My App&lt;/code&gt; running Claude becomes &lt;code dir=&quot;auto&quot;&gt;ralph-claude-my-app-a1b2c3d4&lt;/code&gt;. The path hash keeps two same-named directories on different paths from colliding, and the agent slug in the name means switching &lt;code dir=&quot;auto&quot;&gt;--agent&lt;/code&gt; gives you a separate sandbox rather than a surprising state swap.&lt;/p&gt;
&lt;p&gt;You never have to memorize it. Ralph prints the name on its startup line, and you can ask for it directly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That name is the handle for everything else: the sandbox you shell into, the sandbox you allowlist a domain for, and the sandbox Ralph stops on exit.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;create-versus-attach-every-iteration&quot;&gt;Create versus attach, every iteration&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Each iteration of the loop probes whether the deterministic sandbox already exists. Iteration one usually creates it. Iteration two and onward attach to it. If you manually &lt;code dir=&quot;auto&quot;&gt;sbx rm&lt;/code&gt; the sandbox mid-run, the next probe simply creates it again. That re-probe is what makes a long YOLO run resilient to you poking at the sandbox by hand.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
  Start[&quot;./ralph.sh -n 50&quot;] --&gt; Name[&quot;Compute name: ralph-agent-dir-hash8&quot;]
  Name --&gt; CheckSbx{&quot;sbx installed?&quot;}
  CheckSbx --&gt;|&quot;no&quot;| Fail[&quot;Exit with docs link&quot;]
  CheckSbx --&gt;|&quot;yes&quot;| Iter[&quot;Start iteration&quot;]
  Iter --&gt; Probe{&quot;sandbox exists?&quot;}
  Probe --&gt;|&quot;no&quot;| Create[&quot;sbx run --name ... agent .&quot;]
  Probe --&gt;|&quot;yes&quot;| Attach[&quot;sbx run name&quot;]
  Create --&gt; Work[&quot;Agent works one task in YOLO mode&quot;]
  Attach --&gt; Work
  Work --&gt; Signal{&quot;completion signal?&quot;}
  Signal --&gt;|&quot;COMPLETE&quot;| Done[&quot;Exit 0, then sbx stop&quot;]
  Signal --&gt;|&quot;keep going&quot;| Iter&lt;/pre&gt;
&lt;p&gt;The loop stops on an explicit signal, not a vibe. The agent emits a promise tag: &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; when all tasks are done, &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; when it needs human help, or &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt; when it needs a decision. Those map to exit codes &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt;, with &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; reserved for hitting the iteration cap. When the loop ends, Ralph stops the sandbox through its exit trap and hands you back a contained microVM you can inspect or discard.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-safe-yolo-run-end-to-end&quot;&gt;A safe YOLO run, end to end&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Putting the pieces in order, here is what a clean bypass-permissions run looks like. None of these steps put the agent’s hands on your host.&lt;/p&gt;
&lt;p&gt;First, install Ralph in your project:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@pageai/ralph-loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Next, authenticate the agent inside its sandbox and accept the bypass-permissions warning once, in the microVM:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--login&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If the task needs network access for installs, allowlist only the domains it needs instead of opening everything. Get the name, then add a rule:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;*.npmjs.org,github.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The network gate matters as much as the filesystem boundary when the agent runs unattended in YOLO mode. A tight allowlist lets installs through and keeps exfiltration out. The full treatment of building that allowlist lives in &lt;a href=&quot;https://ralphloop.sh/blog/ai-agent-sandbox-network-policies/&quot;&gt;network policies for AI agent sandboxes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then run the loop and walk away. The agent runs with permissions bypassed, but the sandbox is the boundary, so fast and contained are the same thing:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If something looks off mid-run, shell in and look. The sandbox is a normal container from the inside:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When the loop finishes, Ralph stops the sandbox for you. The agent skipped every prompt and ran at full speed, and your host never had its hands on it. This is the default posture Ralph ships, and the broader setup is covered in the pillar guide on &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-agents-in-docker-sandboxes-safely/&quot;&gt;running AI coding agents in Docker Sandboxes safely&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-yolo-mode-still-bites&quot;&gt;Where YOLO mode still bites&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A sandbox is a strong boundary and not a magic one. Two limits are worth naming so you do not over-trust the setup.&lt;/p&gt;
&lt;p&gt;First, the project directory is shared, which is the entire point, so the agent can absolutely wreck your working tree. The protection there is git, not the sandbox. Commit often, work on a branch, and treat the sandbox as protection for everything outside the project rather than a substitute for version control inside it. A YOLO agent that thrashes on a bad task can still produce a messy diff. The fix is the same as for any agent: clean tasks, frequent commits, and a verification gate of tests, type checks, and linting on every iteration.&lt;/p&gt;
&lt;p&gt;Second, whatever you allowlist on the network is genuinely reachable. If you grant a domain that can receive uploads, an agent in YOLO mode could in principle send data there. A &lt;code dir=&quot;auto&quot;&gt;&quot;**&quot;&lt;/code&gt; allow-all rule throws the whole network surface wide open and undoes half the reason you built the sandbox. Treat network grants like firewall rules: minimal, specific, and reviewed.&lt;/p&gt;
&lt;p&gt;Inside those limits, the model is simple and it holds. Skip permissions only when the blast radius is a disposable microVM, enforce the boundary from the outside, and let the agent be fearless where fearless is cheap.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;Is it safe to run an agent with --dangerously-skip-permissions?&lt;/h3&gt; &lt;p&gt;It is unsafe on your host and safe inside a Docker Sandbox. On the host the flag gives the agent full shell access with no confirmation, so a single bad command can touch your SSH keys, credentials, or other repositories. Inside a microVM sandbox the same flag only grants access to the shared project directory and an allowlisted network, so the worst case is something you can reset with git or throw away.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What is YOLO mode for an AI coding agent?&lt;/h3&gt; &lt;p&gt;YOLO mode is any configuration where the agent stops asking you to approve each command and just executes. In Claude Code it is the flag --dangerously-skip-permissions, also written as --permission-mode bypassPermissions. Other agents have their own full-auto or auto-accept setting that does the same thing.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why do unattended agent loops need bypass-permissions mode?&lt;/h3&gt; &lt;p&gt;A long loop issues many shell commands per iteration. If every command needs your approval, the loop blocks the moment you step away and does nothing until you return. Bypass-permissions mode is what lets an agent run overnight without a human approving each action.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Does Ralph Loop run agents in YOLO mode by default?&lt;/h3&gt; &lt;p&gt;Yes. Ralph launches each supported agent in bypass-permissions mode because the Docker Sandbox is the boundary. You accept the one-time bypass warning inside the sandbox during ./ralph.sh --login, and every iteration after that runs without prompts inside the contained microVM.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Can a sandboxed YOLO agent still cause damage?&lt;/h3&gt; &lt;p&gt;It can damage the shared project directory, since that directory is shared on purpose, so commit often and work on a branch. It can also reach any network domain you allowlist, so keep the allowlist minimal and avoid the allow-all double-star rule. It cannot reach the rest of your host, which is the boundary the sandbox enforces.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>sandboxing</category></item><item><title>Spec-Driven Development vs Vibe Coding: When Each One Wins</title><link>https://ralphloop.sh/blog/spec-driven-development-vs-vibe-coding/</link><guid isPermaLink="true">https://ralphloop.sh/blog/spec-driven-development-vs-vibe-coding/</guid><description>Vibe coding is fast for throwaways and dangerous for production. Here is an honest comparison with spec-driven development, why autonomous agents need a spec, and how to pick per task.

</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Vibe coding wins when the code is disposable. Spec-driven development wins when the code has to survive: production systems, code other people maintain, and anything an autonomous agent builds while you are asleep. The split is not about taste. It is about whether the work has verifiable acceptance criteria or just a feeling that the output looks right.&lt;/p&gt;
&lt;p&gt;This post defines both honestly, including where vibe coding is genuinely the correct choice. Then it covers why an autonomous loop has no choice but to go spec-driven, what vibe coding actually costs once it scales past one person and one afternoon, and how to pick per task instead of picking a religion. It ends with how Ralph leans spec-driven through a PRD and a task list, without forcing you to spec a five line script.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-vibe-coding-actually-is&quot;&gt;What vibe coding actually is&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Vibe coding is writing code by feel. You describe what you want in loose terms, you read the output, you eyeball whether it looks correct, and you keep nudging until it runs. The acceptance test is your gut. The spec lives in your head and changes as you go.&lt;/p&gt;
&lt;p&gt;For an AI workflow, vibe coding means prompting an agent with intent and judging the result by reading it. “Add a settings page.” “Make this faster.” “Fix the layout on mobile.” You accept the diff because it looks plausible and the page renders, not because a test confirmed a specific condition.&lt;/p&gt;
&lt;p&gt;This is not an insult. Vibe coding is the fastest way to explore an idea you do not fully understand yet. When the goal is to learn the shape of a problem, writing a spec first is premature: you do not know enough to write a good one. You vibe a spike, see what the problem actually wants, then throw the spike away.&lt;/p&gt;
&lt;p&gt;Where vibe coding is genuinely fine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spikes and prototypes you intend to delete.&lt;/li&gt;
&lt;li&gt;One off scripts: a data migration you run once, a quick scraper, a throwaway chart.&lt;/li&gt;
&lt;li&gt;Demos and hackathon code where shipping in an hour beats shipping correctly.&lt;/li&gt;
&lt;li&gt;Exploratory work where the spec would just be a guess anyway.&lt;/li&gt;
&lt;li&gt;Personal tools with exactly one user who is also the author.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The common thread is that nobody inherits the code. There is no future maintainer, no on call engineer, no agent that has to extend it next week. When the blast radius of a mistake is your own afternoon, the overhead of a spec is not worth it. Vibe away.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-spec-driven-development-actually-is&quot;&gt;What spec-driven development actually is&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Spec-driven development inverts the order. You write the specification first, then the code exists to satisfy it. The spec is not a wishlist. It is goals (what to build and why), constraints (the stack, the boundaries, what is out of scope), and verifiable acceptance criteria (conditions something can check by running a command and reading the output).&lt;/p&gt;
&lt;p&gt;The phases come from &lt;a href=&quot;https://github.com/github/spec-kit&quot;&gt;GitHub Spec Kit&lt;/a&gt;: specify the intent, plan the approach, break it into tasks, then implement and verify. Each phase produces an artifact the next phase consumes. The full version of this workflow, with PRDs, task lists, and breakdown, is laid out in &lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;spec-driven development with AI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The defining property is that “done” is not a feeling. A spec-driven task is done when a specific, checkable condition is true. “Login works” is a vibe. “POST /api/login with a wrong password returns 401 and the body &lt;code dir=&quot;auto&quot;&gt;{ error: &apos;Invalid credentials&apos; }&lt;/code&gt;” is a criterion a machine can confirm without your opinion. That single difference is what makes the rest of this comparison fall out.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-an-autonomous-agent-needs-a-spec&quot;&gt;Why an autonomous agent needs a spec&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here is the part that decides the whole debate for anyone running agents in a loop. A human can vibe code because a human carries the unwritten spec. You know the system. You remember the edge case from last month. You ask a teammate when something is ambiguous. An autonomous agent does none of that.&lt;/p&gt;
&lt;p&gt;An agent reads what is on disk. When the spec is silent, it does not pause and ask. It invents an answer and proceeds with full confidence. The guess looks fine in the diff and breaks on the case nobody wrote down. Multiply that across a long run and you get a pile of confident, untested code that drifts further from your intent on every iteration.&lt;/p&gt;
&lt;p&gt;The Ralph technique makes this concrete. Each iteration starts the agent with a fresh context window, which is what keeps a long run from rotting (the agent losing the plot over a marathon session). The mechanics are covered in &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;what is the Ralph technique&lt;/a&gt;, the loop popularized by Geoffrey Huntley in his &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;original Ralph writeup&lt;/a&gt;. Fresh context is the reason the loop survives. It is also the reason vibe coding cannot drive it.&lt;/p&gt;
&lt;p&gt;Think about what fresh context implies. The agent that starts iteration 30 has no memory of the conversation from iterations 1 through 29. It rebuilds its entire understanding from files: the PRD, the task list, the logs, the git history. There is no “you know what I meant” to fall back on. If the intent is not written down, it does not exist for that iteration’s agent.&lt;/p&gt;
&lt;p&gt;So the loop needs the spec for two reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No guessing.&lt;/strong&gt; Written goals and constraints mean the fresh-context agent reorients to the same target every pass instead of inventing a new one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A stop condition.&lt;/strong&gt; The loop ends on an explicit completion signal, not a vibe. The agent emits &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; when every task passes its criteria, and &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; exits with code &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;. Without verifiable criteria there is nothing for the loop to check, so it can never honestly say it is done.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the core argument. You cannot run an agent unattended against “make it good.” You can run it against a task list where every task carries acceptance criteria the verification stack can confirm.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-vibe-coding-costs-at-scale&quot;&gt;What vibe coding costs at scale&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Vibe coding feels free because the cost is deferred. It shows up later, somewhere else, and usually larger. Three costs dominate once the work outgrows a single afternoon.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rework.&lt;/strong&gt; Code accepted on a vibe gets rejected on a test you write three weeks later, or worse, by an incident. The fix is rarely a one line change, because the original code encoded a misunderstanding, not a typo. You are not patching a bug. You are re-deriving the requirement that was never written down, then rewriting against it. Specifying first front-loads that thinking when it is cheap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Drift.&lt;/strong&gt; Every implicit decision is a decision someone, or some agent, makes for you. Vibe coding scatters those decisions across files and weeks. One place validates email with a regex, another with a library, a third not at all. There was never a source of truth, so the codebase has three answers to one question. With a spec, the answer is written once and every task inherits it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unverifiable output.&lt;/strong&gt; This is the expensive one for AI workflows. If you cannot state how to check that the work is correct, you cannot automate the check, which means a person has to read every diff and judge it by hand. That person becomes the bottleneck. The whole promise of an autonomous loop is that the machine verifies its own work. Vibe coding removes the thing the machine would verify against, so you are back to manual review at the exact moment you wanted to step away.&lt;/p&gt;
&lt;p&gt;The repo mantra is blunt: if you didn’t test it, it doesn’t work. Vibe coding at scale is a bet that you will remember every unwritten assumption and that nobody else will touch the code. Both halves of that bet lose over time.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-to-pick-per-task&quot;&gt;How to pick per task&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The honest answer is that you do not pick one approach for your whole life. You pick per task, and the deciding questions are quick.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
  Start[&quot;New piece of work&quot;] --&gt; Survive{&quot;Does the code survive past today?&quot;}
  Survive --&gt;|&quot;No, it is a spike or demo&quot;| Vibe[&quot;Vibe code it, delete it later&quot;]
  Survive --&gt;|&quot;Yes&quot;| Criteria{&quot;Can you write pass or fail acceptance criteria?&quot;}
  Criteria --&gt;|&quot;Not yet, too fuzzy&quot;| Spike[&quot;Vibe a spike first, then spec what you learned&quot;]
  Criteria --&gt;|&quot;Yes&quot;| Agent{&quot;Will an autonomous agent build it unattended?&quot;}
  Agent --&gt;|&quot;No, you are at the keyboard&quot;| Mixed[&quot;Spec the risky parts, vibe the glue&quot;]
  Agent --&gt;|&quot;Yes&quot;| Spec[&quot;Spec-driven: PRD, tasks, criteria, verify&quot;]&lt;/pre&gt;
&lt;p&gt;Walk the branches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Does it survive past today?&lt;/strong&gt; If the answer is no, stop reading and vibe it. A migration script you run once does not need a PRD.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can you write pass or fail criteria?&lt;/strong&gt; If the problem is still fuzzy, you do not know enough to spec well. Vibe a spike to learn the shape, then write the spec from what you learned. The spike was reconnaissance, not the deliverable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Will an agent build it unattended?&lt;/strong&gt; This is the hard cutoff. The moment a fresh-context agent is doing the work without you watching, you need the spec. There is no in between, because the agent has no judgment to fall back on and no way to ask you mid-iteration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You at the keyboard, code that survives?&lt;/strong&gt; This is the common case, and it is mixed. Spec the parts where a wrong guess is expensive (auth, money, data integrity, public APIs). Vibe the glue where a mistake is cheap and obvious. You do not need acceptance criteria for a button’s hover color.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The mistake is treating this as ideology. Spec-driven purists waste hours writing criteria for code they will delete. Vibe coding diehards ship confident nonsense to production. The discipline is matching the rigor to the stakes.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-ralph-leans-spec-driven&quot;&gt;How Ralph leans spec-driven&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph is built for the unattended case, so it leans spec-driven by design. It does not force you to write a PRD for a one line fix, but the loop architecture assumes a spec exists when you run it for real.&lt;/p&gt;
&lt;p&gt;The spec lives in two files under &lt;code dir=&quot;auto&quot;&gt;.agent/prd/&lt;/code&gt;. &lt;code dir=&quot;auto&quot;&gt;PRD.md&lt;/code&gt; is the full document: goals, constraints, core features, technical stack, security considerations, and assumptions. &lt;code dir=&quot;auto&quot;&gt;SUMMARY.md&lt;/code&gt; is the short executive overview sent to the agent every iteration so it reorients fast without rereading the entire PRD. Long document for depth, short summary for the working context on each pass.&lt;/p&gt;
&lt;p&gt;You do not write all of this by hand. The &lt;code dir=&quot;auto&quot;&gt;prd-creator&lt;/code&gt; skill, run in plan mode, interviews you one question at a time, researches the problem, and writes both files plus a task list. The details of writing that document, and acceptance criteria an agent can actually verify, are in &lt;a href=&quot;https://ralphloop.sh/blog/how-to-write-prd-for-ai-agent/&quot;&gt;how to write a PRD an AI agent can actually build from&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The PRD then decomposes into a task lookup table. &lt;code dir=&quot;auto&quot;&gt;tasks.json&lt;/code&gt; holds the list, and each &lt;code dir=&quot;auto&quot;&gt;tasks/TASK-{ID}.json&lt;/code&gt; carries its own description, dependencies, and an &lt;code dir=&quot;auto&quot;&gt;acceptanceCriteria&lt;/code&gt; array. This is the structure that lets a loop grind through hundreds of tasks without losing track, covered in &lt;a href=&quot;https://ralphloop.sh/blog/task-lookup-table-for-agents/&quot;&gt;task lookup tables for agents&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@pageai/ralph-loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Each iteration the loop finds the highest-priority incomplete task, works its steps, runs tests and linting and type checking, takes a screenshot, flips the task to passing only when the gate is green, and commits. The verification stack is Playwright, Vitest, TypeScript, ESLint, and Prettier. The agent does not get to declare victory on a vibe. It declares victory when the criteria pass, and the loop stops on the completion promise.&lt;/p&gt;
&lt;p&gt;That is the whole point of the comparison in one workflow. The spec is what lets the machine verify its own output, which is what lets you close the laptop. Vibe coding is the right tool when nobody inherits the code. The moment an agent inherits it, the spec stops being overhead and starts being the only thing keeping the loop honest.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-to-go-next&quot;&gt;Where to go next&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you are deciding how much structure a piece of work deserves, read across the spec-driven cluster:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;Spec-driven development with AI&lt;/a&gt; for the full Specify, Plan, Tasks, Implement workflow.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/how-to-write-prd-for-ai-agent/&quot;&gt;How to write a PRD an AI agent can actually build from&lt;/a&gt; for the document that sits at the top of it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/task-lookup-table-for-agents/&quot;&gt;Task lookup tables for agents&lt;/a&gt; for scaling a spec to hundreds of tasks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the loop that reads your spec on every pass and why fresh context changes the rules, read &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;what is the Ralph technique&lt;/a&gt;.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What is the difference between spec-driven development and vibe coding?&lt;/h3&gt; &lt;p&gt;Vibe coding writes code by feel: you prompt with loose intent, read the output, and accept it because it looks plausible and runs. Spec-driven development writes the specification first, so the code exists to satisfy concrete goals, explicit constraints, and verifiable acceptance criteria. The defining difference is whether done is a feeling or a checkable condition a machine can confirm by running a command and reading the output.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Is vibe coding ever the right choice?&lt;/h3&gt; &lt;p&gt;Yes, when the code is disposable and nobody inherits it. Spikes you intend to delete, one off scripts, demos, hackathon code, and personal tools with one user are all fine to vibe. The overhead of a spec is not worth it when the blast radius of a mistake is your own afternoon. Vibe a spike to learn a fuzzy problem, then write the spec from what you learned.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why do autonomous AI agents need a spec instead of vibe coding?&lt;/h3&gt; &lt;p&gt;An autonomous agent reads only what is on disk, and a Ralph loop starts each iteration with a fresh context window that has no memory of previous iterations. When the spec is silent, the agent invents an answer and proceeds with full confidence, and that guess compounds across a long run. Verifiable acceptance criteria give the loop something to check and a real stop condition, so it can honestly signal completion instead of guessing it is done.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What does vibe coding cost once a project scales?&lt;/h3&gt; &lt;p&gt;Three things: rework, because code accepted on a vibe encodes a misunderstanding you later re-derive and rewrite; drift, because every implicit decision gets answered differently across files with no source of truth; and unverifiable output, because if you cannot state how to check correctness, a person has to read every diff by hand. That manual review is the exact bottleneck an autonomous loop is supposed to remove.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does Ralph use spec-driven development?&lt;/h3&gt; &lt;p&gt;Ralph keeps the spec in .agent/prd/PRD.md and a short SUMMARY.md sent to the agent each iteration, generated by the prd-creator skill in plan mode. The PRD decomposes into a task lookup table where each TASK-ID.json carries acceptance criteria. Every iteration the loop works one task, runs Playwright, Vitest, TypeScript, ESLint, and Prettier, and only marks the task passing when the gate is green, then commits and stops on a completion promise.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>spec-driven</category></item><item><title>Docker Sandboxes vs Plain Containers for AI Agents</title><link>https://ralphloop.sh/blog/docker-sandboxes-vs-containers-for-agents/</link><guid isPermaLink="true">https://ralphloop.sh/blog/docker-sandboxes-vs-containers-for-agents/</guid><description>A plain Docker container is a start, not a boundary. Here is how Docker Sandboxes microVMs compare to a hand-rolled container for isolating an autonomous coding agent.

</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A plain container is a start, not a boundary. Running an autonomous coding agent inside &lt;code dir=&quot;auto&quot;&gt;docker run&lt;/code&gt; does isolate it from your host filesystem, which is already better than running the agent directly on your laptop. The gap is that a normal container shares your host kernel, so the wall between an agent executing arbitrary shell commands and your machine is thinner than most people assume. Docker Sandboxes close that gap by giving each sandbox its own microVM with a separate guest kernel, which is the difference this post is about when you compare a &lt;code dir=&quot;auto&quot;&gt;docker sandbox vs container ai agent&lt;/code&gt; setup.&lt;/p&gt;
&lt;p&gt;If you want the full safety argument first, the pillar guide on &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-agents-in-docker-sandboxes-safely/&quot;&gt;how to run AI coding agents in Docker Sandboxes safely&lt;/a&gt; covers the whole boundary. This post zooms in on one question: what does a hand-rolled container actually give you, where does it leak, and why does a microVM hold where a container does not.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;container-vs-sandbox-the-short-answer&quot;&gt;Container vs sandbox, the short answer&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The decision comes down to what you are isolating from and how much you trust the workload.&lt;/p&gt;
&lt;p&gt;A container is the right tool for software you trust. You built it, you reviewed it, and you are shipping it. Namespaces and cgroups keep processes tidy and resource bounded, and the shared kernel is a feature because it is fast and cheap.&lt;/p&gt;
&lt;p&gt;A microVM is the right tool for software you explicitly do not trust to behave. An autonomous agent running in bypass-permissions mode is exactly that. It decides which commands to run, it can &lt;code dir=&quot;auto&quot;&gt;curl | bash&lt;/code&gt; something it found in a README, and it does not have an undo. For that workload you want a virtualization boundary, not just a kernel namespace.&lt;/p&gt;
&lt;p&gt;So the short answer: use a container when the code is yours and reviewed, use a sandbox when a probabilistic system is executing shell commands on your behalf. The rest of this post is the detail behind that rule.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-a-hand-rolled-container-actually-gives-you&quot;&gt;What a hand-rolled container actually gives you&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Start with the honest version of the container story, because containers are useful and a &lt;code dir=&quot;auto&quot;&gt;docker run&lt;/code&gt; is genuinely a step up from nothing.&lt;/p&gt;
&lt;p&gt;A container gives the agent its own process namespace, its own mount namespace, its own network namespace, and a cgroup that can cap CPU and memory. The agent sees its own process tree, not yours. It sees the filesystem you mounted, not your whole home directory by default. That isolation is real, and for a lot of workloads it is enough.&lt;/p&gt;
&lt;p&gt;Here is a minimal version of what people reach for first:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--rm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-v&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$PWD&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:/work&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-w&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/work&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;node:22&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This drops you into a container with your project mounted at &lt;code dir=&quot;auto&quot;&gt;/work&lt;/code&gt;. The agent can edit those files, run installs, and run tests. Your &lt;code dir=&quot;auto&quot;&gt;~/.ssh&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;~/.aws&lt;/code&gt; are not mounted, so they are not visible. For a quick experiment that is a reasonable boundary.&lt;/p&gt;
&lt;p&gt;The problem is that the defaults of a plain container were designed for running trusted software conveniently, not for caging an untrusted process. So the moment you run a real agent for hours, the soft spots start to matter.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;where-the-container-leaks&quot;&gt;Where the container leaks&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;There are four leaks worth naming, because each one is a place a long autonomous run can go wrong.&lt;/p&gt;
&lt;p&gt;First, root by default. Unless you say otherwise, the process inside the container runs as root (uid 0). It is namespaced root, not host root, but it is still root inside the container, which widens what a container escape or a misconfigured mount can reach. An agent that runs &lt;code dir=&quot;auto&quot;&gt;chmod&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;chown&lt;/code&gt;, or installs system packages as root is one surprised step closer to your host than it needs to be.&lt;/p&gt;
&lt;p&gt;Second, the shared kernel. This is the big one. Every container on the machine, including the agent’s, talks to the same host kernel you are using. Namespaces are a kernel feature isolating a process from other processes, not a separate operating system. A kernel level bug, a privileged syscall, or a known container escape has your actual kernel as its target. You are trusting the kernel boundary to hold against code you specifically decided not to trust.&lt;/p&gt;
&lt;p&gt;Third, bind mounts. The convenient &lt;code dir=&quot;auto&quot;&gt;-v &quot;$PWD&quot;:/work&lt;/code&gt; is a two way door. The agent writes to your real working tree on the host, which is the point, but it also means a bad &lt;code dir=&quot;auto&quot;&gt;rm -rf&lt;/code&gt; or an overzealous “let me clean this up” reaches your real files, not a copy. People also tend to mount more than they should over time (a config directory here, a credentials file there) and each added mount is another path out of the box. Mounting the Docker socket (&lt;code dir=&quot;auto&quot;&gt;/var/run/docker.sock&lt;/code&gt;) is the worst case, because a process that can talk to the Docker daemon can start a new container as host root and own the machine.&lt;/p&gt;
&lt;p&gt;Fourth, the network. By default a container gets full outbound network access through the default bridge. An agent with unrestricted egress can fetch arbitrary code and, in the worst case, send data out. There is no allowlist unless you build one, and building one with raw containers means writing firewall rules by hand.&lt;/p&gt;
&lt;p&gt;None of these are reasons to never use a container. They are reasons to not assume &lt;code dir=&quot;auto&quot;&gt;docker run&lt;/code&gt; is a security boundary for an untrusted agent. For the broader case on why every autonomous run needs a wall around it, see &lt;a href=&quot;https://ralphloop.sh/blog/why-sandbox-ai-coding-agents/&quot;&gt;why you should sandbox every autonomous coding agent&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-docker-sandboxes-differ&quot;&gt;How Docker Sandboxes differ&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Docker Sandboxes (the &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt; CLI) are built for the untrusted workload case, and the &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/&quot;&gt;Docker Sandboxes documentation&lt;/a&gt; is the primary source for the internals. Three differences matter most for agents.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;a-microvm-per-sandbox&quot;&gt;A microVM per sandbox&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Each sandbox runs inside its own lightweight virtual machine with its own guest kernel, not a namespaced process sharing your host kernel. That single change is what upgrades the boundary from “trust the kernel namespace” to “defeat a virtualization layer.” A process that escapes the agent’s view inside the sandbox lands in a guest kernel that is not your kernel, with a virtual machine monitor between it and your host.&lt;/p&gt;
&lt;p&gt;For the agent this is invisible. It still gets a Linux environment, a shell, a filesystem, and the tools it expects. The isolation is underneath, where the agent cannot see it and does not need to.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TB
  subgraph PlainPath[&quot;Plain container&quot;]
    direction TB
    PApp[&quot;Agent process, often root&quot;]
    PNS[&quot;Namespaces + cgroups&quot;]
    PKernel[&quot;Shared host kernel&quot;]
    PHost[&quot;Host: files, keys, network&quot;]
    PApp --&gt; PNS --&gt; PKernel --&gt; PHost
  end
  subgraph SbxPath[&quot;Docker Sandbox (sbx)&quot;]
    direction TB
    SApp[&quot;Agent process in YOLO mode&quot;]
    SGuest[&quot;Guest kernel (microVM)&quot;]
    SVMM[&quot;Virtual machine monitor&quot;]
    SHost[&quot;Host: isolated, project dir shared only&quot;]
    SApp --&gt; SGuest --&gt; SVMM --&gt; SHost
  end&lt;/pre&gt;
&lt;p&gt;Read the two stacks top to bottom. In the plain container, the agent process reaches the host across one shared kernel. In the sandbox, the agent process reaches the host only after getting through a guest kernel and a virtual machine monitor, and the host shares nothing but the one project directory you pointed it at.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;standalone-no-docker-desktop-seconds-to-spin-up&quot;&gt;Standalone, no Docker Desktop, seconds to spin up&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Docker Sandboxes run as a standalone CLI. You do not need Docker Desktop running to use &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt;, which keeps the dependency surface small and makes it easy to script. A sandbox spins up in seconds, so the microVM is not a heavy thing you provision once and babysit. It is closer to the ergonomics of &lt;code dir=&quot;auto&quot;&gt;docker run&lt;/code&gt;: you ask for it, you get it quickly, you throw it away when you are done.&lt;/p&gt;
&lt;p&gt;That speed matters for a loop. A tool like &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Ralph Loop&lt;/a&gt; creates or reattaches a sandbox on every iteration, so a slow boundary would tax every pass. Fast startup is what makes “isolate every run” practical instead of aspirational.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;deterministic-names-and-a-network-gate&quot;&gt;Deterministic names and a network gate&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Two more things come for free with the sandbox model that you would otherwise build by hand.&lt;/p&gt;
&lt;p&gt;Sandboxes get deterministic names so the same project and agent pair reuses the same microVM. Ralph uses the form &lt;code dir=&quot;auto&quot;&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;current-dir&gt;-&amp;#x3C;hash8&gt;&lt;/code&gt;, for example &lt;code dir=&quot;auto&quot;&gt;ralph-claude-my-app-a1b2c3d4&lt;/code&gt;. You list them with &lt;code dir=&quot;auto&quot;&gt;sbx ls&lt;/code&gt;, shell into one with &lt;code dir=&quot;auto&quot;&gt;sbx exec -it &amp;#x3C;name&gt; bash&lt;/code&gt;, and reattach with &lt;code dir=&quot;auto&quot;&gt;sbx run &amp;#x3C;name&gt;&lt;/code&gt;. No tracking container IDs by hand.&lt;/p&gt;
&lt;p&gt;The network is a gate, not an open door. Docker Sandboxes default to deny-by-default egress, and you allowlist exactly what a task needs:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;*.npmjs.org,github.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You can apply a rule globally with &lt;code dir=&quot;auto&quot;&gt;-g&lt;/code&gt;, and the &lt;code dir=&quot;auto&quot;&gt;&quot;**&quot;&lt;/code&gt; pattern opens a single sandbox fully when you genuinely cannot enumerate the domains. The full treatment of building an allowlist that lets installs through while keeping exfiltration out lives in &lt;a href=&quot;https://ralphloop.sh/blog/ai-agent-sandbox-network-policies/&quot;&gt;network policies for AI agent sandboxes&lt;/a&gt;. The point versus a plain container: you get this gate without writing iptables rules.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;if-you-must-use-a-plain-container-harden-it&quot;&gt;If you must use a plain container, harden it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Sometimes a microVM is not available in your environment and a container is what you have. You can close most of the obvious leaks with flags. This will not give you a separate kernel, so it is not equivalent to a microVM, but it is a real improvement over a naked &lt;code dir=&quot;auto&quot;&gt;docker run&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Run as a non-root user instead of root:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--user&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1000:1000&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Drop every Linux capability and add back only what you actually need:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--cap-drop=ALL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Block privilege escalation so a setuid binary cannot regain capabilities:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--security-opt=no-new-privileges&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Make the root filesystem read-only and give the agent a scratch space that is not your host:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--read-only&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--tmpfs&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/tmp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Cut the network entirely when a task does not need it, or attach a tightly scoped network when it does:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;none&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Cap resources so a runaway loop cannot starve the host:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--memory&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;4g&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--cpus&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--pids-limit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;512&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Put together, a hardened run looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--rm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--user&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1000:1000&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--cap-drop=ALL&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--security-opt=no-new-privileges&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--read-only&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--tmpfs&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/tmp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--memory&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;4g&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--cpus&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--pids-limit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;512&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;none&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;-v&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$PWD&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:/work&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-w&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/work&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;node:22&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Two things to keep in mind. Never mount the Docker socket into an agent container, because that hands the agent the daemon and therefore the host. And remember that all of this still rides on your shared kernel, so a kernel level escape defeats every flag above at once. Hardened containers raise the cost of an escape. A microVM changes what an escape even targets.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-ralph-defaults-to-sbx&quot;&gt;Why Ralph defaults to sbx&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph runs every agent inside a Docker Sandbox by default, and the reasoning is the whole post in one line: the sandbox is the boundary, so the agent is free to move fast inside it. Because the boundary is external and real, Ralph runs agents in bypass-permissions mode (Claude Code’s &lt;code dir=&quot;auto&quot;&gt;--dangerously-skip-permissions&lt;/code&gt;, with equivalents for other CLIs) without the usual dread. The danger of YOLO mode comes from the target on the host, and the microVM removes the target. The deeper version of that argument is in &lt;a href=&quot;https://ralphloop.sh/blog/bypass-permissions-yolo-mode-safely/&quot;&gt;running agents in YOLO mode safely&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The loop wires the sandbox lifecycle so you never manage it by hand. Each iteration computes the deterministic name, checks that &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt; exists, creates the sandbox on the first pass and attaches on later passes, runs the agent on exactly one task, and stops the sandbox through an exit trap when the run ends. The loop stops on an explicit completion promise (&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt;), not on a vibe, and those map to exit codes &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; reserved for hitting the iteration cap.&lt;/p&gt;
&lt;p&gt;Starting a run is the same regardless of which agent you pick:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gpt-5.5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Supported agents are &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt; (the default), &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt;. For the field guide on which CLI behaves best inside a long sandboxed loop, the &lt;a href=&quot;https://ralphloop.sh/blog/agentic-coding-clis/&quot;&gt;agentic coding CLIs&lt;/a&gt; pillar is the cross-hub companion to this one. The takeaway stands on its own: a plain container is where you start, and a per-sandbox microVM is the boundary you actually want around an autonomous agent.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;Is a Docker container enough to sandbox an AI coding agent?&lt;/h3&gt; &lt;p&gt;A plain container is a start, not a full boundary. It isolates the process and the filesystem you mount, which is better than running the agent on the host, but it shares your host kernel, often runs as root, and has open outbound network by default. For an untrusted agent running arbitrary commands, a microVM that gives each sandbox its own guest kernel is a stronger boundary.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What is the difference between a Docker Sandbox and a plain Docker container?&lt;/h3&gt; &lt;p&gt;A plain container shares the host kernel and isolates processes with namespaces and cgroups. A Docker Sandbox, run with the sbx CLI, gives each sandbox its own lightweight virtual machine with a separate guest kernel and a virtual machine monitor between it and the host. The sandbox also defaults to deny-by-default network egress, which a plain container does not.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Where does a hand-rolled container leak when running an agent?&lt;/h3&gt; &lt;p&gt;Four common places: the process runs as root by default, the container shares your host kernel so a kernel level escape reaches your machine, bind mounts write straight to your real files and can expose more than intended, and the default network gives the agent full outbound access. Mounting the Docker socket is the worst case because it hands the agent control of the host.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How can I harden a plain container if I cannot use a microVM?&lt;/h3&gt; &lt;p&gt;Run as a non-root user, drop all capabilities with cap-drop ALL, set no-new-privileges, make the root filesystem read-only with a tmpfs scratch space, cap memory, CPU, and pids, and restrict the network. Never mount the Docker socket. These flags raise the cost of an escape but still rely on the shared kernel, so they are weaker than a separate guest kernel.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why does Ralph default to Docker Sandboxes instead of plain containers?&lt;/h3&gt; &lt;p&gt;Because the sandbox is the boundary that makes bypass-permissions mode safe. The microVM removes the target an agent could hit on the host, so the agent can run fast and autonomously while the worst case stays contained. Ralph also automates the lifecycle, computing a deterministic sandbox name and creating, attaching, and stopping the sandbox across loop iterations.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>sandboxing</category></item><item><title>Task Lookup Tables: Scaling Autonomous Agents to Hundreds of Tasks</title><link>https://ralphloop.sh/blog/task-lookup-table-for-agents/</link><guid isPermaLink="true">https://ralphloop.sh/blog/task-lookup-table-for-agents/</guid><description>A flat task list stops working past a few dozen entries. A lookup table that indexes per-task spec files lets an autonomous agent grind through hundreds of tasks without losing track.

</description><pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A flat task list does not scale. Once you push past a few dozen entries, a single file that holds every task plus every step, criterion, and note becomes too big to load into an agent’s context window on every iteration. The fix is a task lookup table: a lean index of tasks that points to a separate detailed spec file per task. The agent reads the small index to decide what to do next, then loads exactly one spec file to do it. That separation is what lets an autonomous loop work through hundreds of tasks without choking on its own backlog.&lt;/p&gt;
&lt;p&gt;This post shows how Ralph models that pattern with &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;, why the split scales, how status and priority selection work each iteration, and how you grow the table over time. It assumes you already understand &lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;spec-driven development with AI&lt;/a&gt; and want the data structure that makes a long run survivable.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-a-task-lookup-table-is&quot;&gt;What a task lookup table is&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A lookup table is an index. Instead of one giant document that mixes the list of work with the detail of each item, you keep two layers.&lt;/p&gt;
&lt;p&gt;The first layer is a flat array where each entry is small: an id, a title, a category, a pointer to the detail file, and a status flag. That is the lookup table. It answers one question fast: what is left to do, and which thing is next.&lt;/p&gt;
&lt;p&gt;The second layer is one spec file per task. Each file is the full contract for a single unit of work: description, acceptance criteria, ordered steps, dependencies, complexity, and technical notes. The agent only opens this when it has already decided to work that task.&lt;/p&gt;
&lt;p&gt;The reason to split is the same reason a database keeps an index separate from the rows. You scan the cheap thing to find the right record, then you read the expensive thing once. An agent that has to load 200 fully detailed tasks just to pick the next one burns its context window before it writes a line of code.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-ralph-models-it-tasksjson-plus-per-task-specs&quot;&gt;How Ralph models it: tasks.json plus per-task specs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph keeps the index in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt; and the detail in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;. The root file is deliberately thin. Each entry carries only what the loop needs to choose a task.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Verify project prerequisites and access&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;category&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;specFilePath&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;.agent/tasks/TASK-1.json&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;passes&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-2&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;User table with authentication fields&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;category&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;data-model&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;specFilePath&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;.agent/tasks/TASK-2.json&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;passes&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-3&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST /api/auth/register creates new user account&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;category&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;api-endpoint&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;specFilePath&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;.agent/tasks/TASK-3.json&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;passes&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Five fields, nothing more. The &lt;code dir=&quot;auto&quot;&gt;specFilePath&lt;/code&gt; is the pointer that turns this flat list into a lookup table. The &lt;code dir=&quot;auto&quot;&gt;passes&lt;/code&gt; flag is the status. Everything heavy lives behind the pointer, in the per-task file.&lt;/p&gt;
&lt;p&gt;Here is what one of those spec files holds. This is &lt;code dir=&quot;auto&quot;&gt;TASK-3.json&lt;/code&gt;, the detail behind the one-line index entry above.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-3&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST /api/auth/register creates new user account&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;category&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;api-endpoint&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Validate input, hash the password, store the user, and return a success response.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;acceptanceCriteria&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST with a valid email and password returns 201 with the user id and email&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Invalid email format returns 400 with the error text Please enter a valid email&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Password shorter than 8 characters returns 400&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Duplicate email returns 409, not a generic 500&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;The stored password starts with $2b$ and is never the plaintext value&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;steps&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;step&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Add the register route handler&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;details&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Validate with a zod schema, hash with bcrypt, insert into the users table.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;pass&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;step&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Write Vitest cases for every acceptance criterion&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;details&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Cover valid registration, invalid email, short password, duplicate email, and the stored hash prefix.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;pass&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-2&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;estimatedComplexity&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;medium&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;technicalNotes&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Never log passwords, even in error branches&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Return 409 on duplicate email rather than a generic 500&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Notice the asymmetry. The index entry for &lt;code dir=&quot;auto&quot;&gt;TASK-3&lt;/code&gt; is five lines. The spec file is forty. Multiply that across a real project and the savings are obvious: a 200 task project has a &lt;code dir=&quot;auto&quot;&gt;tasks.json&lt;/code&gt; of roughly a thousand lines of thin entries, while the detail sits in 200 separate files the agent never loads all at once.&lt;/p&gt;
&lt;p&gt;The diagram below is the whole idea. The loop scans the lean table, picks one id, follows its &lt;code dir=&quot;auto&quot;&gt;specFilePath&lt;/code&gt;, and loads only that file into the working context.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart LR
  Index[&quot;tasks.json: lean index, one line per task&quot;]
  Index --&gt; Scan[&quot;Scan for highest-priority task where passes is false&quot;]
  Scan --&gt; Pick[&quot;Selected: TASK-3&quot;]
  Pick --&gt; Path[&quot;Follow specFilePath&quot;]
  Path --&gt; Spec[&quot;tasks/TASK-3.json: full spec loaded into context&quot;]
  Spec --&gt; Work[&quot;Work the steps, run the gate&quot;]
  Work --&gt; Flip[&quot;Set passes true in tasks.json, commit&quot;]
  Index -. &quot;not loaded&quot; .-&gt; Rest[&quot;tasks/TASK-4.json ... TASK-200.json&quot;]&lt;/pre&gt;
&lt;p&gt;The dotted edge is the point. Every other spec file stays on disk, unread, until its turn comes. The agent never pays the token cost of a task it is not working.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-this-scales-to-hundreds-of-tasks&quot;&gt;Why this scales to hundreds of tasks&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The scaling property comes from one fact: the agent only loads the one task it needs. Every iteration of &lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;the Ralph loop&lt;/a&gt; starts the agent with a fresh context window. It does not carry the previous iteration in chat history. It rebuilds its understanding from files. So the cost of each iteration is whatever the agent reads off disk, not the size of the whole project.&lt;/p&gt;
&lt;p&gt;Walk through the math. Suppose the average per-task spec is 40 lines and the project has 200 tasks. If the agent had to load every detailed task to orient itself, that is 8000 lines on every single iteration, repeated 200 times. With a lookup table, each iteration loads the thin index (around 1000 lines of one-line entries) plus exactly one spec file (40 lines). The detail you load grows by one file, not by the entire backlog, no matter how large the table gets.&lt;/p&gt;
&lt;p&gt;That is why a flat, fully detailed task file hits a wall. It works fine at ten tasks. At a hundred it crowds out the actual code in the context window, and the agent starts skimming, missing acceptance criteria, and contradicting earlier decisions. Splitting the index from the detail keeps the working context flat as the project grows.&lt;/p&gt;
&lt;p&gt;The token economics reinforce this. Tokens in the context window cost money and attention on every pass. You do not want to spend that budget reloading 199 tasks the agent will not touch this iteration. A lean index plus one spec is the minimum the agent needs to choose work and do it correctly. This is the same instinct behind keeping a short &lt;code dir=&quot;auto&quot;&gt;SUMMARY.md&lt;/code&gt; for the PRD instead of resending the full document every iteration: load the index constantly, load the detail on demand.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;status-tracking-with-the-passes-flag&quot;&gt;Status tracking with the passes flag&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Status lives in two places, and the split mirrors the index-and-detail structure.&lt;/p&gt;
&lt;p&gt;At the index level, each entry in &lt;code dir=&quot;auto&quot;&gt;tasks.json&lt;/code&gt; has a single &lt;code dir=&quot;auto&quot;&gt;passes&lt;/code&gt; boolean. It starts &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt;. The agent only flips it to &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt; after the work is built and verified. The loop reads this flag to decide what is finished and what remains. Scanning for incomplete work is a pass over the lean index, which stays cheap even at hundreds of entries.&lt;/p&gt;
&lt;p&gt;At the detail level, each step inside a spec file has its own &lt;code dir=&quot;auto&quot;&gt;pass&lt;/code&gt; boolean. These track progress within a single task: step one done, step two not yet. A task is not complete until every step passes and the acceptance criteria hold. Only then does the top-level &lt;code dir=&quot;auto&quot;&gt;passes&lt;/code&gt; in the index flip.&lt;/p&gt;
&lt;p&gt;The rule that protects this system is strict: never flip &lt;code dir=&quot;auto&quot;&gt;passes&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt; until the work is verified. Tasks are generated with &lt;code dir=&quot;auto&quot;&gt;passes: false&lt;/code&gt; and stay false until the agent runs the verification stack the loop assumes, which is Playwright for end to end, Vitest for unit tests, TypeScript for types, ESLint for lint, and Prettier for format. The repo mantra is blunt: if you didn’t test it, it doesn’t work. A status flag that flips on a vibe instead of a passing gate turns the lookup table into a lie, and a fresh-context agent will trust that lie on the next iteration.&lt;/p&gt;
&lt;p&gt;Because status is just a flag in a file, the run is resumable and auditable. Any fresh agent on any machine can read &lt;code dir=&quot;auto&quot;&gt;tasks.json&lt;/code&gt;, see which entries are still &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt;, and pick up exactly where the last one stopped. Watching those flags flip across a run, alongside the per-iteration history and screenshots, is the basis of &lt;a href=&quot;https://ralphloop.sh/blog/observability-for-ai-coding-agents/&quot;&gt;observability for autonomous coding agents&lt;/a&gt;. The table is both the work queue and the progress report.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;priority-selection-each-iteration&quot;&gt;Priority selection each iteration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Picking the next task is a scan over the index, and the order is not arbitrary. Each iteration, the loop finds the highest-priority incomplete task in &lt;code dir=&quot;auto&quot;&gt;tasks.json&lt;/code&gt;, then loads that one spec file and works its steps.&lt;/p&gt;
&lt;p&gt;Two things shape the selection.&lt;/p&gt;
&lt;p&gt;First, dependencies. Each spec file carries a &lt;code dir=&quot;auto&quot;&gt;dependencies&lt;/code&gt; array of task ids that must finish before it can start. &lt;code dir=&quot;auto&quot;&gt;TASK-3&lt;/code&gt; (the register endpoint) lists &lt;code dir=&quot;auto&quot;&gt;[&quot;TASK-1&quot;, &quot;TASK-2&quot;]&lt;/code&gt;, so the loop will not select it until the prerequisite gate and the users table are both &lt;code dir=&quot;auto&quot;&gt;passes: true&lt;/code&gt;. The agent never builds on a foundation that is not there yet.&lt;/p&gt;
&lt;p&gt;Second, the prerequisite gate. &lt;code dir=&quot;auto&quot;&gt;TASK-1&lt;/code&gt; is always reserved for prerequisite verification: environment variable placeholders exist, database access works, required tools are authenticated, and any open gaps have an explicit proceed or block decision. Every downstream task that needs those prerequisites lists &lt;code dir=&quot;auto&quot;&gt;TASK-1&lt;/code&gt; as a dependency. You do not want an agent discovering halfway through a 200 task run that it never had database credentials.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
  Start[&quot;Fresh context&quot;] --&gt; Read[&quot;Read tasks.json index&quot;]
  Read --&gt; Filter[&quot;Filter to passes false&quot;]
  Filter --&gt; Deps{&quot;Dependencies satisfied?&quot;}
  Deps --&gt;|&quot;no, skip&quot;| Next[&quot;Try next candidate&quot;]
  Next --&gt; Deps
  Deps --&gt;|&quot;yes&quot;| Select[&quot;Select highest-priority task&quot;]
  Select --&gt; Load[&quot;Load its TASK-ID.json spec&quot;]
  Load --&gt; Build[&quot;Work the steps&quot;]
  Build --&gt; Gate[&quot;Tests, lint, types, screenshot&quot;]
  Gate --&gt;|&quot;fail&quot;| Build
  Gate --&gt;|&quot;pass&quot;| Update[&quot;Set passes true, commit&quot;]
  Update --&gt; Stop[&quot;Stop. Next iteration starts clean&quot;]&lt;/pre&gt;
&lt;p&gt;The loop completes exactly one task per invocation, commits, and stops. It never batches. That discipline is what keeps a long run reliable, and the reasoning behind it is covered in &lt;a href=&quot;https://ralphloop.sh/blog/one-task-per-iteration/&quot;&gt;one task per iteration&lt;/a&gt;. The lookup table is what makes one-task-per-iteration cheap to execute: selection is a quick scan of flags and dependencies, not a re-read of the entire project.&lt;/p&gt;
&lt;p&gt;You control how many of these iterations run. The default is 10. Run more with a flag.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@pageai/ralph-loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If the table has 200 tasks and you cap the loop at 50 iterations, the run stops at the cap with exit code 1 (MAX_ITERATIONS) and the remaining tasks stay &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt; in the index, ready for the next run to resume. Nothing is lost. The lookup table is the durable record.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;adding-tasks-over-time-with-the-prd-creator-skill&quot;&gt;Adding tasks over time with the prd-creator skill&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A lookup table is not a frozen document. You grow it as the project grows, and you do not hand-edit a 200 entry JSON file to do that.&lt;/p&gt;
&lt;p&gt;Ralph ships a &lt;code dir=&quot;auto&quot;&gt;prd-creator&lt;/code&gt; skill that turns unstructured requirements into a PRD plus a task list. The first time you run it, it interviews you, writes &lt;code dir=&quot;auto&quot;&gt;.agent/prd/PRD.md&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;.agent/prd/SUMMARY.md&lt;/code&gt;, then generates &lt;code dir=&quot;auto&quot;&gt;tasks.json&lt;/code&gt; with one &lt;code dir=&quot;auto&quot;&gt;TASK-{ID}.json&lt;/code&gt; spec per task. &lt;code dir=&quot;auto&quot;&gt;TASK-1&lt;/code&gt; is always the prerequisite gate, and every task is initialized with &lt;code dir=&quot;auto&quot;&gt;passes: false&lt;/code&gt;. For a typical project that is dozens to hundreds of entries, not five, because the skill keeps tasks small: anything too complex to finish in a short sitting gets split.&lt;/p&gt;
&lt;p&gt;When you want to add a feature or fix a bug later, you run the skill again. It updates the PRD and appends new tasks to the index, each with its own spec file, each starting &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt;. The completed entries keep their &lt;code dir=&quot;auto&quot;&gt;passes: true&lt;/code&gt; status, so the loop ignores them and works only the new &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt; ones. The table grows without disturbing the finished work.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Use the prd-creator skill in plan mode. Add a password reset flow to the&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;existing PRD and append the new tasks to .agent/tasks.json with one spec&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;file each under .agent/tasks/.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Run it in plan mode, where the agent is read-only and asks questions instead of writing code. The skill decomposes the new feature into atomic tasks the same way it did the first batch, which is its own discipline covered in &lt;a href=&quot;https://ralphloop.sh/blog/break-prd-into-agent-tasks/&quot;&gt;breaking a PRD into atomic agent tasks&lt;/a&gt;. The result is a lookup table that accretes work over the life of the project while staying scannable, because the index entries stay thin no matter how many you add.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-to-go-next&quot;&gt;Where to go next&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you are building the spec that drives a long run, read across the spec-driven cluster:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;Spec-driven development with AI&lt;/a&gt; for the full Specify, Plan, Tasks, Implement workflow the lookup table sits inside.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/break-prd-into-agent-tasks/&quot;&gt;Breaking a PRD into atomic agent tasks&lt;/a&gt; for decomposing work into the packets that fill the table.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/one-task-per-iteration/&quot;&gt;One task per iteration&lt;/a&gt; for the rule that makes the lookup table cheap to execute.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For watching the table fill in across a run, with logs, history, and screenshots, read &lt;a href=&quot;https://ralphloop.sh/blog/observability-for-ai-coding-agents/&quot;&gt;observability for autonomous coding agents&lt;/a&gt;.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What is a task lookup table for an AI agent?&lt;/h3&gt; &lt;p&gt;It is a two-layer structure that separates the list of work from the detail of each item. The first layer is a lean index where each entry has an id, title, category, a pointer to a detail file, and a status flag. The second layer is one spec file per task with the full description, acceptance criteria, steps, and dependencies. The agent scans the cheap index to pick the next task, then loads only that one spec file to do the work.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why does a flat task list stop scaling for autonomous agents?&lt;/h3&gt; &lt;p&gt;A flat list that holds every task plus all of its detail gets too large to load into the context window on every iteration. At ten tasks it is fine. At a hundred it crowds out the actual code, so the agent skims, misses acceptance criteria, and contradicts earlier decisions. A lookup table keeps the working context flat because each iteration loads the thin index plus exactly one detailed spec, no matter how many tasks exist.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does Ralph store tasks on disk?&lt;/h3&gt; &lt;p&gt;The index is .agent/tasks.json, a flat array where each entry has id, title, category, specFilePath, and a passes flag. The detail for each task lives in .agent/tasks/TASK-{ID}.json with description, acceptance criteria, ordered steps, dependencies, estimated complexity, and technical notes. The agent reads these files fresh on every iteration, so the filesystem is the memory rather than the chat history.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does the agent choose which task to work next?&lt;/h3&gt; &lt;p&gt;Each iteration it scans tasks.json for the highest-priority entry where passes is false and whose dependencies are already satisfied. TASK-1 is always prerequisite verification, and downstream tasks list it as a dependency, so feature work never starts before access and environment are confirmed. The agent loads that one spec file, works the steps, runs the verification gate, then flips passes to true and commits.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I add more tasks to the lookup table over time?&lt;/h3&gt; &lt;p&gt;Run the prd-creator skill again in plan mode. It updates the PRD and appends new tasks to tasks.json, each with its own spec file under .agent/tasks/ and each initialized with passes set to false. Completed entries keep their passes true status, so the loop ignores them and works only the new false ones. The table grows without disturbing finished work and stays scannable because the index entries stay thin.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>spec-driven</category></item><item><title>Why You Should Sandbox Every Autonomous Coding Agent</title><link>https://ralphloop.sh/blog/why-sandbox-ai-coding-agents/</link><guid isPermaLink="true">https://ralphloop.sh/blog/why-sandbox-ai-coding-agents/</guid><description>An autonomous coding agent runs with your full user permissions, which means it can read your SSH keys and push to your remotes. A sandbox is the only blast radius cheap enough to lose.

</description><pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Sandbox every autonomous coding agent because the agent runs with your permissions. When you start an agent on your laptop, it does not run as some restricted service account. It runs as you, with your user, which means it can read your SSH keys, your cloud credentials, and your full git history, and it can execute any shell command those permissions allow. A sandbox moves all of that out of reach, so the worst case becomes a throwaway environment you reset instead of a credential leak you cannot undo. That is the entire argument for &lt;code dir=&quot;auto&quot;&gt;why sandbox ai agent&lt;/code&gt; work, and the rest of this post is the detail behind it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;an-agent-runs-as-you-with-everything-you-can-touch&quot;&gt;An agent runs as you, with everything you can touch&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Think about what your own user account can do on a normal dev machine. You can read &lt;code dir=&quot;auto&quot;&gt;~/.ssh/id_ed25519&lt;/code&gt;, the private key that authenticates you to GitHub and every server you SSH into. You can read &lt;code dir=&quot;auto&quot;&gt;~/.aws/credentials&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;~/.config/gh/hosts.yml&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.env&lt;/code&gt; files scattered across projects, and the browser session cookies sitting in your home directory. You can &lt;code dir=&quot;auto&quot;&gt;git push --force&lt;/code&gt;, delete directories, and run a &lt;code dir=&quot;auto&quot;&gt;curl | bash&lt;/code&gt; line you copied from a README.&lt;/p&gt;
&lt;p&gt;An autonomous agent inherits all of it. The model is not malicious. It is a probabilistic system that emits shell commands, and shell commands do not ask for intent. When the agent decides to “clean up some old files” or “reset the environment to a known state,” there is no separate permission layer protecting your home directory. The agent is you, and you have access to everything.&lt;/p&gt;
&lt;p&gt;This is fine when a human drives each command and reviews it. Autonomy removes the human from that loop on purpose. The whole point of a Ralph-style loop is that the agent works for hours without you watching, picking up the next task in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, running commands, and committing. The thing that made interactive use safe (you reading each command before approving it) is exactly the thing autonomy deletes.&lt;/p&gt;
&lt;p&gt;So the question is not whether the agent will eventually run a command you would not have approved. Over a long enough run, it will. The question is what that command can reach when it does.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-blast-radius-problem-with-yolo-mode-on-your-laptop&quot;&gt;The blast radius problem with YOLO mode on your laptop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;To run unattended, an agent has to stop asking permission. In Claude Code that switch is &lt;code dir=&quot;auto&quot;&gt;--dangerously-skip-permissions&lt;/code&gt; (also &lt;code dir=&quot;auto&quot;&gt;--permission-mode bypassPermissions&lt;/code&gt;, documented in the &lt;a href=&quot;https://code.claude.com/docs&quot;&gt;Claude Code docs&lt;/a&gt;). Other CLIs ship their own version of the same idea, usually called YOLO mode. The naming is honest. You are telling the agent to execute whatever it decides, with no confirmation, for the entire run.&lt;/p&gt;
&lt;p&gt;On your host that is a genuinely bad idea, and the danger scales with the length of the run. One iteration is a handful of commands. A fifty-iteration overnight run is hundreds of commands, each one a chance for a hallucinated path, a destructive cleanup, or a misread instruction. You will not be awake to catch the one that matters.&lt;/p&gt;
&lt;p&gt;The damage is not limited to the project either. Because the agent runs as you, a single bad command can reach far outside the directory you pointed it at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It can read secrets from other projects and from your home directory, then paste them into a file, a log, or an outbound request.&lt;/li&gt;
&lt;li&gt;It can push to remotes you have credentials for, including repositories that have nothing to do with the current task.&lt;/li&gt;
&lt;li&gt;It can delete or rewrite files anywhere your user can write, with no undo.&lt;/li&gt;
&lt;li&gt;It can install and execute arbitrary code pulled from the network.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these require the model to “go rogue.” They are the normal capabilities of your shell, exposed to a system running without a gate. Running bypass-permissions mode directly on a host is the failure mode, not a feature. For the safe version of that same flag, see &lt;a href=&quot;https://ralphloop.sh/blog/bypass-permissions-yolo-mode-safely/&quot;&gt;running agents in YOLO mode safely&lt;/a&gt;.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart LR
  subgraph NoSandbox[&quot;Without a sandbox: blast radius is your whole machine&quot;]
    AgentA[&quot;Agent in YOLO mode (runs as you)&quot;]
    AgentA --&gt; Keys[&quot;SSH keys, cloud creds, cookies&quot;]
    AgentA --&gt; Repos[&quot;Every other git repo&quot;]
    AgentA --&gt; Net[&quot;Unrestricted network&quot;]
    AgentA --&gt; Proj1[&quot;Project directory&quot;]
  end
  subgraph WithSandbox[&quot;With a sandbox: blast radius is one disposable microVM&quot;]
    AgentB[&quot;Agent in YOLO mode (contained)&quot;]
    AgentB --&gt; Proj2[&quot;Project directory (only this)&quot;]
    AgentB --&gt; Gate[&quot;Network gate: deny-by-default&quot;]
    AgentB -. &quot;no path to&quot; .-&gt; HostKeys[&quot;Host keys and creds&quot;]
    AgentB -. &quot;no path to&quot; .-&gt; HostRepos[&quot;Other repos&quot;]
  end&lt;/pre&gt;
&lt;p&gt;The two pictures use the same agent in the same mode. The only thing that changes is what “anything it can do” resolves to. On the left it resolves to your machine. On the right it resolves to a microVM you can throw away.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-a-sandbox-makes-autonomy-acceptable&quot;&gt;Why a sandbox makes autonomy acceptable&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A sandbox does not make the agent trustworthy. It makes trust unnecessary. You stop trying to predict every command the model might emit and instead make the set of things any command can reach small and disposable. That inversion is what lets you walk away from a running agent and sleep.&lt;/p&gt;
&lt;p&gt;Concretely, a sandbox shrinks the worst case from “the agent leaked my SSH key and force-pushed to a client repo” to “the agent trashed a throwaway environment, so I reset it and re-ran the loop.” The first outcome is a security incident. The second is a Tuesday. Same model, same flags, completely different cost when something goes wrong.&lt;/p&gt;
&lt;p&gt;This is also why a sandbox is the precondition for real autonomy rather than a nice-to-have. Long unattended runs are the entire premise of &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-coding-agent-overnight/&quot;&gt;running an AI coding agent overnight&lt;/a&gt;, and you cannot responsibly leave an agent running for hours unless the place it runs is one you can afford to lose. The sandbox is what makes the overnight run a reasonable decision instead of a gamble with your credentials.&lt;/p&gt;
&lt;p&gt;There is a structural point underneath this. A permission prompt is not a security boundary, it is a question. The only real boundary is one enforced from outside the agent, by something the agent cannot reach through or talk its way around. A sandbox is that external boundary. The full version of this argument lives in the pillar guide, &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-agents-in-docker-sandboxes-safely/&quot;&gt;how to run AI coding agents in Docker Sandboxes safely&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-ralph-uses-docker-sandboxes-by-default&quot;&gt;How Ralph uses Docker Sandboxes by default&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph does not bolt on isolation as an option you remember to enable. It runs every agent inside a Docker Sandbox by default, using the &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt; CLI. Each agent gets a lightweight virtual machine with its own kernel, which is a stronger boundary than a namespaced process sharing your host kernel. The &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/&quot;&gt;Docker Sandboxes documentation&lt;/a&gt; is the primary source for how the microVM works underneath.&lt;/p&gt;
&lt;p&gt;You do not manage that lifecycle by hand. The script computes a deterministic sandbox name, checks that &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt; is installed, decides whether to create or attach, runs the agent in bypass-permissions mode inside the microVM, and stops the sandbox when the run ends. Because the sandbox is the boundary, the agent is free to skip permission prompts and move fast inside it.&lt;/p&gt;
&lt;p&gt;Installing and running looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@pageai/ralph-loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The first command drops Ralph into your project. The second runs the loop for fifty iterations, each one starting the agent with a fresh context inside the sandbox. Pick a different agent and pass agent-specific flags after a &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; separator:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gpt-5.5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Supported agents are &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt; (the default), &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt;. The default iteration count is 10, and &lt;code dir=&quot;auto&quot;&gt;--once&lt;/code&gt; runs exactly one iteration.&lt;/p&gt;
&lt;p&gt;The sandbox name is deterministic so the same project and agent pair always reuses the same microVM:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;current-dir&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;agent&gt;&lt;/code&gt; is the agent slug, &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;current-dir&gt;&lt;/code&gt; is the sanitized basename of the project directory, and &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;hash8&gt;&lt;/code&gt; is the first eight hex characters of a &lt;code dir=&quot;auto&quot;&gt;sha256&lt;/code&gt; of the absolute project path. The path hash keeps two same-named directories on different paths from colliding. You never have to memorize the name, because Ralph prints it on startup and on demand:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When the run ends, by normal exit, by a double Ctrl+C, or by any path that fires the exit trap, Ralph stops only the sandbox it started:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;stop&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Stopping is not deleting, so you can reattach later to inspect what the agent did, then remove it with &lt;code dir=&quot;auto&quot;&gt;sbx rm&lt;/code&gt; when you are finished.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-good-isolation-actually-looks-like&quot;&gt;What good isolation actually looks like&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Not every “sandbox” is a real boundary. A bare container that bind-mounts your whole home directory and has open network is theater. Three properties separate isolation that holds from isolation that only looks like it does.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;mount-only-the-project-nothing-else&quot;&gt;Mount only the project, nothing else&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The agent should see the project directory and nothing above it. Ralph shares your project at the same absolute path it has on your host, so tooling, config, and lockfiles resolve identically, while the rest of your home directory stays outside. No SSH keys, no &lt;code dir=&quot;auto&quot;&gt;~/.aws&lt;/code&gt;, no unrelated repositories, no shell history full of tokens. The blast radius becomes the project you pointed at, plus whatever network you explicitly allow.&lt;/p&gt;
&lt;p&gt;The corollary matters: because the project is shared, the agent can absolutely wreck your working tree. The protection there is git, not the sandbox. Work on a branch and commit often. The sandbox protects everything outside the project, and version control protects the code inside it.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;ephemeral-by-design&quot;&gt;Ephemeral by design&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The environment has to be cheap to destroy and recreate. If resetting a contaminated sandbox is a multi-hour chore, you will avoid resetting it, and a sandbox you never reset slowly turns back into a pet you are afraid to lose. Ralph leans on the deterministic name here: tear a sandbox down with &lt;code dir=&quot;auto&quot;&gt;sbx rm&lt;/code&gt;, and the next iteration simply probes, finds nothing, and creates a clean one. That re-probe each iteration is what makes a long run resilient to you poking at the sandbox by hand.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;network-limited-not-wide-open&quot;&gt;Network limited, not wide open&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Filesystem isolation is half the boundary. The other half is the network, because an agent with unrestricted outbound access can fetch arbitrary code and, in the worst case, send data out. Docker Sandboxes default to blocking outbound HTTP and HTTPS, then you allowlist exactly what the task needs:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-claude-my-app-a1b2c3d4&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;*.npmjs.org,github.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The practical symptom of deny-by-default is that &lt;code dir=&quot;auto&quot;&gt;npm install&lt;/code&gt; fails or an API call is refused until you grant the domain. That is the gate doing its job. You open specific hosts, not everything. There is a full-open escape hatch with the &lt;code dir=&quot;auto&quot;&gt;&quot;**&quot;&lt;/code&gt; rule, and it is the right tool only when you genuinely cannot enumerate the domains and you accept the tradeoff for that one sandbox. Building a tight allowlist that lets installs through while keeping exfiltration out is its own topic, covered in &lt;a href=&quot;https://ralphloop.sh/blog/ai-agent-sandbox-network-policies/&quot;&gt;network policies for AI agent sandboxes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A plain container can satisfy some of this and miss the rest, which is why the kind of sandbox matters. For where a hand-rolled container leaks and where a microVM holds, read &lt;a href=&quot;https://ralphloop.sh/blog/docker-sandboxes-vs-containers-for-agents/&quot;&gt;Docker Sandboxes vs plain containers for AI agents&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-quick-test-before-you-run-an-agent-unsandboxed&quot;&gt;A quick test before you run an agent unsandboxed&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you are tempted to skip the sandbox for a “quick” autonomous run, ask one question: if this agent ran &lt;code dir=&quot;auto&quot;&gt;rm -rf ~&lt;/code&gt; or piped the contents of &lt;code dir=&quot;auto&quot;&gt;~/.ssh&lt;/code&gt; to a pastebin, what would it cost you? If the honest answer is “a rebuild and some annoyance,” you are already in a disposable environment and you are fine. If the answer involves rotating credentials, notifying anyone, or the phrase “client repository,” you need the boundary before you start the loop.&lt;/p&gt;
&lt;p&gt;The reason to make this the default rather than a judgment call is that the judgment is the part autonomy removes. You are not going to evaluate the risk of each of the next four hundred commands. You evaluate the environment once, up front, and then let the agent be fearless inside it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-the-boundary-ends&quot;&gt;Where the boundary ends&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A sandbox is a strong boundary and not a magic one. Two limits are worth stating so you do not over-trust the setup.&lt;/p&gt;
&lt;p&gt;First, the shared project directory is genuinely shared. The agent can corrupt your working tree, and only git will save you. Branch and commit.&lt;/p&gt;
&lt;p&gt;Second, whatever you allowlist is genuinely reachable. Grant a domain that accepts uploads and an agent could in principle send data there. Keep network grants minimal, specific, and reviewed, the same way you would treat firewall rules, and avoid the &lt;code dir=&quot;auto&quot;&gt;&quot;**&quot;&lt;/code&gt; rule except on purpose.&lt;/p&gt;
&lt;p&gt;Inside those limits the model is simple and it holds. Enforce the boundary from the outside, give the agent a disposable place to be fearless, and let the loop run. The sandbox is the blast radius, so fast and autonomous becomes the same thing as contained.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;Why do I need to sandbox an AI coding agent at all?&lt;/h3&gt; &lt;p&gt;An autonomous agent runs with your user permissions, so it can read your SSH keys, cloud credentials, and git history, push to your remotes, and delete files anywhere you can write. In autonomous mode there is no human reviewing each command, so over a long run the agent will eventually execute something you would not have approved. A sandbox limits what any command can reach to a disposable environment.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What is the worst case if I run an agent without a sandbox?&lt;/h3&gt; &lt;p&gt;The worst case is a security incident rather than an inconvenience. A single bad command can leak credentials from your home directory, force-push to an unrelated repository, or delete files with no undo, because the agent has your full permissions. With a sandbox the same bad command only touches a throwaway microVM that you reset and re-run.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Is bypass-permissions or YOLO mode ever safe?&lt;/h3&gt; &lt;p&gt;It is unsafe on your host and safe inside a sandbox. On the host, --dangerously-skip-permissions gives the agent full shell access with no confirmation. Inside a Docker Sandbox microVM the same flag only grants access to the shared project directory and an allowlisted network, so the danger has no target on your machine.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Does Ralph sandbox agents automatically?&lt;/h3&gt; &lt;p&gt;Yes. Ralph runs every agent inside a Docker Sandbox by default using the sbx CLI. It computes a deterministic sandbox name, creates or attaches the microVM each iteration, runs the agent in bypass-permissions mode inside it, and stops the sandbox when the run ends.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What makes isolation good rather than just theater?&lt;/h3&gt; &lt;p&gt;Good isolation mounts only the project directory and nothing above it, stays ephemeral so it is cheap to destroy and recreate, and limits the network to an allowlist instead of leaving it wide open. A microVM with its own kernel is a stronger boundary than a container that shares your host kernel, especially for code you do not trust to behave.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>sandboxing</category></item><item><title>One Task Per Iteration: The Rule That Makes Autonomous Agents Reliable</title><link>https://ralphloop.sh/blog/one-task-per-iteration/</link><guid isPermaLink="true">https://ralphloop.sh/blog/one-task-per-iteration/</guid><description>The most reliable rule for autonomous coding is one task per invocation, commit, then stop. Here is why batching tasks wrecks an agent loop and how Ralph enforces the rule.

</description><pubDate>Tue, 21 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The most reliable rule for autonomous coding is also the most boring: one task per invocation, commit, then stop. Each iteration the agent picks the single highest-priority incomplete task, works it to done, verifies it, commits, and exits. It never batches two tasks into one run. Break that rule and a loop that should grind cleanly through fifty tasks turns into a branch full of half-finished, hard-to-review work.&lt;/p&gt;
&lt;p&gt;This rule is the operational core of a Ralph loop, and it sits one level below the &lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;spec-driven development workflow&lt;/a&gt; that produces the task list in the first place. The spec decides what the agent builds. This rule decides how it builds it without losing the plot.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-rule-one-task-one-commit-one-stop&quot;&gt;The rule: one task, one commit, one stop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Stated as plainly as it goes in the repo’s own &lt;code dir=&quot;auto&quot;&gt;AGENTS.md&lt;/code&gt;: one task per invocation. When working from &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, the agent completes exactly one task, commits, and stops. It never batches multiple tasks.&lt;/p&gt;
&lt;p&gt;That is the whole rule. Three verbs: complete, commit, stop. The reason it is worth a thousand words is that every instinct (yours and the model’s) pushes the other way. You have fifty tasks and an agent that can clearly do more than one. Letting it power through feels efficient. It is not. It is the fastest way to turn a clean autonomous run into a debugging session.&lt;/p&gt;
&lt;p&gt;An iteration that follows the rule produces one self-contained unit of progress: a feature built, its tests passing, its types checked, and a single commit that maps to exactly one task in the list. The next iteration starts from a known-good state. That property, every iteration ends in a verified checkpoint, is what makes long runs survivable.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-batching-tasks-wrecks-an-agent-loop&quot;&gt;Why batching tasks wrecks an agent loop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Batching means telling the agent (or letting it decide) to knock out three or five tasks before it commits. It looks like a throughput win. In practice it degrades the run in three compounding ways.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;context-bloat-and-context-rot&quot;&gt;Context bloat and context rot&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A coding agent has a finite context window and finite attention inside it. The longer a single session runs, the more it fills with intermediate state: files it opened, tests it ran, dead ends it explored, decisions it half-remembers. This is context rot. The agent starts contradicting earlier choices, re-editing files it already finished, and forgetting which of the five tasks it was actually on.&lt;/p&gt;
&lt;p&gt;One task per iteration keeps the working context small and on-topic. The agent reads the summary and the one spec it needs, builds the one thing, and exits before the window gets crowded. Batching does the opposite: it lets the context grow across multiple unrelated tasks, which is precisely the condition under which agents start producing confident nonsense. Context rot is one of the headline &lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-failure-modes/&quot;&gt;Ralph loop failure modes&lt;/a&gt;, and batching is the most direct route to it.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;half-finished-work-piles-up&quot;&gt;Half-finished work piles up&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;When an agent juggles several tasks at once, a failure on task four can leave tasks one through three in a partial state. It edited shared files, started a refactor it did not finish, and now nothing in the batch is cleanly done. You cannot ship any of it, and you cannot easily tell where the good work ends and the broken work begins.&lt;/p&gt;
&lt;p&gt;One task per iteration makes failure local. If the agent cannot finish the current task, it emits a &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; and stops, and everything committed before that point is intact and verified. You lost one iteration, not five tasks of tangled progress.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;commits-get-impossible-to-verify&quot;&gt;Commits get impossible to verify&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A commit that contains one task is reviewable. The diff maps to a single entry in &lt;code dir=&quot;auto&quot;&gt;tasks.json&lt;/code&gt;, the acceptance criteria for that task tell you what to check, and the tests in the diff prove it. A commit (or worse, a single giant commit) that contains five tasks is archaeology. You cannot bisect it, you cannot revert one piece of it, and you cannot read why any single change exists six months later.&lt;/p&gt;
&lt;p&gt;Clean, atomic commits are not a nicety here. They are the audit trail that makes you trust a diff you did not write. Batching trades that trail for a vague sense of speed you do not actually get, because the review and rework cost lands on you in the morning.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;fresh-context-per-iteration-is-why-one-task-works&quot;&gt;Fresh context per iteration is why one task works&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;One task per iteration only makes sense because of the other half of the design: every iteration starts the agent with a fresh context window. The loop does not carry chat history forward. It reboots the agent’s understanding from files on disk each time.&lt;/p&gt;
&lt;p&gt;This is the central idea of the &lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;Ralph technique&lt;/a&gt;, popularized by Geoffrey Huntley in his &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;original Ralph writeup&lt;/a&gt;. The agent’s memory is not the conversation. It is the filesystem and the git history: &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, the per-task specs, the logs, and the commits. Because state lives on disk, a fresh agent can reconstruct exactly where the project stands and pick up cleanly.&lt;/p&gt;
&lt;p&gt;Fresh context and one-task scope are two sides of the same coin. Fresh context per iteration only helps if the unit of work fits inside one clean window, and one task is the unit sized to do that. Pair them and each iteration is crisp: clean slate in, one verified task out. Try to batch tasks on top of fresh context and you reintroduce the bloat that fresh context was meant to prevent.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
  Fresh[&quot;Fresh context window&quot;] --&gt; Read[&quot;Read SUMMARY.md and tasks.json&quot;]
  Read --&gt; Pick[&quot;Pick highest-priority incomplete task&quot;]
  Pick --&gt; Spec[&quot;Open one TASK-{ID}.json&quot;]
  Spec --&gt; Work[&quot;Work the steps for that one task&quot;]
  Work --&gt; Gate[&quot;Verify: tests, lint, types, screenshot&quot;]
  Gate --&gt;|&quot;fail&quot;| Work
  Gate --&gt;|&quot;pass&quot;| Commit[&quot;Set passes true and commit one task&quot;]
  Commit --&gt; Stop[&quot;Stop. Iteration ends at a checkpoint&quot;]
  Stop --&gt; Next[&quot;Next iteration starts with fresh context&quot;]
  Next --&gt; Fresh&lt;/pre&gt;
&lt;p&gt;Read that diagram as a single lap. The only way out of a lap is a verified commit or an explicit stop signal. There is no path where the agent commits two tasks in one pass, because the lap ends the moment one task is done.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;clean-commits-are-your-checkpoints-and-rollback-points&quot;&gt;Clean commits are your checkpoints and rollback points&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Treat each per-task commit as a save point in a game. When the agent finishes a task and commits, it records a state you can return to. If iteration twelve goes sideways, you do not lose the eleven good iterations before it. You reset to the last clean commit and keep the verified work.&lt;/p&gt;
&lt;p&gt;This is why the commit is not optional and not deferred. The loop’s standard iteration ends by updating the task status, taking a screenshot for UI work, and committing in Conventional Commit format. One task, one commit, one checkpoint. The git log becomes a ledger of progress where each line is a unit you can read, verify, and if necessary revert in isolation.&lt;/p&gt;
&lt;p&gt;Batching destroys this. A commit that bundles several tasks is a single, coarse save point. Revert it and you lose good work alongside the bad. Bisect it and every “bad” commit contains multiple changes, so you cannot pin the regression to one task. The granularity of your commits is the granularity of your recovery, and one task per commit is the finest useful grain.&lt;/p&gt;
&lt;p&gt;There is a second benefit that matters for long runs. Because every checkpoint is verified before it is committed, the branch is always in a shippable-ish state between iterations. You can stop the loop at any point, after iteration three or after iteration thirty, and what you have is a set of completed, tested tasks rather than a half-built mess. That is what lets you run an agent overnight and trust the morning diff.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-ralph-enforces-one-task-per-iteration&quot;&gt;How Ralph enforces one task per iteration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The rule is not a suggestion the agent is free to ignore. It is wired into how the loop and the prompt work together.&lt;/p&gt;
&lt;p&gt;The loop itself is a Bash script (&lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt;) that runs the agent once per iteration. Each iteration follows the same shape:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the highest-priority incomplete task in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Work the ordered steps in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run tests, linting, and type checking.&lt;/li&gt;
&lt;li&gt;Complete the task, take a screenshot, update task status, and commit.&lt;/li&gt;
&lt;li&gt;Repeat until all tasks pass or the iteration cap is reached.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Step one is where scope gets enforced: the agent selects the highest-priority incomplete task, singular. The prompt sent each iteration tells the agent to complete that one task, commit, and stop rather than continuing to the next. Because the next iteration starts a brand new agent with fresh context, there is no natural way to “keep going” across tasks. The process boundary between iterations is the enforcement mechanism.&lt;/p&gt;
&lt;p&gt;You control how many iterations run. The default is ten. Use the flag to set your own cap:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Run up to 50 iterations (50 tasks, one per iteration)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Run exactly one iteration to watch a single task closely&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;./ralph.sh --once&lt;/code&gt; is the cleanest demonstration of the rule. It runs a single iteration: one task, one commit, one stop. Use it the first time you point Ralph at a new task list, watch it complete exactly one task, review the commit, and only then turn it loose with a larger cap.&lt;/p&gt;
&lt;p&gt;The loop does not stop on a feeling. It stops on an explicit signal. After the run, the agent emits a promise tag and the script maps it to an exit code:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;         all tasks finished      exit 0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;   needs human help        exit 2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;  needs a decision        exit 3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If the loop hits its iteration cap before the list is finished, it exits with code 1 (MAX_ITERATIONS). Either way, every task that did complete completed on its own iteration, with its own verified commit. The completion signal is per-run; the one-task discipline is per-iteration.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-about-tasks-that-are-too-small-or-too-big&quot;&gt;What about tasks that are too small or too big?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The obvious objection: if the agent does only one task per iteration, the task had better be the right size. That is true, and it is why scoping happens before the loop ever runs, during breakdown.&lt;/p&gt;
&lt;p&gt;If your tasks are too big, the agent runs out of room inside one iteration and you get half-finished work, the exact failure the rule was meant to prevent. The fix is not to relax the rule. It is to cut the task smaller. A good task is something an agent can finish in one short sitting, with its own acceptance criteria and its own verification. Getting tasks to that size is the subject of &lt;a href=&quot;https://ralphloop.sh/blog/break-prd-into-agent-tasks/&quot;&gt;breaking a PRD into atomic agent tasks&lt;/a&gt;, and it is the prerequisite that makes one task per iteration practical.&lt;/p&gt;
&lt;p&gt;If your tasks are too small, you simply burn an iteration on something trivial, which is cheap and harmless. The asymmetry matters: tasks that are slightly too small cost you a little time, tasks that are too big cost you a tangled branch. When in doubt, split.&lt;/p&gt;
&lt;p&gt;The other piece is ordering. One task per iteration only flows smoothly if the highest-priority incomplete task is actually buildable right now, with its dependencies already satisfied. That is what the task lookup table manages. Each task declares its dependencies, and the loop respects them so it never tries to build the dashboard before the data exists. Scaling that ordering to hundreds of tasks is covered in &lt;a href=&quot;https://ralphloop.sh/blog/task-lookup-table-for-agents/&quot;&gt;task lookup tables for agents&lt;/a&gt;. Get the breakdown and the ordering right, and one task per iteration is not a constraint you fight. It is the rhythm the whole run settles into.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-to-go-next&quot;&gt;Where to go next&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;One task per iteration is the discipline that turns a spec into a reliable run. To build the rest of the workflow around it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;Spec-driven development with AI&lt;/a&gt; for the full Specify, Plan, Tasks, Implement loop this rule lives inside.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/break-prd-into-agent-tasks/&quot;&gt;Breaking a PRD into atomic agent tasks&lt;/a&gt; for sizing tasks so one fits cleanly in one iteration.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/task-lookup-table-for-agents/&quot;&gt;Task lookup tables for agents&lt;/a&gt; for ordering and scaling to hundreds of tasks.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-failure-modes/&quot;&gt;Ralph loop failure modes&lt;/a&gt; for what goes wrong when this rule and its guardrails are not in place.&lt;/li&gt;
&lt;/ul&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What does one task per iteration mean?&lt;/h3&gt; &lt;p&gt;It means the agent completes exactly one task per run of the loop, commits the result, and stops. The next iteration starts a fresh agent that picks the next highest-priority incomplete task. The agent never batches several tasks into a single invocation.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why should an agent not do multiple tasks in one run?&lt;/h3&gt; &lt;p&gt;Batching causes three problems. The context window fills with unrelated state and the agent starts contradicting itself, which is context rot. A failure mid-batch leaves several tasks half-finished and unshippable. And the resulting commit bundles many changes, so it is hard to review, revert, or bisect. One task per iteration keeps context small, failures local, and commits atomic.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does one task per iteration relate to fresh context?&lt;/h3&gt; &lt;p&gt;They are two halves of the same design. Each iteration starts the agent with a fresh context window and reconstructs state from files on disk, not from chat history. That only works if the unit of work fits inside one clean window, and one task is the unit sized to do that. Together they keep every iteration crisp: clean slate in, one verified task out.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does Ralph enforce one task per iteration?&lt;/h3&gt; &lt;p&gt;The loop runs the agent once per iteration. Each iteration the agent finds the single highest-priority incomplete task in .agent/tasks.json, works only that task spec, runs tests, lint, and type checks, updates the status, commits, and stops. Because the next iteration starts a brand new agent with fresh context, there is no way to carry work across tasks. The process boundary between iterations is the enforcement.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What if a task is too big to finish in one iteration?&lt;/h3&gt; &lt;p&gt;Do not relax the rule, cut the task smaller. A good task is something an agent can finish in one short sitting with its own acceptance criteria. If the agent runs out of room inside one iteration, the breakdown was too coarse. Split the task during the breakdown phase so one task fits cleanly in one iteration.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>spec-driven</category></item><item><title>Breaking a PRD Into Atomic Agent Tasks</title><link>https://ralphloop.sh/blog/break-prd-into-agent-tasks/</link><guid isPermaLink="true">https://ralphloop.sh/blog/break-prd-into-agent-tasks/</guid><description>A PRD is too big for an agent to build in one shot. Here is how to decompose it into atomic, independently verifiable task packets the loop finishes one at a time.

</description><pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;To break a PRD into tasks an agent can build, you decompose it into atomic task packets: small, self-contained units of work, each with one objective, the files to inspect, ordered steps, and acceptance criteria a machine can check. The agent picks one packet, builds it, verifies it, commits, and stops. The PRD is the contract for the whole project. A task packet is the contract for one iteration.&lt;/p&gt;
&lt;p&gt;This post is the decomposition itself: what goes inside a single packet, how Ralph stores packets in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;, how to size each one so a single iteration can finish and commit, and how dependencies and priority decide the order the loop works them. It assumes you already have a buildable PRD. If you do not, start with &lt;a href=&quot;https://ralphloop.sh/blog/how-to-write-prd-for-ai-agent/&quot;&gt;how to write a PRD an AI agent can actually build from&lt;/a&gt;, then come back here to chop it up.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-a-prd-is-the-wrong-unit-of-work-for-an-agent&quot;&gt;Why a PRD is the wrong unit of work for an agent&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A PRD describes a finished product. An agent does not build finished products. It edits files in a single context window, and that window is finite. Hand it the whole PRD and tell it to build, and one of two things happens. It tries to do everything at once and produces a sprawling diff you cannot review, or it loses the plot halfway through and starts contradicting decisions it made an hour earlier. That second failure is context rot, and it gets worse the longer a single session runs.&lt;/p&gt;
&lt;p&gt;The fix is to never ask the agent to hold the whole project in its head. You decompose the PRD into the smallest pieces that still make sense on their own, and you feed the agent exactly one piece per iteration with a fresh context window each time. The &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;Ralph technique&lt;/a&gt; is built around this: each loop starts the agent clean, it reads the task list, it picks one packet, and the filesystem carries the memory between passes instead of a long chat history.&lt;/p&gt;
&lt;p&gt;So the real question is not “how do I get the agent to build my PRD.” It is “what is the smallest packet of work I can define that the agent can finish and verify in one sitting.” Get that unit right and the loop becomes reliable. Get it wrong and no amount of prompting saves you.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-goes-inside-an-atomic-task-packet&quot;&gt;What goes inside an atomic task packet&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A task packet is atomic when it has a single objective and everything the agent needs to hit it. Four parts make a packet complete.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The objective.&lt;/strong&gt; One sentence describing the one thing this packet delivers. Not “build authentication.” That is a feature, not a task. “POST /api/auth/register creates a new user account” is a task. If you cannot state the objective in a single sentence without the word “and,” the packet is too big and you split it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The files to inspect.&lt;/strong&gt; Name the files the agent should read before it touches anything, and the files it is expected to change. This is the cheapest way to stop an agent from inventing a parallel module next to the one you already have. In Ralph these live in the step &lt;code dir=&quot;auto&quot;&gt;details&lt;/code&gt; and in &lt;code dir=&quot;auto&quot;&gt;technicalNotes&lt;/code&gt;: “extend the existing &lt;code dir=&quot;auto&quot;&gt;auth&lt;/code&gt; module in &lt;code dir=&quot;auto&quot;&gt;src/lib/auth.ts&lt;/code&gt;, do not add a second one.” A packet that points the agent at the right files reuses code instead of duplicating it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The ordered steps.&lt;/strong&gt; The packet breaks the objective into a short sequence the agent works in order. Each step is concrete enough to act on, and the last step is almost always “write the tests that prove the acceptance criteria.” Tests are steps inside the packet, not a separate task scheduled for later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Testable acceptance criteria.&lt;/strong&gt; Each criterion is a condition the agent can confirm by running a command and reading the output. “Login works” is a vibe. “POST with a wrong password returns 401 and the body &lt;code dir=&quot;auto&quot;&gt;{ error: &apos;Invalid credentials&apos; }&lt;/code&gt;” is a criterion. The test is simple: can a machine return yes or no without your opinion? If not, rewrite it. The discipline of writing verifiable criteria is the same one that makes the PRD buildable, covered in &lt;a href=&quot;https://ralphloop.sh/blog/how-to-write-prd-for-ai-agent/&quot;&gt;how to write a PRD for an AI agent&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The packet is the unit of verification, not just the unit of work. When the agent finishes, it does not ask you whether it is done. It checks the criteria, and the criteria answer for it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-ralph-stores-this-a-lookup-table-plus-per-task-specs&quot;&gt;How Ralph stores this: a lookup table plus per-task specs&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph splits the task list into two layers, and the split is what lets a run scale to hundreds of packets without drowning the agent in detail.&lt;/p&gt;
&lt;p&gt;The root &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt; is a lookup table, not the detail. It is a flat list where each entry is a stub that points at a spec file. The agent scans this list every iteration to find the next thing to do, so it stays lean on purpose.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Verify project prerequisites and access&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;category&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;specFilePath&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;.agent/tasks/TASK-1.json&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;passes&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-2&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Add avatars storage bucket and config&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;category&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;infrastructure&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;specFilePath&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;.agent/tasks/TASK-2.json&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;passes&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-3&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST /api/avatar uploads and stores a user avatar&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;category&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;api-endpoint&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;specFilePath&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;.agent/tasks/TASK-3.json&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;passes&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Each stub carries a &lt;code dir=&quot;auto&quot;&gt;passes&lt;/code&gt; flag. It starts &lt;code dir=&quot;auto&quot;&gt;false&lt;/code&gt; and only flips to &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt; after the agent verifies the work for that packet. The loop reads this flag to decide what is left. Keeping the table this thin matters: a run with two hundred packets still scans fast, because the table holds titles and pointers, not the full contracts. That scaling pattern is its own topic in &lt;a href=&quot;https://ralphloop.sh/blog/task-lookup-table-for-agents/&quot;&gt;task lookup tables for agents&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The detail lives in the per-task spec at &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;. This is the full packet: the objective as a description, the files to inspect inside the steps, the ordered steps, the testable acceptance criteria, the dependencies, an estimated complexity, and any technical notes.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-3&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST /api/avatar uploads and stores a user avatar&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;category&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;api-endpoint&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Accept an image upload from an authenticated user, validate it, store it in the avatars bucket, and save the URL on the user row.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;acceptanceCriteria&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST with a valid PNG or JPEG under 2 MB returns 200 with the stored avatar URL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;A file over 2 MB returns 413 with the error text File too large&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;A non-image content type returns 415&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;An unauthenticated request returns 401&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;The avatar URL is persisted on the user row and survives a re-fetch&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;steps&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;step&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Add the upload route handler&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;details&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Read src/lib/auth.ts for the session helper and src/lib/storage.ts for the bucket client. Add POST /api/avatar, validate size and content type, upload, then update users.avatarUrl.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;pass&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;step&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Write tests for every acceptance criterion&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;details&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Add Vitest cases for valid upload, oversized file, wrong content type, unauthenticated request, and persistence after re-fetch.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;pass&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-2&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;estimatedComplexity&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;medium&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;technicalNotes&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Reuse the storage client in src/lib/storage.ts, do not add a second SDK&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Strip EXIF data before storing the image&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Notice the four parts of a packet map onto the JSON. &lt;code dir=&quot;auto&quot;&gt;description&lt;/code&gt; is the objective. The step &lt;code dir=&quot;auto&quot;&gt;details&lt;/code&gt; name the files to inspect and change. &lt;code dir=&quot;auto&quot;&gt;steps&lt;/code&gt; is the ordered sequence. &lt;code dir=&quot;auto&quot;&gt;acceptanceCriteria&lt;/code&gt; is the checkable definition of done. &lt;code dir=&quot;auto&quot;&gt;TASK-1&lt;/code&gt; is always reserved for prerequisite verification, so the agent confirms credentials, tools, and access exist before any feature work starts.&lt;/p&gt;
&lt;p&gt;Here is how a PRD turns into packets and lands in the lookup table.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
  PRD[&quot;prd/PRD.md: goals, constraints, criteria&quot;] --&gt; Decompose[&quot;Decompose into atomic packets&quot;]
  Decompose --&gt; P1[&quot;tasks/TASK-1.json: prerequisite gate&quot;]
  Decompose --&gt; P2[&quot;tasks/TASK-2.json: storage config&quot;]
  Decompose --&gt; P3[&quot;tasks/TASK-3.json: upload endpoint&quot;]
  Decompose --&gt; Pn[&quot;tasks/TASK-N.json: ...&quot;]
  P1 --&gt; Index[&quot;tasks.json lookup table (stubs + passes)&quot;]
  P2 --&gt; Index
  P3 --&gt; Index
  Pn --&gt; Index
  Index --&gt; Loop[&quot;ralph.sh: pick one packet per iteration&quot;]
  Loop --&gt; Verify[&quot;Build, test, screenshot, commit, set passes true&quot;]&lt;/pre&gt;
&lt;p&gt;The PRD is the source. Decomposition produces one spec file per packet. The lookup table indexes them. The loop reads the table, opens one spec, and works it. You do not write all of this by hand: the &lt;code dir=&quot;auto&quot;&gt;prd-creator&lt;/code&gt; skill generates the table and the spec files from your PRD, then you edit the packets that need sharpening.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;sizing-a-packet-so-one-iteration-can-finish-and-commit&quot;&gt;Sizing a packet so one iteration can finish and commit&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The hardest part of decomposition is sizing. Too big and the agent runs out of context before it finishes, leaves the packet half built, and the next iteration starts cold on a mess. Too small and you spend more iterations on overhead than on work. The target is the largest packet that still finishes, verifies, and commits inside a single iteration.&lt;/p&gt;
&lt;p&gt;A few concrete rules keep packets in the right range.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One objective, no “and.”&lt;/strong&gt; If the title needs the word “and” to be honest, it is two packets. “Add the upload endpoint and the avatar UI” is two: the endpoint is an api-endpoint packet, the UI is a ui-ux packet that depends on it. Split on the “and.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One area of the codebase.&lt;/strong&gt; A packet that touches the data model, the API, and the frontend in one pass is too wide. Each layer is its own packet. The migration is one. The endpoint that reads the new column is another. The component that renders it is a third. Narrow packets keep the diff readable and the failure isolated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verifiable in the steps it contains.&lt;/strong&gt; If proving the criteria would take more steps than building the feature, the packet is doing too much. A well sized packet has a handful of steps, and the test step covers the criteria without ballooning into a second project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A clean commit at the end.&lt;/strong&gt; The end state of every packet is a commit that builds, passes its tests, and changes nothing it did not need to. If you cannot imagine that commit as a single coherent change, the packet is wrong.&lt;/p&gt;
&lt;p&gt;The economics back this up. The loop runs one packet per iteration, and you cap iterations with &lt;code dir=&quot;auto&quot;&gt;./ralph.sh -n 50&lt;/code&gt; (the default is 10). A run of small packets makes steady, auditable progress: one commit per packet, one test suite per criterion, one screenshot per UI change. A run of giant packets thrashes, because the agent keeps almost finishing and never quite committing. When you want to watch a single packet land before turning the loop loose, run exactly one iteration:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@pageai/ralph-loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The rule that one invocation handles exactly one packet, commits, and stops is the reliability backbone of the whole approach. It has enough nuance to deserve its own treatment in &lt;a href=&quot;https://ralphloop.sh/blog/one-task-per-iteration/&quot;&gt;one task per iteration&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;dependencies-and-priority-ordering&quot;&gt;Dependencies and priority ordering&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Atomic packets are not independent of each other. The upload endpoint cannot exist before the storage bucket. The avatar UI cannot render a URL the endpoint does not yet return. Decomposition has to capture this order, or the agent builds on a foundation that is not there and the iteration fails.&lt;/p&gt;
&lt;p&gt;Ralph encodes order in two places. The &lt;code dir=&quot;auto&quot;&gt;dependencies&lt;/code&gt; array on each spec names the packets that must pass first. In the example above, &lt;code dir=&quot;auto&quot;&gt;TASK-3&lt;/code&gt; depends on &lt;code dir=&quot;auto&quot;&gt;[&quot;TASK-1&quot;, &quot;TASK-2&quot;]&lt;/code&gt;, so the loop will not start the upload endpoint until the prerequisite gate and the storage config both report &lt;code dir=&quot;auto&quot;&gt;passes: true&lt;/code&gt;. The agent respects this when it selects the next packet, which means you can write the whole task list up front without worrying that the agent jumps ahead.&lt;/p&gt;
&lt;p&gt;Priority decides what to do among packets that are all unblocked. Each iteration the loop finds the highest-priority incomplete task whose dependencies are satisfied, opens its spec, and works it. The selection logic each pass is small.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
  Start[&quot;Fresh context&quot;] --&gt; Scan[&quot;Scan tasks.json for incomplete packets&quot;]
  Scan --&gt; Ready{&quot;Dependencies all passes true?&quot;}
  Ready --&gt;|&quot;no&quot;| Skip[&quot;Skip, try next packet&quot;]
  Ready --&gt;|&quot;yes&quot;| Pick[&quot;Pick highest priority among ready packets&quot;]
  Skip --&gt; Scan
  Pick --&gt; Work[&quot;Open TASK-{ID}.json, work the steps&quot;]
  Work --&gt; Gate[&quot;Tests, lint, types, screenshot&quot;]
  Gate --&gt;|&quot;fail&quot;| Work
  Gate --&gt;|&quot;pass&quot;| Commit[&quot;Set passes true, commit, stop&quot;]&lt;/pre&gt;
&lt;p&gt;Two practices keep ordering dependable. First, front-load the foundation. The prerequisite gate is &lt;code dir=&quot;auto&quot;&gt;TASK-1&lt;/code&gt; for a reason: nothing should run before the agent confirms it has the access and tools to run at all. Data-model packets come early, because most feature packets depend on them. Second, keep dependency chains shallow. A packet that depends on a packet that depends on a packet is a sign the work is really one larger unit you split too aggressively, or that an intermediate packet is doing too little. Wide and shallow beats narrow and deep, because shallow graphs give the loop more ready packets to choose from at any moment, which keeps it moving even if one branch is blocked.&lt;/p&gt;
&lt;p&gt;When you need to reorder mid-run, you do not kill the loop. You edit &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt;, and the agent handles that critical work on its next iteration before returning to the task list. That is how you inject “fix the failing migration before anything else” without losing momentum.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-short-worked-example&quot;&gt;A short worked example&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Take “let users upload a profile avatar” from a PRD and watch it become packets.&lt;/p&gt;
&lt;p&gt;That sentence is a feature, not a task. Decomposition asks: what are the smallest verifiable units, and in what order. &lt;code dir=&quot;auto&quot;&gt;TASK-1&lt;/code&gt; is the prerequisite gate, always. &lt;code dir=&quot;auto&quot;&gt;TASK-2&lt;/code&gt; configures the storage bucket and is a dependency for everything that stores a file. &lt;code dir=&quot;auto&quot;&gt;TASK-3&lt;/code&gt; is the upload endpoint, depending on the bucket. &lt;code dir=&quot;auto&quot;&gt;TASK-4&lt;/code&gt; is the avatar component in the UI, depending on the endpoint because it renders the URL the endpoint returns. &lt;code dir=&quot;auto&quot;&gt;TASK-5&lt;/code&gt; handles deletion, depending on the upload existing.&lt;/p&gt;
&lt;p&gt;Each packet gets criteria a machine can check, not opinions. For the endpoint: a valid image under the size limit returns 200 with a URL, an oversized file returns 413, a wrong content type returns 415, an unauthenticated request returns 401. For the UI: the component shows a placeholder when no avatar is set, shows the image after a successful upload, and a Playwright run plus a screenshot confirms it. The feature that read as one line in the PRD is now five committed, tested steps, each small enough to finish in a single iteration, ordered so the loop never builds ahead of its foundation.&lt;/p&gt;
&lt;p&gt;The framing of phases that produces these packets (specify, plan, decompose into tasks, implement and verify) comes from &lt;a href=&quot;https://github.com/github/spec-kit&quot;&gt;GitHub Spec Kit&lt;/a&gt;, and the loop that runs the packets autonomously was popularized by Geoffrey Huntley in his &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;original Ralph writeup&lt;/a&gt;. Decomposition is the phase that decides whether the loop succeeds, which is why it is worth slowing down for.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-to-go-next&quot;&gt;Where to go next&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you are building this workflow, read across the spec-driven cluster:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;Spec-driven development with AI&lt;/a&gt; for the full Specify, Plan, Tasks, Implement workflow these packets sit inside.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/how-to-write-prd-for-ai-agent/&quot;&gt;How to write a PRD an AI agent can actually build from&lt;/a&gt; for the goals, constraints, and acceptance criteria you decompose here.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/one-task-per-iteration/&quot;&gt;One task per iteration&lt;/a&gt; for the rule that makes one-packet-at-a-time reliable.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/task-lookup-table-for-agents/&quot;&gt;Task lookup tables for agents&lt;/a&gt; for scaling the table to hundreds of packets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the mechanics of the loop that reads these packets on every pass and the fresh-context design behind it, start with &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;what is the Ralph technique&lt;/a&gt;.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;How do I break a PRD into tasks an AI agent can build?&lt;/h3&gt; &lt;p&gt;Decompose the PRD into atomic task packets, where each packet has one objective, the files to inspect, ordered steps, and acceptance criteria a machine can check by running a command and reading the output. Split on any objective that needs the word and, keep each packet inside one area of the codebase, and make sure the agent can finish and commit it in a single iteration.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What makes a task packet atomic?&lt;/h3&gt; &lt;p&gt;A packet is atomic when it has a single objective and everything the agent needs to hit it without holding the rest of the project in its head. That means one deliverable, the files to read and change, a short ordered sequence of steps with tests as the final step, and verifiable acceptance criteria. If you cannot state the objective in one sentence without the word and, the packet is too big.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does Ralph store a decomposed PRD?&lt;/h3&gt; &lt;p&gt;Ralph uses two layers. The root .agent/tasks.json is a lean lookup table of stubs, each with an id, title, category, a pointer to a spec file, and a passes flag. The detail lives in per-task specs at .agent/tasks/TASK-{ID}.json, which hold the description, acceptance criteria, ordered steps, dependencies, estimated complexity, and technical notes. The agent scans the table to pick a packet and opens the spec to work it.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How big should a single agent task be?&lt;/h3&gt; &lt;p&gt;The right size is the largest packet that still finishes, verifies, and commits inside one iteration. Too big and the agent runs out of context and leaves the work half done. Too small and overhead dominates. Aim for one objective, one area of the codebase, a handful of steps including the tests, and a single clean commit at the end.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do dependencies and priority decide task order?&lt;/h3&gt; &lt;p&gt;Each spec has a dependencies array naming the packets that must pass first, so the loop will not start a packet until its dependencies report passes true. Among packets whose dependencies are satisfied, the loop works the highest priority one. Front-load the prerequisite gate and data-model packets, keep dependency chains shallow, and edit STEERING.md to reorder mid-run without stopping the loop.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>spec-driven</category></item><item><title>How to Steer a Running AI Agent Without Stopping the Loop</title><link>https://ralphloop.sh/blog/steering-a-running-ai-agent/</link><guid isPermaLink="true">https://ralphloop.sh/blog/steering-a-running-ai-agent/</guid><description>You do not have to kill a Ralph loop to redirect it. Edit .agent/STEERING.md mid-run and the agent reads it at the top of the next iteration, handles the critical work first, then resumes the task list.

</description><pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;To steer a running AI agent, edit &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt; while the loop is going. The agent reads that file at the top of every iteration, before it picks a task. Anything you write there gets handled first, in order, then the agent removes the finished items and goes back to the task list. You change direction without killing the process and without losing the momentum of a warm run.&lt;/p&gt;
&lt;p&gt;This is the redirect control inside the larger system for &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-coding-agent-overnight/&quot;&gt;running an AI coding agent overnight&lt;/a&gt;. A loop that runs for hours will sometimes pick a task you no longer want, chase a dead end, or need a hotfix you only just noticed. Steering ai agent behavior through a file is how you correct course mid-flight instead of stopping, editing, and starting over.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-steering-an-ai-agent-actually-means&quot;&gt;What steering an AI agent actually means&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Steering is not a chat message and not a new prompt. It is a single Markdown file, &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt;, that lives next to the rest of the agent state. The Ralph prompt has one job for it. In the “Before Starting” section of &lt;code dir=&quot;auto&quot;&gt;.agent/PROMPT.md&lt;/code&gt;, the agent is told to check &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt; for critical work, complete the items in sequence, remove them when done, and only proceed to implement tasks if no critical work is pending.&lt;/p&gt;
&lt;p&gt;That ordering is the whole mechanism. Because each iteration starts the agent with a &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;fresh context window&lt;/a&gt;, the agent rereads the prompt and the steering file every single loop. There is no stale chat history to fight and no need to interrupt the agent mid-thought. You write to a file, and the next clean iteration discovers it.&lt;/p&gt;
&lt;p&gt;A steering file looks like a short work order. The default one in the repo handles sandbox setup before any feature work runs:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Critical Steering Work&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Install and verify dependencies&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Run &lt;/span&gt;&lt;span&gt;`npm install --ignore-scripts`&lt;/span&gt;&lt;span&gt;, then fix native arm64 binaries.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Main Tasks&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Install Playwright system deps, start the dev server, take a screenshot.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;After you finish this work, exit with message &lt;/span&gt;&lt;span&gt;`Steering complete`&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You can replace that content with whatever you need the agent to do next. The file is plain text, so you edit it with any tool from any machine that has the working tree, including a shell inside the sandbox.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;when-to-steer-a-running-agent&quot;&gt;When to steer a running agent&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Steer when the loop is healthy enough to keep running but pointed at the wrong thing, or when something urgent jumps the queue. There are three common triggers.&lt;/p&gt;
&lt;p&gt;The agent is stuck. You are watching the run and the same failure keeps coming back across iterations. Maybe a test depends on a binary that did not install, or a dev server never came up. The agent is not blocked enough to emit a promise tag, but it is spinning. A steering note that tells it exactly how to fix the environment clears the jam. Spotting this early is the payoff of &lt;a href=&quot;https://ralphloop.sh/blog/observability-for-ai-coding-agents/&quot;&gt;observability for autonomous coding agents&lt;/a&gt;: you see the climbing iteration time and the repeated debugging step before the loop burns ten more iterations.&lt;/p&gt;
&lt;p&gt;The agent went the wrong direction. The task list was fine when you wrote it, but the agent picked a task whose approach you now disagree with, or requirements changed since you started the run. You do not want to throw away the work it already shipped. You want it to switch tracks on the next iteration.&lt;/p&gt;
&lt;p&gt;You have an urgent fix. A bug landed in production, a dependency needs pinning, or a teammate needs a small change merged into the same branch. The loop is the fastest path to get it done, and you would rather inject the work than wait for the run to finish.&lt;/p&gt;
&lt;p&gt;Steering is the wrong tool for two cases. If the agent is fundamentally confused about the goal, fix &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt; or the PRD instead, because steering is for one-off interventions, not for rewriting the plan. And if the agent emitted &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; and the loop already exited, you do not steer a stopped process. You answer it and rerun, which I cover below.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-the-loop-reads-steeringmd-every-iteration&quot;&gt;How the loop reads STEERING.md every iteration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The reason steering works without a restart is that the loop already reopens the file on every pass. Each iteration is a clean run of the agent against the prompt, and the prompt routes through the steering check before it ever touches the task list.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
    Start[&quot;Iteration starts (fresh context)&quot;] --&gt; Read[&quot;Read .agent/PROMPT.md&quot;]
    Read --&gt; Check{&quot;STEERING.md has critical work?&quot;}
    Check --&gt;|&quot;yes&quot;| Steer[&quot;Do steering items in sequence&quot;]
    Steer --&gt; Remove[&quot;Remove finished items from STEERING.md&quot;]
    Remove --&gt; Tasks[&quot;Pick highest-priority task in tasks.json&quot;]
    Check --&gt;|&quot;no&quot;| Tasks
    Tasks --&gt; Verify[&quot;Run tests, lint, typecheck&quot;]
    Verify --&gt; Commit[&quot;Commit, emit promise tag&quot;]
    Commit --&gt; Next[&quot;Next iteration&quot;]
    You[&quot;You edit STEERING.md mid-run&quot;] -.-&gt;|&quot;injected here&quot;| Check&lt;/pre&gt;
&lt;p&gt;The dotted edge is the part you control. Your edit lands in the file at any moment, and the next iteration picks it up at the diamond. You never have to time it perfectly. If iteration 12 is in flight when you save the file, iteration 13 reads your note. Because the agent removes completed steering items, the file empties itself out and the loop returns to normal task work without you touching it again.&lt;/p&gt;
&lt;p&gt;This is why a fresh context per iteration is a feature, not a limitation. A single long session would have buried your note under thousands of tokens of prior reasoning. A loop that reloads its instructions every pass treats the file as the source of truth, so the latest version of &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt; always wins.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;steering-versus-killing-and-restarting-the-loop&quot;&gt;Steering versus killing and restarting the loop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The blunt alternative is to stop the run, edit something, and start again. Steering beats that on three counts.&lt;/p&gt;
&lt;p&gt;You keep warm state. A sandbox that already installed dependencies, built the project, and warmed its caches is expensive to recreate. Ralph runs each agent in a deterministic Docker Sandbox named &lt;code dir=&quot;auto&quot;&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;dir&gt;-&amp;#x3C;hash8&gt;&lt;/code&gt;, and that sandbox persists between iterations. Killing the loop and recreating the box means paying the setup cost again. Steering reuses the running sandbox.&lt;/p&gt;
&lt;p&gt;You keep the run history intact. Every iteration writes a clean transcript to &lt;code dir=&quot;auto&quot;&gt;.agent/history/&lt;/code&gt;, appends to &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt;, and commits per task. A restart starts a new session id and fragments that trail. Steering threads your intervention into the same continuous record, so the morning review reads as one story.&lt;/p&gt;
&lt;p&gt;You avoid the gap. When you kill a loop manually, you have to remember to restart it. People walk away and forget. A steering note keeps the loop alive and self-correcting, which is the entire point of an unattended overnight run.&lt;/p&gt;
&lt;p&gt;There is one case where stopping is correct: when you want to change how the loop runs, not what it does next. Flags like the iteration count and the agent are set at launch, so switching from &lt;code dir=&quot;auto&quot;&gt;./ralph.sh -n 50&lt;/code&gt; to a different agent with &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --agent codex&lt;/code&gt; does require a restart. For everything that is “do this work next,” steer instead.&lt;/p&gt;
&lt;p&gt;If you only need to validate a single steering note before trusting it overnight, run one pass with &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --once&lt;/code&gt;, confirm the agent did what the file said, then start the full loop.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-to-write-a-steering-note-that-works&quot;&gt;How to write a steering note that works&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A good steering note reads like a task spec: specific, ordered, and verifiable. A bad one reads like a vibe. The agent reads your note with a fresh context, so it has no memory of the hallway conversation in your head. Spell it out.&lt;/p&gt;
&lt;p&gt;Write the exact commands and file paths. “Fix the build” is vague. The agent has to guess what broke and what done looks like. Compare:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Critical Steering Work&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Pin the failing dependency&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;1.&lt;/span&gt;&lt;span&gt; In &lt;/span&gt;&lt;span&gt;`package.json`&lt;/span&gt;&lt;span&gt;, set &lt;/span&gt;&lt;span&gt;`vite`&lt;/span&gt;&lt;span&gt; to &lt;/span&gt;&lt;span&gt;`5.4.10`&lt;/span&gt;&lt;span&gt; exactly.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;2.&lt;/span&gt;&lt;span&gt; Run &lt;/span&gt;&lt;span&gt;`npm install --ignore-scripts`&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;3.&lt;/span&gt;&lt;span&gt; Run &lt;/span&gt;&lt;span&gt;`npm run build`&lt;/span&gt;&lt;span&gt; and confirm it exits 0.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;4.&lt;/span&gt;&lt;span&gt; Commit with message &lt;/span&gt;&lt;span&gt;`fix: pin vite to 5.4.10`&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;---&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;After you finish this work, exit with message &lt;/span&gt;&lt;span&gt;`Steering complete`&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Every line is checkable. The agent knows the version, the commands, the success condition (build exits 0), and the commit message. That is the same standard the loop expects from a real task, and the same reason &lt;a href=&quot;https://ralphloop.sh/blog/verification-loops-for-ai-agents/&quot;&gt;verification loops&lt;/a&gt; matter: a steering note with a verifiable end state lets the agent prove it is done instead of guessing.&lt;/p&gt;
&lt;p&gt;Give the agent an exit condition. The default file ends with “After you finish this work, exit with message &lt;code dir=&quot;auto&quot;&gt;Steering complete&lt;/code&gt;.” That tells the agent the boundary of the steering work so it does not bleed into task work uninvited. Keep that pattern. Without a clear end, the agent may either stop too early or wander.&lt;/p&gt;
&lt;p&gt;Keep it small. Steering is for one or a few critical items, not a second task list. If your note grows into a dozen steps across unrelated areas, that is a sign the work belongs in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt; as real tasks with their own specs. Steering is the fast lane, not the highway.&lt;/p&gt;
&lt;p&gt;Order matters. The prompt says complete items in sequence. List prerequisites first. If the agent must install a binary before a test can pass, the install goes above the test.&lt;/p&gt;
&lt;p&gt;A few quick contrasts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vague: “Make the homepage better.” Specific: “On the homepage hero, change the CTA label to &lt;code dir=&quot;auto&quot;&gt;Start free&lt;/code&gt;, verify with a Playwright screenshot saved to &lt;code dir=&quot;auto&quot;&gt;.agent/screenshots/&lt;/code&gt;.”&lt;/li&gt;
&lt;li&gt;Vague: “Tests are flaky, look into it.” Specific: “The test &lt;code dir=&quot;auto&quot;&gt;auth.spec.ts&lt;/code&gt; fails on a timeout. Increase the wait in that file to 10 seconds and rerun it until green.”&lt;/li&gt;
&lt;li&gt;Vague: “Clean up the deps.” Specific: “Remove &lt;code dir=&quot;auto&quot;&gt;lodash&lt;/code&gt; from &lt;code dir=&quot;auto&quot;&gt;package.json&lt;/code&gt;, replace its three usages in &lt;code dir=&quot;auto&quot;&gt;src/utils/&lt;/code&gt; with native equivalents, run the unit tests.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern is always the same. Name the file, name the command, name the proof.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;combining-steering-with-blocked-and-decide-promises&quot;&gt;Combining steering with BLOCKED and DECIDE promises&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Steering and promise tags solve different halves of the human-in-the-loop problem, and they work together.&lt;/p&gt;
&lt;p&gt;Promise tags are how the agent talks to you. When the agent cannot continue, it emits &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; and the loop exits with code &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt;. When it needs a decision it cannot make alone, it emits &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt; and exits with code &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt;. A clean finish is &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; with exit code &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;, and hitting the iteration cap is exit code &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;. Ralph plays a sound and sends a desktop notification on &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt;, so you find out the moment a human is actually needed.&lt;/p&gt;
&lt;p&gt;Steering is how you talk back. The promise tells you the loop stopped and why. Your answer goes into &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt;, and then you rerun the loop. The next iteration reads your steering note first, handles the unblock or the chosen direction, removes it, and continues with the task list. The flow is tight:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# loop exits 2 with BLOCKED:cannot reach npm registry&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# you allow the domain from the host, then leave a steering note&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# in .agent/STEERING.md describing the retry, then:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For a &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt;, the agent gave you a question like “Option A vs B.” You pick one in the steering file in plain terms: “Use option A. Implement the REST client, not the GraphQL one. Then continue tasks.” The agent does not relitigate the choice because the steering note is now the instruction, and the fresh context means it reads that note as current truth.&lt;/p&gt;
&lt;p&gt;This pairing is the reason an overnight run stays autonomous without being unaccountable. The agent stops itself on the two events that genuinely need a person, notifies you, and waits. You drop a precise note into one file and restart. The promise mechanics behind those exit codes are worth knowing in full if you want the loop to stop on a signal rather than a guess, and the deny-by-default network policy that triggers many &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; exits is enforced by the sandbox, not the agent.&lt;/p&gt;
&lt;p&gt;When a steering note itself is not enough, get inside the box. List the running sandboxes and open a shell to debug by hand, then write what you learned back into the steering file:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;ralph-sandbox-name&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Print the exact name for your project with &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --print-name&lt;/code&gt;. Between promise tags that pull you in, a steering file that pushes instructions back, and a sandbox you can open and inspect, you can run a long autonomous loop and still keep both hands on the wheel whenever you want them there.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;How do I steer a running AI agent in a Ralph loop?&lt;/h3&gt; &lt;p&gt;Edit the .agent/STEERING.md file while the loop is running. The agent reads that file at the top of every iteration, before it picks a task from tasks.json. It completes the steering items in sequence, removes them when done, and then resumes normal task work. Because each iteration starts with a fresh context, you do not have to time the edit precisely. The next iteration picks up whatever the file says.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Do I have to stop the loop to change what the agent is doing?&lt;/h3&gt; &lt;p&gt;No. That is the point of steering. Editing STEERING.md injects work into the next iteration without killing the process, so you keep the warm sandbox, the running session, and the per-task git history intact. You only need to restart the loop when you want to change how it runs, such as switching the agent or the iteration count, since those flags are set at launch.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What makes a good steering note?&lt;/h3&gt; &lt;p&gt;Specific, ordered, verifiable instructions. Name the exact files and commands, list prerequisites first, and give a clear success condition such as a build exiting 0 or a test going green. End the note with an explicit exit message so the agent knows where steering work stops. Keep it to one or a few critical items. If it grows into a full plan, that work belongs in tasks.json as real tasks instead.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does steering work together with BLOCKED and DECIDE promises?&lt;/h3&gt; &lt;p&gt;Promise tags are how the agent signals it needs you. BLOCKED exits with code 2 and DECIDE exits with code 3, both with a desktop notification. Steering is how you respond. You write the unblock steps or the chosen option into STEERING.md and rerun the loop, and the next fresh-context iteration reads your note first, handles it, removes it, and continues the task list.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Where does the agent look for steering instructions?&lt;/h3&gt; &lt;p&gt;In .agent/STEERING.md. The .agent/PROMPT.md file tells the agent in its Before Starting section to check that file for critical work, complete items in sequence, remove them when done, and only proceed to implement tasks if no critical work is pending. So the latest version of STEERING.md is always the source of truth for what the agent does next.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>autonomous-agents</category></item><item><title>Claude Code vs Codex vs Cursor vs Gemini: Best CLI for Long-Running Agent Loops</title><link>https://ralphloop.sh/blog/best-agentic-cli-for-long-running-tasks/</link><guid isPermaLink="true">https://ralphloop.sh/blog/best-agentic-cli-for-long-running-tasks/</guid><description>There is no single best agentic CLI. Here is how Claude Code, Codex, Cursor, Gemini, Copilot, and opencode hold up over a long autonomous loop, and how to pick per task.

</description><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There is no single best agentic CLI for long-running loops. The honest answer is that you pick per task, and the agent matters less than the loop you wrap around it. Claude Code, OpenAI’s Codex CLI, the Cursor CLI agent, Gemini CLI, GitHub Copilot CLI, and opencode all run inside Ralph behind the same &lt;code dir=&quot;auto&quot;&gt;--agent&lt;/code&gt; flag, so swapping one for another is a one word change, not a rewrite. This post compares the six on the dimensions that actually decide a multi-hour run: autonomy quality, cost, model options, tool use, and ecosystem.&lt;/p&gt;
&lt;p&gt;I am not going to quote benchmark numbers. Vendor leaderboards move every few weeks and rarely reflect how an agent behaves over dozens of iterations against your codebase. What follows is concrete and opinionated, grounded in how each CLI is invoked and how each one tends to behave when you leave it running.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-best-is-the-wrong-question-for-a-multi-hour-loop&quot;&gt;Why “best” is the wrong question for a multi-hour loop&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A one-shot demo and a multi-hour loop are different sports. In a demo, raw reasoning on the first try wins. In a loop, the agent runs the same task family dozens or hundreds of times, sees its own test failures, and fixes them on the next pass. What you actually want is an agent that responds well to feedback, follows a structured prompt, and exits cleanly so the harness can decide whether to loop again.&lt;/p&gt;
&lt;p&gt;That reframes the comparison. The strongest model on a leaderboard can still lose a long run if it ignores the prompt structure, never stops on its own, or burns your budget on token-heavy reasoning for mechanical work. A merely good model with tight verification gates will grind toward correct. This is the core point from the &lt;a href=&quot;https://ralphloop.sh/blog/agentic-coding-clis/&quot;&gt;field guide to agentic coding CLIs&lt;/a&gt;: the harness around the agent carries more of the result than the agent does.&lt;/p&gt;
&lt;p&gt;So the real question is not “which CLI is smartest.” It is “which CLI holds up when I leave it alone.” Hold that distinction while you read the comparison.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-ralph-makes-the-six-clis-interchangeable&quot;&gt;How Ralph makes the six CLIs interchangeable&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph is a Bash script you point at a project. It does not reimplement any agent. It wraps whichever one you pick, runs it inside an isolated Docker Sandbox, watches the output, and loops with a fresh context each iteration. Three design choices are what make the agent swappable.&lt;/p&gt;
&lt;p&gt;First, one flag selects the agent. &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt; is the default, and you switch with &lt;code dir=&quot;auto&quot;&gt;--agent&lt;/code&gt; (short &lt;code dir=&quot;auto&quot;&gt;-a&lt;/code&gt;):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Claude Code (default), 50 iterations&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Swap the agent, nothing else changes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Second, a bare &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; separator forwards anything after it straight to the underlying CLI. Ralph parses its own flags first, then hands the rest to the agent untouched. That is how you pin a model without Ralph needing to know each vendor’s model names:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gpt-5.5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Third, each agent gets its own deterministic sandbox, named &lt;code dir=&quot;auto&quot;&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;current-dir&gt;-&amp;#x3C;hash8&gt;&lt;/code&gt;. Your Claude sandbox and your Codex sandbox never share credentials, history, or installed tools, so you can compare agents on the same project without them stepping on each other. Print the name without starting a run:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Under the hood, Ralph builds a different invocation per agent but keeps the loop identical. The expansions are:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# claude:    sbx run ... claude .   -- --output-format stream-json --verbose -p &quot;$PROMPT_CONTENT&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# codex:     sbx run ... codex .    -- exec &quot;$PROMPT_CONTENT&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# copilot:   sbx run ... copilot .  -- -p &quot;$PROMPT_CONTENT&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# cursor:    sbx run ... cursor .   -- -p &quot;$PROMPT_CONTENT&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# gemini:    sbx run ... gemini .   -- -p &quot;$PROMPT_CONTENT&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# opencode:  sbx run ... opencode . -- run &quot;$PROMPT_CONTENT&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Every one of those runs the same loop: pick the top task from &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, work it, run the verification stack, commit, and either continue or stop on a promise tag. Because the loop is shared, you can treat the choice of CLI as a variable. Pick one, run it, and if it stalls on your codebase, change the flag and try another. The mechanics never move.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-dimensions-that-decide-a-long-run&quot;&gt;The dimensions that decide a long run&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Five things separate these agents once you are running them unattended. I will go through each, then break the agents down one by one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Autonomy quality&lt;/strong&gt; is how well the agent works without a human in the chair. Does it follow the prompt structure, stay on one task per invocation, run its own tests, and emit a clean completion signal? A loop has nobody to answer “should I proceed?”, so any agent that pauses for approval will stall unless you put it in a non-interactive mode.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt; over a loop is dominated by model choice and iteration count, not the per-call price you see in a demo. Fifty iterations on a flagship model add up. The lever is the model flag and the iteration cap, which I cover in depth alongside the overnight-run architecture in &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-coding-agent-overnight/&quot;&gt;how to run an AI coding agent overnight&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model options&lt;/strong&gt; decide whether you can match the model to the task. Heavy reasoning (architecture, gnarly refactors) rewards the strongest model. Mechanical work (CRUD wiring, lint fixes, filling in tests) runs fine and cheaper on a mid tier model. The more model choices a CLI exposes through &lt;code dir=&quot;auto&quot;&gt;-- --model&lt;/code&gt;, the more you can tune.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tool use&lt;/strong&gt; is how the agent edits files, runs shell commands, and reads results. All six edit files and run commands. The difference shows up in how cleanly they run headless and how readable their output is while looping.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ecosystem&lt;/strong&gt; is auth, billing, and which world you already live in. A team standardized on GitHub auth has a different default than one paying for an Anthropic or OpenAI plan.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;claude-code&quot;&gt;Claude Code&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Claude Code is Ralph’s default for a reason. It is steady on long, multi-step tasks and follows a structured prompt closely, which is exactly what a fresh-context loop needs. Ralph runs it with &lt;code dir=&quot;auto&quot;&gt;--output-format stream-json --verbose&lt;/code&gt;, and that structured stream gives the loop the richest live, readable step view of any of the six. When you want to watch what the agent is doing each iteration, Claude Code shows the most.&lt;/p&gt;
&lt;p&gt;It runs in bypass-permissions mode inside the sandbox (&lt;code dir=&quot;auto&quot;&gt;--dangerously-skip-permissions&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;--permission-mode bypassPermissions&lt;/code&gt;), so it never pauses for approval during an unattended run. Model selection goes through &lt;code dir=&quot;auto&quot;&gt;-- --model&lt;/code&gt;. For autonomy quality and clarity of feedback, this is the smoothest first run. The end-to-end setup is in &lt;a href=&quot;https://ralphloop.sh/blog/run-claude-code-in-a-loop/&quot;&gt;running Claude Code in a loop&lt;/a&gt;. See the &lt;a href=&quot;https://code.claude.com/docs&quot;&gt;Claude Code docs&lt;/a&gt; for the flag surface.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;codex-cli&quot;&gt;Codex CLI&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Codex is OpenAI’s agent, run through its non-interactive &lt;code dir=&quot;auto&quot;&gt;exec&lt;/code&gt; mode. It is a strong reasoner on hard, self-contained problems, which makes it a natural pick when the task is logic-heavy rather than sprawling. Pin a model with &lt;code dir=&quot;auto&quot;&gt;-- --model gpt-5.5&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The thing to know about Codex in a loop: &lt;code dir=&quot;auto&quot;&gt;codex exec&lt;/code&gt; runs read-only by default, so a loop using the default mode will spin without ever editing a file. You grant write access deliberately with &lt;code dir=&quot;auto&quot;&gt;-- --sandbox workspace-write --ask-for-approval never&lt;/code&gt;, or bypass Codex’s own gates entirely with &lt;code dir=&quot;auto&quot;&gt;-- --dangerously-bypass-approvals-and-sandbox&lt;/code&gt;. The bypass flag is safe here because the microVM is the real boundary, not Codex policing itself. Codex also has a clean &lt;code dir=&quot;auto&quot;&gt;--json&lt;/code&gt; event stream for CI parsing. The full wiring, including the read-only gotcha and CI flags, is in &lt;a href=&quot;https://ralphloop.sh/blog/run-codex-cli-in-a-loop/&quot;&gt;running the Codex CLI in an autonomous loop&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;cursor-cli-agent&quot;&gt;Cursor CLI agent&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The Cursor CLI agent brings the headless &lt;code dir=&quot;auto&quot;&gt;cursor-agent&lt;/code&gt; to the loop, invoked with &lt;code dir=&quot;auto&quot;&gt;-p&lt;/code&gt;. If your team already lives in Cursor, running the same agent unattended over a sandbox lets you review a finished diff in the morning instead of pair-programming all afternoon. The autonomy quality is solid, and the appeal is continuity: you keep the agent and the mental model you already trust, and you add looping on top. Model and other flags pass through after &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; like every other agent.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;gemini-cli&quot;&gt;Gemini CLI&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Gemini CLI is Google’s agent, driven with &lt;code dir=&quot;auto&quot;&gt;-p&lt;/code&gt; and model selection through &lt;code dir=&quot;auto&quot;&gt;-- --model pro&lt;/code&gt;. It is worth reaching for when you want a different model family in the mix or you already live in the Google ecosystem. The argument for keeping a non-Anthropic, non-OpenAI option in your rotation is practical: when one agent stalls on a specific task, a different model family sometimes walks straight through it. Ralph makes that switch a one word change.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;github-copilot-cli&quot;&gt;GitHub Copilot CLI&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Copilot CLI slots in for teams standardized on GitHub’s tooling and auth. Ralph runs it with the same &lt;code dir=&quot;auto&quot;&gt;-p&lt;/code&gt; prompt pattern and the same sandbox boundary as the rest. The pull here is ecosystem, not raw capability: if your auth, your billing, and your repos already run through GitHub, Copilot is the path of least friction. The loop treats it identically to the others.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;opencode&quot;&gt;opencode&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;opencode is the open source option, invoked with its &lt;code dir=&quot;auto&quot;&gt;run&lt;/code&gt; subcommand. It is the pick when you want to avoid vendor lock-in, run an agent you can fully inspect and modify, or route to a provider and model of your own choosing. For cost-sensitive runs where you want maximum control over the model layer, an open agent you can point at any backend is a real advantage. You trade some of the polished, batteries-included feel of the vendor CLIs for control and inspectability.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;pick-this-when&quot;&gt;Pick this when&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here is the practical version, stripped of hedging. Match the agent to the situation rather than hunting for one winner.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pick &lt;strong&gt;Claude Code&lt;/strong&gt; when you want the smoothest first loop and the clearest live view of what the agent is doing. It is the right default, and the right place to start if you are new to running loops.&lt;/li&gt;
&lt;li&gt;Pick &lt;strong&gt;Codex&lt;/strong&gt; when the task is logic-heavy and self-contained, and you want explicit model pinning plus a clean JSON event stream for CI. Remember to grant write access, or it will not edit anything.&lt;/li&gt;
&lt;li&gt;Pick &lt;strong&gt;Cursor&lt;/strong&gt; when your team already uses Cursor and you want the same agent to run unattended so you review a diff instead of babysitting.&lt;/li&gt;
&lt;li&gt;Pick &lt;strong&gt;Gemini&lt;/strong&gt; when you want a different model family in your rotation, or you are already in the Google ecosystem.&lt;/li&gt;
&lt;li&gt;Pick &lt;strong&gt;Copilot&lt;/strong&gt; when your auth and billing already run through GitHub and you want the least new setup.&lt;/li&gt;
&lt;li&gt;Pick &lt;strong&gt;opencode&lt;/strong&gt; when you want an open, inspectable agent, no vendor lock-in, or full control over the model and provider behind it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A decision guide for the common case:&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
  Start([&quot;Choosing an agent for a long loop&quot;]) --&gt; Q1{&quot;New to running loops?&quot;}
  Q1 --&gt;|&quot;yes&quot;| Claude[&quot;Start with claude (default, richest live view)&quot;]
  Q1 --&gt;|&quot;no&quot;| Q2{&quot;What matters most?&quot;}
  Q2 --&gt;|&quot;hard reasoning task&quot;| Codex[&quot;codex (grant write access)&quot;]
  Q2 --&gt;|&quot;already in Cursor&quot;| Cursor[&quot;cursor&quot;]
  Q2 --&gt;|&quot;different model family&quot;| Gemini[&quot;gemini -- --model pro&quot;]
  Q2 --&gt;|&quot;GitHub-standardized team&quot;| Copilot[&quot;copilot&quot;]
  Q2 --&gt;|&quot;open, no lock-in&quot;| Opencode[&quot;opencode&quot;]
  Claude --&gt; Swap{&quot;Stalls on your codebase?&quot;}
  Codex --&gt; Swap
  Cursor --&gt; Swap
  Gemini --&gt; Swap
  Copilot --&gt; Swap
  Opencode --&gt; Swap
  Swap --&gt;|&quot;yes&quot;| Change[&quot;Change one --agent flag, retry&quot;]
  Swap --&gt;|&quot;no&quot;| Ship[&quot;Let the loop run, review in the morning&quot;]&lt;/pre&gt;
&lt;p&gt;The last edge is the important one. Because the harness is shared, “this agent stalled” is not a dead end. It is a flag change. That is the whole reason Ralph treats the agent as swappable rather than picking one for you.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-actually-moves-the-result&quot;&gt;What actually moves the result&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you take one thing from this comparison, take this: a weaker agent inside a good loop will out-ship a stronger agent you babysit in a single session. The loop gives the agent fresh context every iteration, keeps state on disk, isolates it in a sandbox, and runs real verification gates. Those four things matter more than the gap between any two of these CLIs.&lt;/p&gt;
&lt;p&gt;Fresh context per iteration is what beats context rot. Each pass boots the agent clean, so it does not drag an hours-long transcript from one task to the next. The filesystem and git history are the memory layer: progress lives in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, the per-task spec files, &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt;, and the git log, not in a chat window. This is the mechanic Geoffrey Huntley described in the &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;original Ralph writeup&lt;/a&gt;, and it applies identically to all six agents.&lt;/p&gt;
&lt;p&gt;Verification is what lets a cheaper model succeed. The loop assumes a stack of Playwright for end-to-end tests, Vitest for unit tests, TypeScript for types, ESLint for lint, and Prettier for format. The repo mantra is blunt: if you didn’t test it, it doesn’t work. The agent does not need to be right on the first try. It needs to write code, run the gates, read the failures, and fix them next pass. When you compare agents for long runs, you are really comparing how each one responds to that feedback, not how clever its first draft looks.&lt;/p&gt;
&lt;p&gt;The sandbox is what makes any of this safe to walk away from. Ralph runs every agent inside a Docker Sandbox microVM with deny-by-default networking, so bypass-permissions mode is reasonable: the worst the agent can do is wreck a disposable VM, not read your SSH keys. You open exactly what a task needs with &lt;code dir=&quot;auto&quot;&gt;sbx policy allow network &amp;#x3C;name&gt; &amp;#x3C;domain&gt;&lt;/code&gt;. The full reasoning is in the &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/&quot;&gt;Docker Sandboxes docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And the loop stops on a signal, not a vibe. Each agent emits a promise tag that Ralph reads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; means every task is finished.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; means the agent needs human help.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt; means it needs a decision you have to make.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those map to exit codes: &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; for COMPLETE, &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; for MAX_ITERATIONS, &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt; for BLOCKED, and &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt; for DECIDE. You branch on them in CI or a wrapper script, which means the comparison between agents is also fair: every one of them ends with a verdict you can act on.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;putting-it-together&quot;&gt;Putting it together&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;There is no best agentic CLI, so stop looking for one. Start with Claude Code because it is the steady default with the clearest output. Reach for Codex on hard reasoning tasks, Cursor when your team already uses it, Gemini for a different model family, Copilot when you are GitHub-standardized, and opencode when you want an open agent with no lock-in. Then let the harness do the heavy lifting.&lt;/p&gt;
&lt;p&gt;The shortest path to running any of them is three commands:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 1. install&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@pageai/ralph-loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 2. authenticate the agent inside its sandbox&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 3. run the loop on your chosen agent and model&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gpt-5.5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Swap &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt; for any of the six and the loop is identical. Pick the agent, pin the model, and let it work one task per iteration inside the sandbox while you sleep. If it stalls, you already know the fix: change one flag.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;Which is the best agentic CLI for long-running loops?&lt;/h3&gt; &lt;p&gt;There is no single winner. Claude Code is the steady default with the richest live output, Codex is strong on hard reasoning tasks, Cursor suits teams already using Cursor, Gemini brings a different model family, Copilot fits GitHub-standardized teams, and opencode is the open source option. The loop you wrap around the agent matters more than the agent, so pick one and swap with the --agent flag if it stalls.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Claude Code vs Codex: which should I use?&lt;/h3&gt; &lt;p&gt;Use Claude Code as the default when you want the smoothest unattended run and the clearest live view of each iteration, since Ralph parses its stream-json output. Use Codex for logic-heavy, self-contained problems and CI pipelines that parse its JSON event stream. One gotcha: codex exec is read-only by default, so grant write access with -- --sandbox workspace-write --ask-for-approval never or it will not edit files.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I switch between agents in Ralph?&lt;/h3&gt; &lt;p&gt;Pass the --agent flag, or -a for short. ./ralph.sh runs Claude Code by default, ./ralph.sh --agent codex runs Codex, and ./ralph.sh -a cursor -n 5 runs Cursor for five iterations. Each agent gets its own sandbox named ralph-&amp;#x3C;agent&gt;-&amp;#x3C;dir&gt;-&amp;#x3C;hash8&gt;, so they do not share credentials or history. The loop itself is identical across all six.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Does the choice of agent matter more than the loop?&lt;/h3&gt; &lt;p&gt;No. A weaker agent inside a good loop, with fresh context each iteration, state on disk, sandbox isolation, and real verification gates, will out-ship a stronger agent you babysit in a single session. Those four properties decide the result more than the gap between any two CLIs, which is why Ralph treats the agent as a swappable variable.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I pick a model for each agent?&lt;/h3&gt; &lt;p&gt;Model selection is an agent flag, not a Ralph setting, so you pass it after the -- separator. For example ./ralph.sh --agent codex -- --model gpt-5.5 or ./ralph.sh -a gemini -- --model pro. Match the model to the task: a strong model for heavy reasoning, a cheaper mid tier model for mechanical work, since model choice and iteration count are the biggest cost levers in a long run.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>agentic-clis</category></item><item><title>Ralph Loop Failure Modes (Context Rot, Runaway Cost) and How to Avoid Them</title><link>https://ralphloop.sh/blog/ralph-loop-failure-modes/</link><guid isPermaLink="true">https://ralphloop.sh/blog/ralph-loop-failure-modes/</guid><description>A Ralph loop fails in a handful of predictable ways. Here is each failure mode, why it happens, and the specific guardrail that stops it.

</description><pubDate>Sat, 04 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A Ralph loop does not fail in mysterious ways. It fails in a short list of predictable ones: the agent forgets the plot, the loop burns money forever, it thrashes on a task that is too big or too vague, it ships work that looks done but is wrong, or it damages something on your machine. Each of these has a known cause and a specific guardrail. None of them require luck to avoid.&lt;/p&gt;
&lt;p&gt;This post walks through every failure mode of running an &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;AI coding agent in a loop&lt;/a&gt;, why each one happens, and the exact mechanism in &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; that prevents it. If you are setting up a loop for the first time or wondering why a run went sideways, this is the checklist.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-are-the-failure-modes-of-a-ralph-loop&quot;&gt;What are the failure modes of a Ralph loop?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Five, mostly. They are context rot, runaway cost, thrashing on a bad task, silent wrong work, and sandbox damage. Every one maps to a guardrail you can turn on or design around. Here is the short version before the details:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Context rot.&lt;/strong&gt; Fixed by fresh context per iteration and state on disk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runaway cost and infinite loops.&lt;/strong&gt; Fixed by an iteration cap, a completion promise, and a budget you set.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thrashing on a bad or oversized task.&lt;/strong&gt; Fixed by atomic tasks, one task per iteration, and the &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; promise tags.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Silent wrong work.&lt;/strong&gt; Fixed by verification gates and screenshots.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sandbox escape or damage.&lt;/strong&gt; Fixed by running the agent inside a Docker Sandbox.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The rest of this post takes each one in turn.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;context-rot-the-agent-loses-the-plot&quot;&gt;Context rot: the agent loses the plot&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Context rot is the slow decay of an agent’s reasoning as its context window fills with stale and conflicting information. Old tool output, abandoned plans, your corrections, the agent’s own apologies, half-finished edits: all of it competes with the task that actually matters right now. The longer a single session runs, the worse the signal-to-noise ratio gets. You see it as repeated mistakes, forgotten constraints, and confident edits that quietly undo earlier good work.&lt;/p&gt;
&lt;p&gt;This is the failure the Ralph technique was built to kill, so the guardrail is the core of the design rather than a bolt-on.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-fresh-context-every-iteration&quot;&gt;Fix: fresh context every iteration&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Each iteration spawns the agent with a clean context window. It reads only what matters right now: the prompt, a short project summary, the task list, and the spec for the one task it is about to work. There is no transcript of the previous twelve iterations clogging the window. The model spends its attention on the current task instead of re-reading its own history.&lt;/p&gt;
&lt;p&gt;Think of it as a series of sprints rather than one marathon. A marathon session accumulates fatigue. A fresh sprint each time stays sharp.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-state-lives-on-disk-not-in-chat&quot;&gt;Fix: state lives on disk, not in chat&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;If the agent forgets everything between iterations, how does it make progress? Because progress does not live in the conversation. It lives in files. Ralph keeps state under &lt;code dir=&quot;auto&quot;&gt;.agent/&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.agent/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;├── PROMPT.md      # Prompt sent to the agent each iteration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;├── tasks.json     # Task lookup table&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;├── tasks/         # Per-task specs (TASK-{ID}.json)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;├── logs/LOG.md    # Progress log&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;└── history/       # Per-iteration output logs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When a fresh agent boots, it reads &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt; to see what is done, reads &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt; to see what happened recently, and reads the git log to see the actual committed changes. The code on disk is the record. The window never grows large enough to rot because it resets every pass. The deeper version of this design, including how the prompt file makes a fresh agent reorient in seconds, is in &lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-prompt-file/&quot;&gt;how to write the PROMPT.md file that drives a Ralph loop&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;runaway-cost-and-infinite-loops-the-money-fire&quot;&gt;Runaway cost and infinite loops: the money fire&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The naive Ralph loop is one line:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;while&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;do&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PROMPT.md&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;your-agent-cli&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;done&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That loop never stops on its own. It has no iteration cap and no notion of success. Point it at a paid model and walk away, and it will happily keep calling the API until your bill says otherwise. Even when the work is genuinely done, a loop without a stop condition will keep re-running the agent to rediscover that there is nothing left to do, paying full freight each time.&lt;/p&gt;
&lt;p&gt;Runaway cost has two shapes. One is the loop that never terminates. The other is the loop that terminates eventually but does far more iterations than the work needed. Both are guardrail problems, not model problems.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-an-iteration-cap-with--n&quot;&gt;Fix: an iteration cap with -n&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; takes a hard cap on iterations. The default is ten. You set a higher one explicitly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--max-iterations&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;To smoke test the setup without committing to a long run, do exactly one pass:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When the loop hits the cap with work still pending, it exits with code &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; (MAX_ITERATIONS). That is not a crash. It is the safety net doing its job. You read the log, decide whether to top up the budget, and run it again.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-a-completion-promise-not-a-vibe&quot;&gt;Fix: a completion promise, not a vibe&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The loop stops on an explicit signal, never on a guess. The agent emits a promise tag to declare where things stand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; every task is finished.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; the agent needs a human to clear something.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt; the agent hit a real decision point.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; watches for these tags and translates them into exit codes:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Code&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;COMPLETE. All tasks finished.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;MAX_ITERATIONS. Reached the cap.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;BLOCKED. Needs human help.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;DECIDE. Needs a human decision.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt; ends the run cleanly the moment the work is actually done, so you do not pay for victory laps. The full design of the stop condition, and why a promise beats letting the agent decide it feels finished, is in &lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-completion-promise/&quot;&gt;completion promises and exit codes&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-a-budget-you-choose-up-front&quot;&gt;Fix: a budget you choose up front&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Cost control is mostly about deciding limits before you start: how many iterations, which model, and what the loop is allowed to touch. A cheaper model on a well specified task can outperform an expensive model on a vague one, because the expensive model spends its budget flailing. The full treatment of caps, model choice, and the verification gates that keep a loop from spinning is in &lt;a href=&quot;https://ralphloop.sh/blog/ai-agent-cost-control/&quot;&gt;cost control for autonomous AI coding agents&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;thrashing-on-a-bad-or-oversized-task&quot;&gt;Thrashing on a bad or oversized task&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Give an agent a task like “refactor the billing system” and it will thrash. The task is too big to hold in one iteration and too vague to verify, so the agent makes a sprawling change, second-guesses it, partially reverts, and produces a commit you cannot review. Repeat that across iterations and the loop spins without converging. This is the most common reason a loop “does not work,” and it is almost always a task design problem rather than an agent problem.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-atomic-tasks&quot;&gt;Fix: atomic tasks&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The unit of work matters. A task should be small enough to finish in a single iteration and specific enough to verify. “Add a unit test for the discount calculation in &lt;code dir=&quot;auto&quot;&gt;cart.ts&lt;/code&gt;” is atomic. “Improve test coverage” is not. The loop is only as good as the tasks you feed it. Garbage tasks in, garbage commits out. How to decompose a large project into atomic, independently verifiable packets is the heart of spec-driven development, and it is upstream of everything the loop does.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-one-task-per-iteration&quot;&gt;Fix: one task per iteration&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The rule that keeps a loop reliable is blunt: the agent completes exactly one task, commits, and stops. It never batches several tasks into one iteration. Batching is how an agent drifts off scope and produces a tangled diff. One task per invocation keeps each commit reviewable and each iteration’s blast radius small.&lt;/p&gt;
&lt;p&gt;This pairs with the task lookup table. The agent reads &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, picks the highest-priority incomplete task, opens its spec at &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;, works only those steps, and exits. The table is a lightweight index, so a project can hold hundreds of tasks while each iteration loads only the one it needs.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-blocked-and-decide-instead-of-guessing&quot;&gt;Fix: BLOCKED and DECIDE instead of guessing&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;When a task is genuinely underspecified or the agent hits a wall it cannot clear, the worst outcome is for it to guess and burn iterations on a wrong assumption. The promise tags give it an honest exit. &lt;code dir=&quot;auto&quot;&gt;BLOCKED:reason&lt;/code&gt; stops the loop and hands you the blocker. &lt;code dir=&quot;auto&quot;&gt;DECIDE:question&lt;/code&gt; stops and asks for your call. The loop returns exit code &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt;, and you are not staring at a dead terminal wondering what happened. A loop that stops to ask is far cheaper than a loop that thrashes in silence.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;silent-wrong-work-it-looks-done-but-it-is-broken&quot;&gt;Silent wrong work: it looks done but it is broken&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The scariest failure is the one that looks like success. The agent flips a task to done, commits, and moves on, but the code does not actually work. Maybe it compiles and the tests it wrote test the wrong thing. Maybe the UI renders but the button does nothing. Across an overnight run, silent wrong work compounds: later tasks build on the broken one, and you wake up to a green task list and a red product.&lt;/p&gt;
&lt;p&gt;The cause is letting the agent self-assess on a vibe. The fix is to take that judgment away from the agent and give it to a tool that cannot be fooled by optimism.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-verification-gates&quot;&gt;Fix: verification gates&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Before the agent calls a task done, it runs the project’s checks. Ralph assumes a verification stack: Playwright for end to end tests, Vitest for unit tests, TypeScript for types, ESLint for linting, and Prettier for formatting. The repo mantra is exactly this blunt: if you didn’t test it, it doesn’t work.&lt;/p&gt;
&lt;p&gt;The gate is binary. The checks pass or the iteration is not done. If a check fails, the agent loops back inside the same iteration and fixes the code until it passes. The agent does not get to claim success while the test suite is red. This is what makes a commit in the git log trustworthy rather than aspirational.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-screenshots-for-the-things-tests-miss&quot;&gt;Fix: screenshots for the things tests miss&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Some failures do not show up in a unit test. A layout that is technically rendered but visually broken passes a DOM assertion and fails a human glance. For UI work, the loop takes a screenshot as part of completing a task, so the artifact you review in the morning includes proof of what the agent saw. A screenshot is a cheap, honest signal that catches the class of bugs that slip past assertions.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;sandbox-escape-and-damage-to-your-machine&quot;&gt;Sandbox escape and damage to your machine&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Autonomous agents run in bypass-permissions mode, often called YOLO mode, because stopping to approve every file write would defeat the point of an overnight loop. Claude Code calls this &lt;code dir=&quot;auto&quot;&gt;--dangerously-skip-permissions&lt;/code&gt;. The flag name is honest. An agent with your permissions and no approval prompts can read your SSH keys, touch files outside the repo, run arbitrary install scripts, and make network calls you never intended. On your laptop, that is a real risk. The mistake is treating the agent’s good behavior as the safety boundary.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;fix-the-sandbox-is-the-boundary&quot;&gt;Fix: the sandbox is the boundary&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Run the agent inside an isolated Docker Sandbox microVM. The boundary is the sandbox, not the agent’s restraint. &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; does this by default through the &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt; CLI, giving each run a deterministic sandbox name of the form &lt;code dir=&quot;auto&quot;&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;current-dir&gt;-&amp;#x3C;hash8&gt;&lt;/code&gt;. Inside that microVM, the agent can run in YOLO mode because the worst it can damage is the sandbox.&lt;/p&gt;
&lt;p&gt;You can inspect and operate the sandbox like any container:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;name&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Network is deny-by-default. The agent gets only the domains you allow:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;name&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;registry.npmjs.org&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That last point matters for both safety and cost. A deny-by-default network blocks exfiltration and stops an agent from wandering off to install or call something you did not sanction. The isolation model is documented in the &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/&quot;&gt;Docker Sandboxes docs&lt;/a&gt;, and the bypass flag the loop relies on is in the &lt;a href=&quot;https://code.claude.com/docs&quot;&gt;Claude Code docs&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;failure-modes-mapped-to-guardrails&quot;&gt;Failure modes mapped to guardrails&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here is the whole picture in one view: each failure on the left, each guardrail on the right.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart LR
    subgraph Failures[&quot;Failure modes&quot;]
        F1[&quot;Context rot&quot;]
        F2[&quot;Runaway cost / infinite loop&quot;]
        F3[&quot;Thrashing on a bad task&quot;]
        F4[&quot;Silent wrong work&quot;]
        F5[&quot;Sandbox damage&quot;]
    end
    subgraph Guards[&quot;Guardrails&quot;]
        G1[&quot;Fresh context + state on disk&quot;]
        G2[&quot;-n cap + completion promise + budget&quot;]
        G3[&quot;Atomic tasks, one per iteration, BLOCKED / DECIDE&quot;]
        G4[&quot;Verification gates + screenshots&quot;]
        G5[&quot;Docker Sandbox microVM&quot;]
    end
    F1 --&gt; G1
    F2 --&gt; G2
    F3 --&gt; G3
    F4 --&gt; G4
    F5 --&gt; G5&lt;/pre&gt;
&lt;p&gt;Read it as a contract. If you have turned on the guardrail, the matching failure mode is handled. If a run went wrong, find which guardrail was missing or misconfigured.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-pre-flight-checklist-before-a-long-run&quot;&gt;A pre-flight checklist before a long run&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Before you start a multi-hour or overnight loop, walk this list. It is the difference between waking up to a clean branch and waking up to a mess.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cap the iterations.&lt;/strong&gt; Pass &lt;code dir=&quot;auto&quot;&gt;-n&lt;/code&gt; with a number you are willing to pay for. Do not run the uncapped one-liner.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test the setup with one pass.&lt;/strong&gt; Run &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --once&lt;/code&gt; and read the output before trusting a long run.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check your tasks are atomic.&lt;/strong&gt; Each task should be finishable in one iteration and verifiable by a check. If you cannot write the acceptance criteria, the task is not ready.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confirm the verification stack runs.&lt;/strong&gt; Make sure tests, lint, and type checking actually execute in the project, because the loop leans on them as its truth signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run in the sandbox.&lt;/strong&gt; Keep the agent inside the Docker Sandbox and the network on deny-by-default. Allow only the domains the build genuinely needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick the right agent and model.&lt;/strong&gt; Switch agents with &lt;code dir=&quot;auto&quot;&gt;--agent&lt;/code&gt; and pass model flags after a separator:&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gpt-5.5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Supported agents are &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt; (the default), &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-honest-framing&quot;&gt;The honest framing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A Ralph loop is persistence with a memory and a stop button. Its failure modes are the failure modes of any long autonomous process: it forgets, it overspends, it chases a bad goal, it lies to itself about success, and it can break things it touches. The technique does not pretend these do not exist. It pairs each one with a guardrail and makes the guardrails the default.&lt;/p&gt;
&lt;p&gt;None of this removes your judgment from the loop. You still write the tasks, set the budget, and review the commits. What the guardrails buy you is the confidence to let an agent grind for hours without standing over it. Get the five right and the loop becomes boring in the best way: it either finishes, or it stops and tells you exactly why.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What are the most common ways a Ralph loop fails?&lt;/h3&gt; &lt;p&gt;There are five common failure modes: context rot where the agent loses the plot over a long session, runaway cost or infinite loops, thrashing on a task that is too big or too vague, silent wrong work that looks done but is broken, and damage from running an agent without a sandbox. Each one maps to a specific guardrail in ralph.sh.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does a Ralph loop avoid context rot?&lt;/h3&gt; &lt;p&gt;It resets the context window every iteration and stores project state on disk. Because the agent reads the task list, logs, and git history each pass instead of carrying a growing conversation, the window never gets large enough to rot. Progress lives in files like tasks.json and commits, not in chat memory.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I stop a Ralph loop from burning money?&lt;/h3&gt; &lt;p&gt;Set an iteration cap with -n, rely on the completion promise so the loop stops the moment all tasks are done, and choose your model and budget before you start. The loop exits with code 1 when it hits the cap, which is a safety net rather than a failure. Running a single pass with --once first confirms the setup before a long run.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why does my agent thrash instead of finishing the task?&lt;/h3&gt; &lt;p&gt;Almost always because the task is too large or too vaguely specified to finish and verify in one iteration. The fix is atomic tasks, one task per invocation, and clear acceptance criteria. When a task is genuinely blocked or needs a decision, the agent should emit a BLOCKED or DECIDE promise and stop rather than guess.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Is it safe to run an autonomous coding agent in YOLO mode?&lt;/h3&gt; &lt;p&gt;Only inside a sandbox. Bypass-permissions mode is dangerous on your laptop because the agent can read credentials and touch files outside the repo. Run it inside a Docker Sandbox microVM with deny-by-default networking, so the boundary is the sandbox rather than the agent&apos;s good behavior.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>ralph-technique</category></item><item><title>How to Write a PRD an AI Agent Can Actually Build From</title><link>https://ralphloop.sh/blog/how-to-write-prd-for-ai-agent/</link><guid isPermaLink="true">https://ralphloop.sh/blog/how-to-write-prd-for-ai-agent/</guid><description>A PRD for an AI coding agent needs goals, constraints, and verifiable acceptance criteria, not a feature wishlist. Here is what goes in PRD.md and SUMMARY.md and how to draft it.

</description><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A PRD an AI agent can build from is not a feature wishlist. It is three things in one document: goals (what to build and why), constraints (the stack, the boundaries, what is out of scope), and verifiable acceptance criteria (conditions the agent can check by running a command and reading the output). Drop any one of those and the agent fills the gap with a guess. The guess looks fine in the diff and breaks on the case nobody wrote down.&lt;/p&gt;
&lt;p&gt;This post is the practical version: what goes in &lt;code dir=&quot;auto&quot;&gt;.agent/prd/PRD.md&lt;/code&gt;, what goes in the short &lt;code dir=&quot;auto&quot;&gt;SUMMARY.md&lt;/code&gt; the loop sends every iteration, how to draft both with the &lt;code dir=&quot;auto&quot;&gt;prd-creator&lt;/code&gt; skill, and how to write acceptance criteria an agent can actually verify. It assumes you already know the shape of &lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;spec-driven development with AI&lt;/a&gt; and want to write the document that sits at the top of it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-a-prd-for-an-ai-agent-actually-is&quot;&gt;What a PRD for an AI agent actually is&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A human PRD and an agent PRD overlap, but they are not the same document. A human reads a PRD, fills the gaps with judgment, and asks a teammate when something is unclear. An agent does none of that. It reads what is on disk, and when the spec is silent, it invents an answer and proceeds with full confidence. So the agent PRD has to do more work up front.&lt;/p&gt;
&lt;p&gt;Three properties decide whether a PRD is buildable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Goals are concrete, not aspirational.&lt;/strong&gt; “Make onboarding delightful” is not a goal an agent can build toward. “A new user reaches the dashboard in three steps or fewer after submitting the signup form” is. State the outcome in terms something can observe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Constraints are explicit.&lt;/strong&gt; Name the framework, the data store, the auth approach, and the libraries you have already committed to. Name what is out of scope just as clearly. An out-of-scope section is a fence: without it, an agent on a long run happily adds a feature you never asked for and now have to maintain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Acceptance criteria are verifiable.&lt;/strong&gt; Each criterion is a condition the agent can confirm by running a test, hitting an endpoint, or reading a file. “Login works” is a vibe. “POST /api/login with a wrong password returns 401 and the body &lt;code dir=&quot;auto&quot;&gt;{ error: &apos;Invalid credentials&apos; }&lt;/code&gt;” is a criterion. The difference is whether a machine can return a yes or a no without your opinion.&lt;/p&gt;
&lt;p&gt;The reason this matters more for agents than for people: an autonomous loop amplifies whatever you feed it. Feed it ambiguity and it amplifies ambiguity across every iteration. The Ralph technique runs an agent against your task list until the work is done, and the whole design rests on the agent rebuilding its understanding from files on each pass. If you want the mechanics of that loop, start with &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;what is the Ralph technique&lt;/a&gt;. The PRD is the document the entire loop reads from.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-the-prd-lives-prdmd-and-summarymd&quot;&gt;Where the PRD lives: PRD.md and SUMMARY.md&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph keeps the product specification in two files under &lt;code dir=&quot;auto&quot;&gt;.agent/prd/&lt;/code&gt;, and the split is deliberate.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;PRD.md&lt;/code&gt; is the full document. It is for depth: a human reads it once to understand the project, and the agent reads it when it needs detail it cannot get from the summary. The &lt;code dir=&quot;auto&quot;&gt;prd-creator&lt;/code&gt; skill writes it with a consistent set of sections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;App overview and objectives&lt;/li&gt;
&lt;li&gt;Target audience&lt;/li&gt;
&lt;li&gt;Success metrics and KPIs&lt;/li&gt;
&lt;li&gt;Competitive analysis&lt;/li&gt;
&lt;li&gt;Core features and user flows&lt;/li&gt;
&lt;li&gt;Technical stack&lt;/li&gt;
&lt;li&gt;Prerequisites and access&lt;/li&gt;
&lt;li&gt;Security considerations&lt;/li&gt;
&lt;li&gt;Assumptions and dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;SUMMARY.md&lt;/code&gt; is the short executive overview. This is the file that gets sent to the agent every iteration so it reorients fast without rereading the entire PRD. It contains an overall description of the project, the main features, the key user flows, and a short list of key requirements. Nothing more.&lt;/p&gt;
&lt;p&gt;The economics drive the split. Every iteration starts the agent with a fresh context window, and tokens in that window cost money and attention. You do not want to spend that budget reloading a 3000 word PRD on every pass when a tight summary reorients the agent just as well. Long PRD for depth, short summary for the working context on every iteration.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart LR
  Reqs[&quot;Unstructured requirements&quot;] --&gt; Interview[&quot;prd-creator interview, plan mode&quot;]
  Interview --&gt; PRD[&quot;prd/PRD.md: goals, constraints, criteria&quot;]
  PRD --&gt; Summary[&quot;prd/SUMMARY.md: short overview, sent each iteration&quot;]
  PRD --&gt; Tasks[&quot;tasks.json and tasks/TASK-ID.json&quot;]
  Summary --&gt; Loop[&quot;ralph.sh loop, fresh context each iteration&quot;]
  Tasks --&gt; Loop
  Loop --&gt; Verify[&quot;Run tests, lint, types, screenshot&quot;]
  Verify --&gt; Commit[&quot;Commit, set passes true&quot;]&lt;/pre&gt;
&lt;p&gt;The PRD feeds two children. The summary is the version the loop reads constantly. The task list is the executable decomposition. Get the PRD right and both children inherit clear goals and criteria. Get it vague and both inherit the vagueness.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;draft-it-with-the-prd-creator-skill-in-plan-mode&quot;&gt;Draft it with the prd-creator skill in plan mode&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;You do not write all of this by hand. Ralph ships a &lt;code dir=&quot;auto&quot;&gt;prd-creator&lt;/code&gt; skill that turns unstructured requirements into a PRD plus a task list. Run it in plan mode, where the agent is read-only and focused on asking questions instead of writing code. Plan mode matters here: you want the agent interrogating your idea, not racing ahead to scaffold files before the spec exists.&lt;/p&gt;
&lt;p&gt;The flow is a conversation, not a one shot. The instinct most people have is to paste a paragraph and expect a finished plan. The skill instead pushes back. It interviews you to fill the gaps, asking clarifying questions one at a time, and it researches the competitive landscape before it commits anything to &lt;code dir=&quot;auto&quot;&gt;PRD.md&lt;/code&gt;. When a question can be answered by reading the codebase, it reads the codebase instead of asking you.&lt;/p&gt;
&lt;p&gt;A prompt to kick it off looks like plain language:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Use the prd-creator skill in plan mode. I want to build a link shortener&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;with accounts, custom slugs, and click analytics. Interview me, write the&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;PRD to .agent/prd/PRD.md and the summary to .agent/prd/SUMMARY.md, then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;generate the task list in .agent/tasks.json.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;During the interview, the skill also verifies prerequisites and creates or updates &lt;code dir=&quot;auto&quot;&gt;.env.local&lt;/code&gt; with placeholder values only. It never writes a real secret to the PRD, the tasks, the logs, or &lt;code dir=&quot;auto&quot;&gt;.env.local&lt;/code&gt;. You fill the real values in by hand. This is the moment the spec records what credentials and access the project needs, so the agent is not discovering halfway through a 50 task run that it never had database access.&lt;/p&gt;
&lt;p&gt;When it finishes, you have &lt;code dir=&quot;auto&quot;&gt;PRD.md&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;SUMMARY.md&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;tasks.json&lt;/code&gt; with one &lt;code dir=&quot;auto&quot;&gt;TASK-{ID}.json&lt;/code&gt; spec per task. From there you run the loop:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@pageai/ralph-loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You can amend later. When you want to add a feature or fix a bug mid-project, run the skill again to update the PRD and append tasks. The spec grows with the project instead of going stale the moment you start coding.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;write-acceptance-criteria-the-agent-can-verify&quot;&gt;Write acceptance criteria the agent can verify&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;This is the part that separates a PRD an agent can build from a PRD that produces confident nonsense. An acceptance criterion is only useful if the agent can check it without you. The test is simple: can the agent confirm this by running something and reading the output? If not, rewrite it.&lt;/p&gt;
&lt;p&gt;Three rules make a criterion verifiable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Name the input and the expected output.&lt;/strong&gt; Vague: “the endpoint validates email.” Verifiable: “POST /api/register with email ‘not-an-email’ returns 400 and the body &lt;code dir=&quot;auto&quot;&gt;{ error: &apos;Please enter a valid email&apos; }&lt;/code&gt;.” Now the agent can send the request, read the status and body, and compare.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Point at an observable artifact.&lt;/strong&gt; “Passwords are secure” is unprovable. “The stored password starts with the bcrypt prefix $2b$ and never equals the plaintext value” can be confirmed by reading the row. Anchor the criterion to something the agent can inspect: a database row, a response header, a file on disk, a console exit code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Make it pass or fail, never partial.&lt;/strong&gt; A criterion that needs interpretation is a criterion the agent will interpret in its favor. “The UI looks clean” invites argument. “The submit button is disabled until both fields are non-empty” does not.&lt;/p&gt;
&lt;p&gt;In Ralph, the criteria do not float in the PRD. They land in the per-task spec files. Each &lt;code dir=&quot;auto&quot;&gt;TASK-{ID}.json&lt;/code&gt; carries an &lt;code dir=&quot;auto&quot;&gt;acceptanceCriteria&lt;/code&gt; array, and the tests that prove those criteria are steps inside the task, not separate tasks scheduled for later.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-3&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST /api/auth/register creates a new user account&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;category&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;api-endpoint&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Validate input, hash the password, store the user, return a success response.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;acceptanceCriteria&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;POST with valid email and password returns 201 with the user id and email&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Invalid email format returns 400 with the error text Please enter a valid email&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Password shorter than 8 characters returns 400&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Duplicate email returns 409, not a generic 500&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;The stored password starts with $2b$ and is never the plaintext value&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;steps&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;step&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Add the register route handler&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;details&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Validate with a zod schema, hash with bcrypt, insert into users.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;pass&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;step&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;description&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Write Vitest cases for every acceptance criterion&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;details&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Cover valid registration, invalid email, short password, duplicate email, and the stored hash prefix.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;pass&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;TASK-2&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;estimatedComplexity&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;medium&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Every line in &lt;code dir=&quot;auto&quot;&gt;acceptanceCriteria&lt;/code&gt; maps to something the verification stack can confirm. The loop assumes that stack: Playwright for end to end, Vitest for unit tests, TypeScript for types, ESLint for lint, Prettier for format. The repo mantra is blunt: if you didn’t test it, it doesn’t work. The agent runs the gate, and only after it passes does it flip &lt;code dir=&quot;auto&quot;&gt;passes&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt;, take a screenshot, and commit. Turning criteria into atomic, independently verifiable packets like this is its own discipline, covered in &lt;a href=&quot;https://ralphloop.sh/blog/break-prd-into-agent-tasks/&quot;&gt;breaking a PRD into atomic agent tasks&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;name-env-vars-libraries-and-user-flows-so-the-agent-does-not-guess&quot;&gt;Name env vars, libraries, and user flows so the agent does not guess&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The fastest way to get an agent to invent something wrong is to leave a decision implicit. Three categories cause the most trouble, and the PRD should pin all three.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Environment variables.&lt;/strong&gt; List every variable the project reads, with a one line note on what each is for. The &lt;code dir=&quot;auto&quot;&gt;prd-creator&lt;/code&gt; skill writes placeholders into &lt;code dir=&quot;auto&quot;&gt;.env.local&lt;/code&gt; during prerequisite verification, so the agent knows the keys exist without ever seeing a real secret. Without this, an agent guesses variable names, scatters them across files, and you spend the morning reconciling &lt;code dir=&quot;auto&quot;&gt;DATABASE_URL&lt;/code&gt; against &lt;code dir=&quot;auto&quot;&gt;DB_CONNECTION_STRING&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Libraries and the stack.&lt;/strong&gt; Say which framework, which ORM, which validation library, which test runner. If the project already uses zod, the PRD should say so, or the agent will reach for whatever it saw most recently in its training and add a second validation library next to your first. Naming the stack is also where you encode reuse: tell the agent to extend the existing auth module rather than write a parallel one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;User flows.&lt;/strong&gt; A flow is a sequence of steps with branches, and the branches are where agents guess. “Users can reset their password” hides a dozen decisions. Does the reset link expire? After how long? What happens on an expired link? Does requesting a reset for an unknown email reveal that the account does not exist? Write the flow as steps and edge cases, and each edge case becomes an acceptance criterion instead of a surprise in production.&lt;/p&gt;
&lt;p&gt;The pattern across all three: every decision you leave out is a decision the agent makes for you, silently, at the moment it is most expensive to change. The PRD is where you make those decisions while they are still cheap.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-short-worked-example&quot;&gt;A short worked example&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Take the link shortener from the prompt above and watch the three properties show up.&lt;/p&gt;
&lt;p&gt;The goal is concrete: “an authenticated user creates a short link with an optional custom slug and sees total clicks per link.” Not “build a great link tool.” The constraint section names the stack and fences the scope: custom domains and team accounts are out of scope for version one. The acceptance criteria get specific per feature. For slug generation: “a generated slug is 7 characters of base62” and “a collision retries up to 3 times before returning an error.” For the redirect: “GET /:slug on an unknown slug returns 404” and “a valid slug records exactly one click row and issues a 302 to the target URL.”&lt;/p&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;prd-creator&lt;/code&gt; interview is where these get extracted. Are slugs unique globally or per account? What happens on a collision? Do analytics count unique visitors or raw hits? Each answer becomes a line in the PRD, and the unanswered questions become the edge cases that would otherwise blow up the loop. By the time the PRD is approved, the ambiguity is gone, and the task list inherits criteria a machine can check. The loop then runs one task at a time, which is the rule that keeps a long run from drifting, explained in &lt;a href=&quot;https://ralphloop.sh/blog/one-task-per-iteration/&quot;&gt;one task per iteration&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The framing of phases here (specify intent, plan the approach, decompose into tasks, implement and verify) comes from &lt;a href=&quot;https://github.com/github/spec-kit&quot;&gt;GitHub Spec Kit&lt;/a&gt;, and the loop that runs it autonomously was popularized by Geoffrey Huntley in his &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;original Ralph writeup&lt;/a&gt;. The PRD is the artifact the first phase produces and every later phase consumes.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-to-go-next&quot;&gt;Where to go next&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you are writing the spec that drives a loop, read down through the spec-driven cluster:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/spec-driven-development-with-ai/&quot;&gt;Spec-driven development with AI&lt;/a&gt; for the full Specify, Plan, Tasks, Implement workflow this PRD sits inside.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/break-prd-into-agent-tasks/&quot;&gt;Breaking a PRD into atomic agent tasks&lt;/a&gt; for turning the PRD into independently verifiable packets.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ralphloop.sh/blog/one-task-per-iteration/&quot;&gt;One task per iteration&lt;/a&gt; for the rule that keeps long runs reliable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the mechanics of the loop that reads your PRD on every pass, the fresh-context design, and where the technique came from, read &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;what is the Ralph technique&lt;/a&gt;.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What makes a PRD buildable by an AI agent rather than a person?&lt;/h3&gt; &lt;p&gt;A buildable PRD states concrete goals, explicit constraints, and verifiable acceptance criteria. A person can fill gaps with judgment and ask a teammate, but an agent invents an answer whenever the spec is silent. So the agent PRD must name the stack, fence what is out of scope, and define each acceptance criterion as a condition a machine can confirm by running a command and reading the output.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What is the difference between PRD.md and SUMMARY.md in Ralph?&lt;/h3&gt; &lt;p&gt;PRD.md is the full document with app overview, target audience, success metrics, core features and user flows, technical stack, prerequisites, security considerations, and assumptions. SUMMARY.md is a short executive overview with the main features, key user flows, and key requirements. The summary is what gets sent to the agent every iteration so it reorients fast without rereading the entire PRD.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I write acceptance criteria an agent can verify?&lt;/h3&gt; &lt;p&gt;Name the input and the expected output, point at an observable artifact, and make every criterion pass or fail with no interpretation. For example, POST with an invalid email returns 400 with a specific error text, or the stored password starts with the bcrypt prefix and is never the plaintext value. The agent confirms each one with Playwright, Vitest, type checks, and lint before it marks the task done.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I create the PRD without writing it all by hand?&lt;/h3&gt; &lt;p&gt;Use the prd-creator skill in plan mode. It interviews you one question at a time, researches the competitive landscape, verifies prerequisites, and writes placeholder values into .env.local without ever storing a real secret. It then writes PRD.md and SUMMARY.md and generates tasks.json with a prerequisite verification task first. You can run it again later to amend the PRD and append tasks.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why does naming env vars, libraries, and user flows matter so much?&lt;/h3&gt; &lt;p&gt;Every decision left implicit is a decision the agent makes for you, silently, at the moment it is most expensive to change. If you do not name the validation library, the agent may add a second one. If you do not spell out the password reset flow, it guesses how expiry and unknown emails behave. Listing variables, the stack, and flows with their edge cases turns those guesses into acceptance criteria.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>spec-driven</category></item><item><title>Verification Loops: Why Autonomous Agents Need Tests and Screenshots</title><link>https://ralphloop.sh/blog/verification-loops-for-ai-agents/</link><guid isPermaLink="true">https://ralphloop.sh/blog/verification-loops-for-ai-agents/</guid><description>An autonomous agent only stays safe when every task ends in a machine-checkable result. Tests, type checks, lint, and screenshots are the feedback loop that lets the agent grade its own work.

</description><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you didn’t test it, it doesn’t work. That one rule is what makes autonomy safe. An AI agent that writes code with no way to check the result is just generating plausible text. The same agent wired to a verification loop (run the tests, read the failures, fix them, run again) can grade its own work and only mark a task done when the evidence says so. Verification is not a nice extra on top of an autonomous loop. It is the feedback signal the loop runs on.&lt;/p&gt;
&lt;p&gt;This post is about that signal. What the verification stack looks like, why type checks and lint are the cheap gates you run first, why screenshots are the proof for UI work, and how the agent feeds a failing test back into the next iteration instead of declaring victory. The short version: every task ends with a result a machine can confirm, and the agent reads that result before it moves on.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-verification-is-what-makes-autonomy-safe&quot;&gt;Why verification is what makes autonomy safe&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The danger of an autonomous agent is not that it writes bad code once. It is that it writes bad code, believes the code is fine, marks the task done, and builds the next ten tasks on top of the broken one. By the time you wake up, the loop has compounded a small mistake into a tangled diff. Verification is the thing that stops compounding. It forces the agent to confront reality at the end of every task.&lt;/p&gt;
&lt;p&gt;An agent left to self-assess on vibes will tell you it is confident. Confidence is not evidence. A failing assertion is evidence. A red type error is evidence. A screenshot that shows the button in the wrong place is evidence. The job of a verification loop is to replace the agent’s opinion of its work with a result that came from running the work.&lt;/p&gt;
&lt;p&gt;This is why the loop architecture and the verification stack are inseparable. The &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-coding-agent-overnight/&quot;&gt;overnight run pillar&lt;/a&gt; covers how a Ralph loop keeps an agent productive for hours by resetting context and storing state on disk. None of that matters if the recorded state is a lie. Verification is what keeps &lt;code dir=&quot;auto&quot;&gt;status: done&lt;/code&gt; honest, so a fresh agent in the next iteration can trust the disk instead of re-checking everything. The mantra in the repo is blunt: if you didn’t test it, it doesn’t work.&lt;/p&gt;
&lt;p&gt;In a Ralph loop, each iteration follows the same shape, and verification sits in the middle of it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the highest-priority incomplete task in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Work the steps in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run tests, linting, and type checking.&lt;/li&gt;
&lt;li&gt;Complete the task, take a screenshot, update task status, and commit.&lt;/li&gt;
&lt;li&gt;Repeat until all tasks pass or the iteration cap is reached.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Step 3 is the gate. A task does not reach step 4 until step 3 is green. That single ordering is what separates an autonomous loop you can leave running from a code generator you have to babysit.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-verification-stack-tests-types-lint-format-screenshots&quot;&gt;The verification stack: tests, types, lint, format, screenshots&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The verification stack the loop assumes is five tools, each catching a different class of mistake. You run them as gates, fastest and cheapest first, so the agent gets a signal in seconds instead of waiting on a full browser run for a problem a type check would have caught.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;typescript-and-eslint-are-the-cheap-gates&quot;&gt;TypeScript and ESLint are the cheap gates&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Run the static checks first because they are fast and they catch the dumbest mistakes. A type error or a lint failure tells the agent the code is wrong before a single test boots a runtime.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Type check: no emit, just verify the types hold&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tsc&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--noEmit&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Lint: catch unused vars, bad imports, banned patterns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;eslint&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;These run in seconds and they fail loud. An agent that renamed a function but missed a caller gets a type error pointing at the exact file and line. That is a precise signal the agent can act on without guessing. Cheap gates first means the expensive gates (the browser tests) only run on code that already passes the basics.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;prettier-keeps-the-diff-reviewable&quot;&gt;Prettier keeps the diff reviewable&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Formatting is not about taste in an autonomous loop. It is about keeping the morning diff readable. If every iteration reformats the file its own way, your &lt;code dir=&quot;auto&quot;&gt;git diff&lt;/code&gt; fills with noise and you cannot see what actually changed.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Verify formatting without writing changes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;prettier&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--check&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Run this as a gate and the agent is forced to leave the code in the canonical format. The reviewer (you) gets a diff that shows logic changes, not whitespace churn.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;vitest-catches-logic-regressions&quot;&gt;Vitest catches logic regressions&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Unit tests are where the agent proves the logic does what the task said. Vitest runs fast enough to run on every iteration, which is the property that matters. A test suite you only run nightly is not part of the feedback loop.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Run the unit suite once, no watch mode&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;vitest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A unit test failure gives the agent the most actionable signal of all: an expected value, an actual value, and the exact assertion that broke. The agent reads that diff and knows precisely what its change got wrong. This is the difference between “something is off” and “the function returned 3 when the test expected 4”.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;playwright-proves-the-user-flow&quot;&gt;Playwright proves the user flow&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;For anything a user clicks through, unit tests are not enough. Playwright drives a real browser, so the agent verifies the flow end to end: navigate, fill the form, submit, assert the result on screen.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Run the end-to-end suite headless&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;playwright&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;End-to-end tests catch the class of bug that passes every unit test and still breaks in the browser: a missing prop, a broken route, a handler wired to the wrong element. They are slower, which is exactly why they sit last in the gate order. By the time Playwright runs, the cheap checks have already filtered out the obvious failures.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;screenshots-are-the-proof-for-ui-work&quot;&gt;Screenshots are the proof for UI work&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A test that passes tells you the DOM is correct. It does not tell you the page looks right. For UI work, the agent takes a screenshot and that screenshot is the proof. It is the one artifact that lets a human (or a vision-capable agent) confirm the thing actually renders the way the task described.&lt;/p&gt;
&lt;p&gt;Playwright captures screenshots as part of a test run, so this folds into the same gate:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// Inside a Playwright test, capture proof of the rendered state&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;goto&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/dashboard&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;expect&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getByRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, { name: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;Save&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; }))&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toBeVisible&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;screenshot&lt;/span&gt;&lt;span&gt;({ path: &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;artifacts/dashboard.png&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, fullPage: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Two reasons screenshots earn their place in the loop. First, they catch what assertions miss. A button can be present in the DOM, pass every &lt;code dir=&quot;auto&quot;&gt;toBeVisible&lt;/code&gt; check, and still sit behind a modal or off the edge of the viewport. The screenshot shows it. Second, they are the audit trail. When you review a long autonomous run in the morning, the screenshots are how you confirm each UI task landed without re-running anything yourself. That auditability is the same idea covered in &lt;a href=&quot;https://ralphloop.sh/blog/observability-for-ai-coding-agents/&quot;&gt;observability for autonomous coding agents&lt;/a&gt;: you cannot trust what you cannot see, and a screenshot is the cheapest way to see it.&lt;/p&gt;
&lt;p&gt;Screenshots and type checks sit at opposite ends of the cost spectrum. Type and lint checks are nearly free and run constantly. A full browser screenshot is expensive and runs once per UI task at the end. You want both: the cheap gates to fail fast on logic, the screenshot to confirm the pixels.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-verification-results-feed-the-next-iteration&quot;&gt;How verification results feed the next iteration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here is the part that turns verification from a checkbox into a loop. The agent does not just run the gates and pass or fail. It reads the failing output and feeds it back into the next attempt. A stack trace, a failed assertion, a type error: each is structured feedback the agent uses to make the specific fix, then it runs the gates again.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
  Task[&quot;Read task spec and acceptance criteria&quot;]
  Implement[&quot;Implement the change&quot;]
  Verify[&quot;Run gates: tsc, eslint, prettier, vitest, playwright&quot;]
  Pass{&quot;All gates green?&quot;}
  ReadFail[&quot;Read failing output: stack trace, assertion, type error&quot;]
  Fix[&quot;Make the targeted fix&quot;]
  Screenshot[&quot;Capture screenshot for UI work&quot;]
  Commit[&quot;Update tasks.json, commit&quot;]
  Task --&gt; Implement --&gt; Verify --&gt; Pass
  Pass --&gt;|no| ReadFail --&gt; Fix --&gt; Verify
  Pass --&gt;|yes| Screenshot --&gt; Commit&lt;/pre&gt;
&lt;p&gt;The inner cycle (verify, read failure, fix, verify) is the verification loop proper. It can run several times inside a single task before the gates go green. This is the same reason iteration beats a single shot in general: the agent sees its own mistake and corrects it instead of guessing once and hoping. The &lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-vs-one-shot-prompting/&quot;&gt;Ralph loop vs one-shot prompting&lt;/a&gt; comparison makes that case directly. A one-shot prompt has no failing test to read, so it cannot self-correct. A verification loop hands the agent a precise error message and a chance to act on it.&lt;/p&gt;
&lt;p&gt;The quality of the feedback decides how well this works. Good test output names the file, the line, and the difference between expected and actual. That precision is what lets a fresh-context agent fix a bug it has never seen, because the failure itself carries enough information to locate and correct the problem. Vague output (a generic “something failed” with no detail) starves the loop of signal and the agent thrashes. Invest in error messages and assertions that say exactly what went wrong.&lt;/p&gt;
&lt;p&gt;This also connects to how the loop keeps its memory honest. Because each task only commits after the gates pass, the git history and &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt; become a trustworthy record. A later iteration that reads &lt;code dir=&quot;auto&quot;&gt;status: done&lt;/code&gt; does not need to re-verify; the commit is the receipt. That is the discipline described in &lt;a href=&quot;https://ralphloop.sh/blog/context-engineering-for-long-running-agents/&quot;&gt;context engineering for long-running agents&lt;/a&gt;, where the filesystem and git log are the memory layer. Verification is what makes that memory worth trusting.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-do-you-design-machine-checkable-acceptance-criteria&quot;&gt;How do you design machine-checkable acceptance criteria?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Verification only works if the task tells the agent what “done” means in terms a machine can check. An acceptance criterion like “the login page should work well” is useless to a loop. There is nothing to run. A criterion like “submitting valid credentials redirects to /dashboard and the Vitest suite for auth passes” is checkable. The agent can run it and get a yes or no.&lt;/p&gt;
&lt;p&gt;The rule of thumb: every acceptance criterion in a &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt; spec should map to a gate. Write criteria that a test, a type check, or a screenshot can confirm. If you cannot point at the command that proves a criterion, the criterion is too vague to belong in an autonomous task.&lt;/p&gt;
&lt;p&gt;What machine-checkable criteria look like in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bind to a named test. “The new &lt;code dir=&quot;auto&quot;&gt;parsePrice&lt;/code&gt; unit test passes” beats “prices parse correctly”. The agent runs &lt;code dir=&quot;auto&quot;&gt;npx vitest run&lt;/code&gt; and reads the result.&lt;/li&gt;
&lt;li&gt;Bind to an end-to-end flow. “A user can add an item to the cart and the cart count shows 1” maps to a Playwright spec the agent can run.&lt;/li&gt;
&lt;li&gt;Bind to a screenshot. “The settings page renders with the dark-mode toggle visible” is confirmed by capturing the page and checking the toggle is in frame.&lt;/li&gt;
&lt;li&gt;Bind to the static gates. “No type errors, no lint errors, formatting clean” is the floor every task clears.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Criteria written this way are what let the agent finish a task without asking you. It knows it is done because the gates it was given are green. This is the same property that makes &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-coding-agent-overnight/&quot;&gt;one task per iteration&lt;/a&gt; reliable: an atomic task with checkable criteria is a unit the agent can verify on its own, commit, and move past. A task with fuzzy criteria forces the agent to guess at completion, which is exactly when an autonomous loop goes off the rails.&lt;/p&gt;
&lt;p&gt;A useful habit is to write the test first as part of the spec. When the task spec includes the failing test the agent must make pass, the acceptance criterion and the verification command are the same thing. The agent’s whole job becomes “turn this red test green”, and the loop can confirm completion mechanically.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;putting-it-together-verification-as-the-loops-nervous-system&quot;&gt;Putting it together: verification as the loop’s nervous system&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Verification is not a phase that runs after the work. It is the nervous system the autonomous loop runs through. Strip it out and every other part of the architecture loses its meaning. Fresh context per iteration is pointless if the agent records unverified work. Disk-based memory is a liability if the state on disk is wrong. Atomic tasks do not help if “done” is a guess.&lt;/p&gt;
&lt;p&gt;The stack does the catching, cheapest gate first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript and ESLint fail in seconds on the obvious mistakes.&lt;/li&gt;
&lt;li&gt;Prettier keeps the diff clean so the morning review is fast.&lt;/li&gt;
&lt;li&gt;Vitest proves the logic with precise, actionable assertions.&lt;/li&gt;
&lt;li&gt;Playwright proves the user flow in a real browser.&lt;/li&gt;
&lt;li&gt;Screenshots prove the UI renders the way the task described.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The loop does the correcting: run the gates, read the failures, make the targeted fix, run again, and only commit when everything is green. Acceptance criteria that bind to those gates are what let the agent grade itself and a human trust the result without redoing it.&lt;/p&gt;
&lt;p&gt;Get this right and an autonomous run stops being a leap of faith. Every task in the morning diff is backed by a green suite and a screenshot. You are not trusting the agent. You are trusting the evidence it was forced to produce. That is what makes it safe to close the laptop and let the loop run.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What is a verification loop for an AI coding agent?&lt;/h3&gt; &lt;p&gt;A verification loop is the cycle where an agent implements a change, runs automated gates like type checks, lint, unit tests, and end-to-end tests, reads any failures, makes a targeted fix, and runs the gates again until they pass. Only then does it mark the task done and commit. The verification result, not the agent confidence, decides when a task is finished.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Which tests should an autonomous agent run on every iteration?&lt;/h3&gt; &lt;p&gt;Run the cheap static gates first because they are fast: TypeScript with no emit to catch type errors, ESLint for code issues, and Prettier in check mode for formatting. Then run Vitest for unit logic and Playwright for end-to-end flows. Run them in that order so the slow browser tests only execute on code that already passes the basics.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why do AI agents take screenshots when they finish UI work?&lt;/h3&gt; &lt;p&gt;A passing test confirms the DOM is correct but not that the page looks right. A screenshot is proof that the UI actually renders the way the task described, catching things like an element hidden behind a modal or pushed off the viewport. Screenshots also serve as an audit trail, so a human reviewing a long run can confirm each UI task landed without re-running anything.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do verification results feed back into the next iteration?&lt;/h3&gt; &lt;p&gt;The agent reads the failing output: a stack trace, a failed assertion with expected and actual values, or a type error pointing at a file and line. That precise signal tells the agent what to fix. It makes the targeted change and runs the gates again. Because a task only commits after the gates pass, later iterations can trust the recorded status instead of re-checking the work.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What makes an acceptance criterion machine-checkable?&lt;/h3&gt; &lt;p&gt;A machine-checkable criterion maps to a command that returns a yes or no. Bind each criterion to a named test, an end-to-end flow, a screenshot, or the static gates. For example, submitting valid credentials redirects to /dashboard and the auth Vitest suite passes is checkable, while the login page should work well is not. If you cannot point at the command that proves a criterion, it is too vague for an autonomous task.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>autonomous-agents</category></item><item><title>How to Run the Gemini CLI in an Autonomous Coding Loop</title><link>https://ralphloop.sh/blog/run-gemini-cli-in-a-loop/</link><guid isPermaLink="true">https://ralphloop.sh/blog/run-gemini-cli-in-a-loop/</guid><description>Point Ralph at Google&apos;s Gemini CLI with one flag, pick a model after the separator, log in once inside the sandbox, and let it grind through your task list with a fresh context each iteration.

</description><pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;To run Google’s Gemini CLI as an autonomous coding agent, point Ralph at it with one flag: &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --agent gemini&lt;/code&gt;. Ralph runs the Gemini CLI in its non-interactive prompt mode inside a Docker Sandbox, starts the agent with a fresh context window every iteration, and keeps re-running it against your task list until the work is done or you hit the iteration cap. Pick a model after the &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; separator, authenticate once inside the sandbox, and walk away.&lt;/p&gt;
&lt;p&gt;This is the Gemini-specific walkthrough in the larger guide to &lt;a href=&quot;https://ralphloop.sh/blog/agentic-coding-clis/&quot;&gt;agentic coding CLIs&lt;/a&gt;. The loop mechanics are identical to &lt;a href=&quot;https://ralphloop.sh/blog/run-codex-cli-in-a-loop/&quot;&gt;running the Codex CLI in a loop&lt;/a&gt; and &lt;a href=&quot;https://ralphloop.sh/blog/run-cursor-cli-agent-loop/&quot;&gt;running the Cursor CLI agent in a loop&lt;/a&gt;. Only the agent binary and its flags change.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;run-the-gemini-cli-in-a-loop-with-one-flag&quot;&gt;Run the Gemini CLI in a loop with one flag&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph is a Bash script you point at a project. Claude is the default agent, so you switch to Gemini explicitly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That runs 10 iterations, the default. Change the count when you want a longer unattended session or a single smoke test:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 50 iterations&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# exactly one iteration (good for a dry run)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# explicit cap&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--max-iterations&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The short form &lt;code dir=&quot;auto&quot;&gt;-a&lt;/code&gt; works too: &lt;code dir=&quot;auto&quot;&gt;./ralph.sh -a gemini -n 20&lt;/code&gt;. Supported agents are &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt; (default), &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt;, so the same harness drives any of them with the same flags.&lt;/p&gt;
&lt;p&gt;Under the hood, Ralph builds a Gemini command and runs it inside a Docker Sandbox. The expansion for &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --agent gemini&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-gemini-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$PROMPT_CONTENT&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;-p&lt;/code&gt; flag (long form &lt;code dir=&quot;auto&quot;&gt;--prompt&lt;/code&gt;) is the Gemini CLI’s non-interactive mode. It reads a prompt, does the work, prints a final message, and exits. That clean exit is what lets Ralph treat each iteration as a discrete unit instead of one long interactive session. See the &lt;a href=&quot;https://github.com/google-gemini/gemini-cli&quot;&gt;Gemini CLI documentation&lt;/a&gt; for the full command surface.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;select-a-gemini-model-after-the--separator&quot;&gt;Select a Gemini model after the — separator&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Anything to the right of Ralph’s own &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; separator is forwarded straight to the agent. For Gemini, Ralph inserts those arguments after the sandbox’s &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt;, before the &lt;code dir=&quot;auto&quot;&gt;-p&lt;/code&gt; prompt. So this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;expands to:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-gemini-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$PROMPT_CONTENT&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;--model&lt;/code&gt; flag (short form &lt;code dir=&quot;auto&quot;&gt;-m&lt;/code&gt;) picks the model for the run. Use the separator for any valid Gemini CLI flag, not just the model:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# pick a model&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# combine the model with a longer run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The rule to remember: everything left of &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; configures Ralph (agent, iteration count, login). Everything right of &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; configures Gemini. Keep your arguments on the correct side and the loop behaves.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;log-in-to-gemini-inside-the-sandbox&quot;&gt;Log in to Gemini inside the sandbox&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Gemini runs inside an isolated Docker Sandbox, not on your host, so it needs credentials in that environment. Authenticate once with the login action:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This prints the login command for every supported agent, highlights the one for Gemini, and drops you into the sandbox shell. Inside, you run &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt; once and complete its authentication (sign in with your Google account or set a &lt;code dir=&quot;auto&quot;&gt;GEMINI_API_KEY&lt;/code&gt;). The credential persists in that named sandbox, so later runs attach to the same box and start already authenticated.&lt;/p&gt;
&lt;p&gt;Each agent gets its own deterministic sandbox name, derived from the agent slug, the project directory, and a hash of the absolute path:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;project-dir&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For Gemini that is &lt;code dir=&quot;auto&quot;&gt;ralph-gemini-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/code&gt;. Print the exact name for your project without starting a run:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Per-agent names matter because they keep state separate. Your Gemini sandbox and your Claude sandbox never share credentials, history, or installed tools. If Gemini is not authenticated when the loop starts, Ralph watches for auth-failure patterns like &lt;code dir=&quot;auto&quot;&gt;API key not valid&lt;/code&gt;, stops, and tells you to run &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --login --agent gemini&lt;/code&gt;. No silent thrashing on a box that can never make progress.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-happens-each-iteration-fresh-context&quot;&gt;What happens each iteration (fresh context)&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph’s loop is the Bash loop Geoffrey Huntley described in the &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;original Ralph writeup&lt;/a&gt;. Each pass is mechanical and identical:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the highest-priority incomplete task in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Work the steps in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run tests, linting, and type checking.&lt;/li&gt;
&lt;li&gt;Complete the task, take a screenshot, update the task status, and commit.&lt;/li&gt;
&lt;li&gt;Repeat until all tasks pass or the iteration cap is reached.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The critical part is that each iteration spawns a fresh Gemini process with a clean context window. The agent does not carry a bloated, hours-long transcript from one task to the next. It reads the current state from disk, does one task, and exits. That is the fix for context rot, the failure mode where an agent slowly loses the plot over a long session.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
    Start([&quot;./ralph.sh -a gemini -- --model pro&quot;]) --&gt; Pick[&quot;Pick top task from .agent/tasks.json&quot;]
    Pick --&gt; Spawn[&quot;sbx run gemini . -- -p (fresh context)&quot;]
    Spawn --&gt; Work[&quot;Gemini reads state from disk, edits files, runs commands&quot;]
    Work --&gt; Verify[&quot;Run tests, lint, type check, screenshot&quot;]
    Verify --&gt; Commit[&quot;Commit and update task status&quot;]
    Commit --&gt; Check{&quot;Promise tag emitted?&quot;}
    Check --&gt;|&quot;none&quot;| Pick
    Check --&gt;|&quot;COMPLETE&quot;| Done([&quot;exit 0, all tasks done&quot;])
    Check --&gt;|&quot;BLOCKED or DECIDE&quot;| Stop([&quot;exit 2 or 3, wants a human&quot;])&lt;/pre&gt;
&lt;p&gt;The filesystem and git history are the memory layer. Progress lives in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt;, per-task spec files, and the git log, not in a chat transcript. That is what keeps a fresh-context agent oriented across dozens of iterations.&lt;/p&gt;
&lt;p&gt;A loop also needs a stop condition that is a signal, not a vibe. Gemini emits a semantic promise tag in its final message, and Ralph reads it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; means every task is finished.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; means the agent needs human help.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt; means it needs a decision you have to make.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those map to exit codes: &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; for COMPLETE, &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; for hitting MAX_ITERATIONS, &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt; for BLOCKED, and &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt; for DECIDE. Wire those into a wrapper script or a CI step and you get clean branching: ship on &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;, page yourself on &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt;, extend the cap on &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One rule keeps the whole thing reliable: one task per invocation. Gemini completes exactly one task, commits, and stops. It never batches several tasks into a single iteration, which is what keeps each commit small, each diff reviewable, and each context window focused on a single goal.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;verify-every-iteration-with-tests-and-screenshots&quot;&gt;Verify every iteration with tests and screenshots&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A loop is only as good as its feedback. If Gemini cannot tell whether its change worked, it will happily mark a broken task done and move on. The repo mantra is blunt: if you didn’t test it, it doesn’t work.&lt;/p&gt;
&lt;p&gt;Ralph assumes a verification stack and runs it inside step three of every iteration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Playwright for end-to-end tests.&lt;/li&gt;
&lt;li&gt;Vitest for unit tests.&lt;/li&gt;
&lt;li&gt;TypeScript for type checking.&lt;/li&gt;
&lt;li&gt;ESLint for linting.&lt;/li&gt;
&lt;li&gt;Prettier for formatting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gemini runs those commands itself, reads the failures, and fixes them before committing. Screenshots add a second channel: the agent captures the UI state so you can eyeball the result in the morning instead of reading diffs blind.&lt;/p&gt;
&lt;p&gt;Because every iteration starts fresh, verification is also how the next iteration learns what the last one did. The agent does not remember the previous run. It reads the test results, the updated task status, and the new commits, then decides what is next. That feedback loop is the whole point, and it works the same across every agent in this family.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;safe-autonomy-the-sandbox-is-the-boundary&quot;&gt;Safe autonomy: the sandbox is the boundary&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;For an unattended loop there is nobody to approve a file write or a shell command, so the agent has to run without pausing for permission. On your laptop that is reckless. Inside a sandbox it is fine, because the blast radius is the microVM, not your machine.&lt;/p&gt;
&lt;p&gt;A Ralph loop runs Gemini inside a Docker Sandbox: an isolated microVM with its own kernel, an isolated filesystem, and a network that is deny-by-default. The sandbox is the boundary you enforce, so you do not need the agent policing itself. For the full argument, including why a microVM beats a hand-rolled container, read &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-agents-in-docker-sandboxes-safely/&quot;&gt;how to run AI coding agents in Docker sandboxes safely&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When the agent needs a package, the deny-by-default network blocks it until you allow the domain:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-gemini-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;registry.npmjs.org&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That is a feature, not a hurdle. The agent can install what a task needs without a path to reach arbitrary hosts or exfiltrate your source. The &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/&quot;&gt;Docker Sandboxes documentation&lt;/a&gt; covers the policy model in full, including the global &lt;code dir=&quot;auto&quot;&gt;-g&lt;/code&gt; form and the &lt;code dir=&quot;auto&quot;&gt;&quot;**&quot;&lt;/code&gt; wildcard for the rare case where you want to open everything.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;inspect-and-debug-the-gemini-sandbox&quot;&gt;Inspect and debug the Gemini sandbox&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When a run stalls or a task keeps failing, get inside the box. The sandbox is a normal container you can poke at. List what exists:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Open a shell in the Gemini sandbox and look around:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-gemini-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;From there you can check the working tree, re-run a failing test by hand, inspect installed tools, or read &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt; and the per-iteration logs in &lt;code dir=&quot;auto&quot;&gt;.agent/history/&lt;/code&gt;. Reattach to a sandbox session with:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-gemini-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Most stalls trace back to one of three things: Gemini was never authenticated (so every iteration fails the auth check), a network policy is blocking an install, or the prompt in &lt;code dir=&quot;auto&quot;&gt;.agent/PROMPT.md&lt;/code&gt; lacks a clear completion criterion. The sandbox shell shows you which one it is.&lt;/p&gt;
&lt;p&gt;If you need to redirect a running loop without killing it, edit &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt;. Ralph reads it and folds critical work into the next iteration before resuming the normal task list. That is steering, not stopping, and it keeps momentum while you correct course.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;putting-it-together&quot;&gt;Putting it together&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A real Gemini loop, start to finish, is three commands:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 1. authenticate once (creates the sandbox, you sign in inside it)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 2. confirm the sandbox name for network policies and debugging&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 3. run the loop with a model, inside the sandbox boundary&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That is Google’s Gemini CLI running unattended: a fresh context per iteration, state on disk, the microVM as the real boundary, and a hard stop on a completion promise. Define your tasks in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, write a clear &lt;code dir=&quot;auto&quot;&gt;.agent/PROMPT.md&lt;/code&gt;, and let it work through the list while you do something else.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;How do I run the Gemini CLI in a coding loop?&lt;/h3&gt; &lt;p&gt;Use Ralph and pass the agent flag: ./ralph.sh --agent gemini. Ralph runs the Gemini CLI in its non-interactive -p prompt mode inside a Docker Sandbox, starts a fresh context window each iteration, and repeats until every task in .agent/tasks.json is done or the iteration cap is reached. The default is 10 iterations; raise it with -n 50.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I choose a Gemini model through Ralph?&lt;/h3&gt; &lt;p&gt;Put it after the -- separator. Anything to the right of -- is forwarded to the agent, so ./ralph.sh -a gemini -- --model pro expands to gemini . -- --model pro -p with the Ralph prompt. The same separator works for any valid Gemini CLI flag.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I log in to Gemini inside the sandbox?&lt;/h3&gt; &lt;p&gt;Run ./ralph.sh --login --agent gemini. It drops you into the sandbox shell where you run gemini once and complete its authentication, either by signing in with your Google account or setting a GEMINI_API_KEY. The credential persists in that named sandbox, so future runs attach to the same box already authenticated. The sandbox is named ralph-gemini-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why does the loop start each iteration with a clean context?&lt;/h3&gt; &lt;p&gt;Long sessions rot. An agent that carries an hours-long transcript loses track of the goal. Ralph spawns a fresh Gemini process every iteration with a clean context window, and the agent rebuilds its understanding from disk: .agent/tasks.json, the task spec files, .agent/logs/LOG.md, and the git history. That filesystem state is the memory layer, not the chat.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does the loop know when to stop?&lt;/h3&gt; &lt;p&gt;Gemini emits a promise tag in its final message. &amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt; stops the loop with exit code 0, BLOCKED exits with 2, and DECIDE exits with 3. Hitting the iteration cap without completing exits with 1. You branch on those exit codes in a wrapper script or CI.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>agentic-clis</category></item><item><title>Completion Promises and Exit Codes: How a Ralph Loop Knows When to Stop</title><link>https://ralphloop.sh/blog/ralph-loop-completion-promise/</link><guid isPermaLink="true">https://ralphloop.sh/blog/ralph-loop-completion-promise/</guid><description>A Ralph loop stops on an explicit signal, not a guess. How promise tags and exit codes tell the loop and your scripts when the agent is COMPLETE, BLOCKED, or needs a decision.

</description><pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A Ralph loop stops on an explicit signal, not a guess. Each iteration the agent prints a promise tag, a short machine-readable status, and the loop reads it to decide whether to run again, stop, or hand control back to you. When the loop exits, &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; returns a numeric exit code that tells you and any surrounding automation exactly why it stopped. No “looks done”, no agent declaring victory on a feeling. A signal the script can match, and a code your shell can branch on.&lt;/p&gt;
&lt;p&gt;This matters because the alternative is an agent that loops forever, or one that quits the moment it gets tired of the task. The completion promise is the part of the &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;Ralph technique&lt;/a&gt; that turns an open-ended loop into a finite, scriptable job.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-does-a-ralph-loop-know-when-to-stop&quot;&gt;How does a Ralph loop know when to stop?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;It watches the agent’s output for a &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;&lt;/code&gt; tag and reacts to it. There are three signals the agent can emit, and four primary exit codes the script can return. That is the whole stop mechanism.&lt;/p&gt;
&lt;p&gt;The agent never decides when to terminate the process. It only reports status. The loop owns the decision to continue or halt, which keeps control in the script you can read and edit rather than buried inside the model.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-three-promise-tags&quot;&gt;The three promise tags&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A promise tag is a semantic status the agent writes to its output. The format is fixed so the loop can pattern-match it reliably:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;TYPE:content&amp;#x3C;/promise&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The loop scans both the raw agent output and the final summary for these tags after every iteration. There are three that change control flow.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;promisecompletepromise&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt;&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This means every task is finished. The agent has worked through &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, verified each one, committed, and found nothing left to do. When the loop sees &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt;, it prints a success banner and exits cleanly with code &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt; is the happy path. It is also the only tag that should be earned, not asserted. The agent is instructed to emit it only after the task list is empty and the verification gates have passed, which is the difference between real completion and an agent that wants to stop.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;promiseblockedreasonpromise&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt;&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This means the agent cannot continue without you. A missing credential, an ambiguous external dependency, a failing service it does not control. The agent attaches a human-readable reason after the colon, and the loop surfaces it instead of silently stalling.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;BLOCKED:Missing API credentials for the payments service&amp;#x3C;/promise&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When the loop detects a &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; tag, it extracts the reason, plays a notification, prints the blocked message with the iteration number, and exits with code &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt;. You read the reason, fix the thing, and run the loop again. The point is that a blocked agent tells you why it is blocked rather than thrashing on a task it can never finish.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;promisedecidequestionpromise&quot;&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt;&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This means the agent has hit a real decision point and wants your call before it commits to a direction. Not a blocker, a fork. Two valid architectures, a naming convention that will ripple across the codebase, a tradeoff the spec did not pin down.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;DECIDE:Should the new endpoint use REST or GraphQL?&amp;#x3C;/promise&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The loop extracts the question, notifies you, and exits with code &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt;. You answer the question (usually by updating the task spec or &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt;), then restart the loop so a fresh agent picks up with the decision settled.&lt;/p&gt;
&lt;p&gt;There is a fourth tag worth knowing, even though it does not stop the loop. &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;TASK-{ID}:DONE&amp;#x3C;/promise&gt;&lt;/code&gt; reports that a specific task finished during the current iteration. The loop collects these for progress display (the &lt;code dir=&quot;auto&quot;&gt;Tasks: TASK-1, TASK-2&lt;/code&gt; line you see after each pass) but keeps running. It is bookkeeping, not a stop signal.&lt;/p&gt;
&lt;p&gt;Where the agent learns to emit these tags is the prompt file. The instruction to print &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; lives in &lt;code dir=&quot;auto&quot;&gt;.agent/PROMPT.md&lt;/code&gt;, which is why &lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-prompt-file/&quot;&gt;writing a good PROMPT.md&lt;/a&gt; is what makes the promise reliable. A prompt that never tells the agent how to signal a blocker gets you an agent that fakes progress instead of asking for help.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-promise-and-exit-code-decision-flow&quot;&gt;The promise and exit code decision flow&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here is the control flow the loop runs after every iteration. The agent works one task, verifies it, commits, and prints its status. The loop reads the output and branches.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
    Start([&quot;Start ralph.sh -n N&quot;]) --&gt; Run[&quot;Run iteration i: agent works one task&quot;]
    Run --&gt; Scan[&quot;Scan output and final summary for promise tags&quot;]
    Scan --&gt; Complete{&quot;COMPLETE tag?&quot;}
    Complete --&gt;|Yes| Exit0[&quot;Exit 0 COMPLETE&quot;]
    Complete --&gt;|No| Blocked{&quot;BLOCKED tag?&quot;}
    Blocked --&gt;|Yes| Exit2[&quot;Exit 2 BLOCKED, print reason&quot;]
    Blocked --&gt;|No| Decide{&quot;DECIDE tag?&quot;}
    Decide --&gt;|Yes| Exit3[&quot;Exit 3 DECIDE, print question&quot;]
    Decide --&gt;|No| Cap{&quot;i reached max iterations?&quot;}
    Cap --&gt;|Yes| Exit1[&quot;Exit 1 MAX_ITERATIONS&quot;]
    Cap --&gt;|No| Next[&quot;Increment i, fresh context&quot;]
    Next --&gt; Run&lt;/pre&gt;
&lt;p&gt;Read it as a priority order. &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt; wins first, then &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt;, then &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt;. If none of the three fire and there is still iteration budget left, the loop spawns a fresh agent and runs again. Only when the budget runs out does it stop with the max-iterations code.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;exit-codes-and-what-to-do-with-them&quot;&gt;Exit codes and what to do with them&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; returns, its exit code is the single source of truth for why it stopped. These are defined in &lt;code dir=&quot;auto&quot;&gt;scripts/lib/constants.sh&lt;/code&gt;:&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Code&lt;/th&gt;&lt;th&gt;Name&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;COMPLETE&lt;/td&gt;&lt;td&gt;All tasks finished and verified.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;MAX_ITERATIONS&lt;/td&gt;&lt;td&gt;Hit the iteration cap with work pending.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;BLOCKED&lt;/td&gt;&lt;td&gt;Agent needs human help.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;DECIDE&lt;/td&gt;&lt;td&gt;Agent needs a human decision.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;DOCKER_ERROR&lt;/td&gt;&lt;td&gt;The sandbox failed to start or run.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;AUTH_ERROR&lt;/td&gt;&lt;td&gt;The agent is not authenticated.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Codes &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; through &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt; map one to one onto the loop’s logical outcomes. Codes &lt;code dir=&quot;auto&quot;&gt;4&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;5&lt;/code&gt; are environment failures: the Docker Sandbox did not come up, or the agent CLI is not logged in. Treat &lt;code dir=&quot;auto&quot;&gt;4&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;5&lt;/code&gt; as setup problems to fix, not as something the loop did wrong.&lt;/p&gt;
&lt;p&gt;A key point: exit code &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; is not a failure. It means the loop spent its iteration budget and there is still work in the queue, which is exactly what a safety cap is supposed to do. You read the log, decide whether to top up the budget, and run again.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;branching-on-the-exit-code-in-a-script&quot;&gt;Branching on the exit code in a script&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Because the codes are standard, you can wrap the loop in a script and react to each outcome. A &lt;code dir=&quot;auto&quot;&gt;case&lt;/code&gt; statement on &lt;code dir=&quot;auto&quot;&gt;$?&lt;/code&gt; covers every branch:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;#!/usr/bin/env bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$code&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;All tasks complete. Ship it.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Hit the iteration cap. Topping up and rerunning.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt; ;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Blocked. A human needs to clear something.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Decision needed. Check the question and update the spec.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Sandbox failed to start. Check Docker.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Not authenticated. Run ./ralph.sh --login first.&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;*)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Unexpected exit code: &lt;/span&gt;&lt;span&gt;$code&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;esac&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is the difference between an agent loop you babysit and one you can automate. The script tells you whether to walk away, top up the budget, or step in.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;branching-on-the-exit-code-in-ci&quot;&gt;Branching on the exit code in CI&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The same property makes the loop usable in continuous integration or a scheduled job. CI treats exit &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; as success and everything else as failure by default, which is almost right. You usually want to fail the pipeline on a blocker or a decision, succeed on completion, and treat the iteration cap as a soft outcome that needs a human glance.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$?&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Complete is a pass. Blocked and Decide are hard failures.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Max iterations is a soft outcome we flag but do not hard-fail.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$code&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-eq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;elif&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$code&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-eq&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;::warning::Ralph hit max iterations with work pending&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;else&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Ralph stopped with code &lt;/span&gt;&lt;span&gt;$code&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$code&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;fi&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Running an agent unattended in CI only works because each agent runs inside an isolated Docker Sandbox microVM, so the loop can run in bypass-permissions mode without risking the host. The sandbox is the boundary, and the exit code is how the pipeline learns what happened inside it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-machine-verifiable-completion-beats-looks-done&quot;&gt;Why machine-verifiable completion beats “looks done”&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The reason a Ralph loop emits &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt; only after tests pass, and not when the agent feels finished, is that agents are unreliable narrators of their own work. An agent will cheerfully tell you a feature is done while the build is red. “Looks done” is a vibe, and a loop driven by vibes either stops too early on broken code or never stops at all.&lt;/p&gt;
&lt;p&gt;Machine-verifiable completion replaces the vibe with a gate. Before the agent is allowed to call a task done, it runs the project’s checks: Playwright for end to end tests, Vitest for unit tests, TypeScript for types, ESLint for linting, Prettier for formatting. The repo mantra is blunt: if you didn’t test it, it doesn’t work. A task is not done because the agent says so. It is done because the suite is green.&lt;/p&gt;
&lt;p&gt;This is why the promise is trustworthy. &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt; is downstream of a passing verification stack, not upstream of it. The agent cannot emit a real completion signal for code that fails its own checks, because the gate sits between “I wrote it” and “I am done”. The deeper version of this argument, including how tests and screenshots feed an agent the signal it needs to self-correct, is in &lt;a href=&quot;https://ralphloop.sh/blog/verification-loops-for-ai-agents/&quot;&gt;verification loops for AI agents&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Tie this back to the tags. &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt; is verified work. &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; is honest failure with a reason. &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; is honest uncertainty with a question. None of the three is a guess. That is the whole design goal: every way the loop can stop is a signal you can act on, not a state you have to interpret.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-max-iterations-interacts-with-the-promise&quot;&gt;How max iterations interacts with the promise&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The promise and the iteration cap are two independent stop conditions, and you want both. The promise stops the loop when the agent reaches a logical endpoint. The cap stops the loop when it has run long enough regardless of what the agent thinks.&lt;/p&gt;
&lt;p&gt;You set the cap with &lt;code dir=&quot;auto&quot;&gt;-n&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;--max-iterations&lt;/code&gt;. The default is ten:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--max-iterations&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;--once&lt;/code&gt; is the special case of a cap of one. It runs a single iteration and stops, which is how you smoke test a setup before turning it loose.&lt;/p&gt;
&lt;p&gt;Internally the loop is a counted loop, roughly &lt;code dir=&quot;auto&quot;&gt;for i in 1..N&lt;/code&gt;. On each pass it runs the agent, then checks for &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; in that order. If any tag fires, it exits with the matching code immediately, before the cap is relevant. If no tag fires, the loop checks whether &lt;code dir=&quot;auto&quot;&gt;i&lt;/code&gt; has reached &lt;code dir=&quot;auto&quot;&gt;N&lt;/code&gt;. If it has, the loop falls out the bottom and exits &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;. If not, it increments and runs a fresh agent.&lt;/p&gt;
&lt;p&gt;So the two conditions race, and the promise almost always wins on a healthy run. The cap is the backstop for the cases where it does not: an agent stuck repeating a task without finishing it, a task list with a subtle dependency the agent keeps tripping over, a spec that is too vague to ever satisfy. Without a cap, those situations become a money fire that runs until you notice. With a cap, the worst case is a bounded, inspectable run that exits &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; and waits for you.&lt;/p&gt;
&lt;p&gt;A practical pattern is to set the cap above your honest estimate of the task count, then read the exit code. Exit &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; means the agent finished inside the budget. Exit &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; means it did not, which is your cue to inspect the log and figure out whether the loop is making progress slowly or thrashing in place. Thrashing against the cap is one of the classic &lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-failure-modes/&quot;&gt;Ralph loop failure modes&lt;/a&gt;, and the cap is precisely the guardrail that keeps it from running up an unbounded bill.&lt;/p&gt;
&lt;p&gt;One more interaction worth naming. &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; short-circuit the cap entirely. If the agent hits a blocker on iteration three of a fifty iteration budget, the loop stops at three with code &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt;. It does not waste the remaining forty seven iterations re-discovering the same blocker, because a fresh-context agent would just hit the same wall. Stopping early and telling you the reason is the correct behavior.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;putting-it-together&quot;&gt;Putting it together&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The completion promise is a small mechanism with a large payoff. Three tags the agent emits, four primary exit codes the script returns, and a strict priority order between them. That is enough to make a loop both safe to leave running and easy to wire into automation.&lt;/p&gt;
&lt;p&gt;The flow, end to end:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The agent works one verified task and prints a status tag.&lt;/li&gt;
&lt;li&gt;The loop reads the tag and branches: &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt; exits &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; exits &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; exits &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If no tag fires and budget remains, a fresh agent runs again.&lt;/li&gt;
&lt;li&gt;If the budget runs out first, the loop exits &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Your script or CI reads the exit code and decides what happens next.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The promise is what stops the loop on a signal. Verification is what makes the &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt; signal honest. The iteration cap is what stops the loop when no signal comes. Use all three together and you get an agent you can run overnight and trust the exit code in the morning.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What is a completion promise in a Ralph loop?&lt;/h3&gt; &lt;p&gt;It is a machine-readable status tag the agent prints to its output, in the format &amp;#x3C;promise&gt;TYPE:content&amp;#x3C;/promise&gt;. The loop scans for it after every iteration and uses it to decide whether to continue, stop, or hand control back to you. The three control-flow tags are COMPLETE, BLOCKED, and DECIDE.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What are the Ralph loop exit codes?&lt;/h3&gt; &lt;p&gt;There are six. 0 means COMPLETE, all tasks finished and verified. 1 means MAX_ITERATIONS, the loop hit its iteration cap with work pending. 2 means BLOCKED, the agent needs human help. 3 means DECIDE, the agent needs a human decision. 4 means a Docker Sandbox error, and 5 means an authentication error. They are defined in scripts/lib/constants.sh.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Is exit code 1 a failure?&lt;/h3&gt; &lt;p&gt;No. Exit code 1 means the loop reached its iteration cap while tasks were still pending, which is the safety cap working as designed. You read the log, decide whether to add more iterations, and run the loop again. It is not an error, just an unfinished run.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does the loop avoid stopping on broken code?&lt;/h3&gt; &lt;p&gt;The agent is only allowed to emit COMPLETE after the verification stack passes: Playwright, Vitest, TypeScript, ESLint, and Prettier. Completion is downstream of a green test suite, not upstream of it, so the agent cannot signal done on code that fails its own checks.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What is the difference between BLOCKED and DECIDE?&lt;/h3&gt; &lt;p&gt;BLOCKED means the agent cannot continue without external help, like a missing credential or a failing service it does not control. DECIDE means the agent reached a fork it can technically pass but wants your input on, like choosing between two valid architectures. BLOCKED exits with code 2 and DECIDE exits with code 3.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>ralph-technique</category></item><item><title>Observability for Autonomous Coding Agents: Logs, History, and Live Output</title><link>https://ralphloop.sh/blog/observability-for-ai-coding-agents/</link><guid isPermaLink="true">https://ralphloop.sh/blog/observability-for-ai-coding-agents/</guid><description>An autonomous agent is only as trustworthy as what it shows you. Here are the live stream, per-iteration history, progress log, screenshots, and timing metrics that make a Ralph loop auditable.

</description><pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You cannot trust what you cannot see. An autonomous coding agent that runs for hours while you sleep is a black box unless you instrument it, so the rule for AI agent observability is simple: make every iteration leave a trail you can read after the fact and a live signal you can read during the run. Ralph does both. It streams a parsed preview of what the agent is doing right now, classifies each line into a named step, writes a clean log per iteration to disk, records a running progress file, captures screenshots per task, and commits after every task so the git log doubles as an audit trail.&lt;/p&gt;
&lt;p&gt;This is the observability piece of the larger guide to &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-coding-agent-overnight/&quot;&gt;running an AI coding agent overnight&lt;/a&gt;. Long runs fail quietly when you have no visibility into them, so the surfaces below are what let you walk away from a loop and still know exactly what happened when you walk back.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-observability-is-the-difference-between-trust-and-hope&quot;&gt;Why observability is the difference between trust and hope&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A single prompt finishes in seconds and you read the diff. A loop of 50 iterations runs for an hour or more, edits dozens of files, runs tests, and commits along the way. If the only thing you have at the end is a final message, you are hoping the agent did the right thing 50 times in a row. Hope is not a review process.&lt;/p&gt;
&lt;p&gt;Observability replaces hope with evidence. Every claim the agent makes (“tests pass”, “task done”) should have a file you can open to verify it. Every minute the loop spends should map to a step you can name. Every commit should be small enough to read. When those three things are true, you can audit an overnight run in the time it takes to drink a coffee, and you can catch a loop that has gone sideways before it burns another twenty iterations.&lt;/p&gt;
&lt;p&gt;The repo mantra applies here too: if you didn’t test it, it doesn’t work. The corollary for observability is that if you cannot see it, you cannot trust it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-an-autonomous-coding-agent-should-expose&quot;&gt;What an autonomous coding agent should expose&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph wraps your chosen agent (&lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, or &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt;) and turns its raw output stream into a set of observability surfaces. You start a run the usual way:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;From that point on, five surfaces are live or being written. Here is each one and where it lives.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;live-stream-preview-and-step-detection&quot;&gt;Live stream preview and step detection&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;While the agent works, Ralph reads its &lt;code dir=&quot;auto&quot;&gt;stream-json&lt;/code&gt; output line by line, parses out the text and tool calls, and shows two things under a spinner: the current step name and a dimmed rolling preview of the latest line. You are not watching a frozen spinner wondering if the process hung. You are watching a parsed feed of what the agent is touching right now.&lt;/p&gt;
&lt;p&gt;The step name comes from a classifier that maps output patterns to one of fourteen named steps:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Thinking, Planning, Reading code, Web research, Implementing,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Debugging, Writing tests, Testing, Linting, Typechecking,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Installing, Verifying, Waiting, Committing&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A line that calls a &lt;code dir=&quot;auto&quot;&gt;Write&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;Edit&lt;/code&gt; tool with a file path reads as &lt;code dir=&quot;auto&quot;&gt;Implementing&lt;/code&gt;. A &lt;code dir=&quot;auto&quot;&gt;vitest&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;playwright&lt;/code&gt; invocation reads as &lt;code dir=&quot;auto&quot;&gt;Testing&lt;/code&gt;. A &lt;code dir=&quot;auto&quot;&gt;git commit&lt;/code&gt; reads as &lt;code dir=&quot;auto&quot;&gt;Committing&lt;/code&gt;. An &lt;code dir=&quot;auto&quot;&gt;eslint&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;prettier&lt;/code&gt; run reads as &lt;code dir=&quot;auto&quot;&gt;Linting&lt;/code&gt;. The point is not perfect accuracy on every line. The point is that at a glance you know whether the agent is reading code, writing it, or running tests, without parsing raw JSON yourself.&lt;/p&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;Waiting&lt;/code&gt; step is the one to watch. It fires on patterns like a question prompt or “blocked on”, which on an unattended loop usually means the agent is stuck asking for input that nobody is there to give.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;per-iteration-history-in-agenthistory&quot;&gt;Per-iteration history in .agent/history/&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Every iteration writes its full output, with the ANSI color codes stripped out, to a timestamped file:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;.agent/history/ITERATION-&amp;#x3C;session&gt;-&amp;#x3C;n&gt;.txt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The session id is a &lt;code dir=&quot;auto&quot;&gt;YYYYMMDD-HHMMSS&lt;/code&gt; stamp taken when the run starts, so a fresh run never overwrites the history of an earlier one. Iteration 7 of a run that started at 02:15:00 lands in &lt;code dir=&quot;auto&quot;&gt;ITERATION-20260314-021500-7.txt&lt;/code&gt;. To replay what the agent thought and did on a specific iteration, open that file. To scan the tail of the latest one while a run is going, point &lt;code dir=&quot;auto&quot;&gt;tail&lt;/code&gt; at the directory:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;tail&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.agent/history/ITERATION-&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;.txt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is the most underrated surface. The live preview is ephemeral, but the history file is the full, clean transcript of a single fresh-context iteration. When a task goes wrong three iterations back, this is where you find out why.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-progress-log-agentlogslogmd&quot;&gt;The progress log: .agent/logs/LOG.md&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt; is the human-readable run journal. Ralph creates it on first run, and the agent appends an entry per task with the date, a brief summary, and the path to the screenshot it captured, newest entry at the top. It is the high-level story of the run, where the history files are the line-by-line detail.&lt;/p&gt;
&lt;p&gt;Read it top to bottom in the morning and you get the narrative: what shipped, in what order, and where to look for the visual proof of each step.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# the story of the run, newest first&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;head&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;40&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.agent/logs/LOG.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;screenshots-per-task&quot;&gt;Screenshots per task&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Step four of every iteration is “complete the task, take a screenshot, update status, and commit.” The agent saves UI screenshots to &lt;code dir=&quot;auto&quot;&gt;.agent/screenshots/TASK-&amp;#x3C;id&gt;-&amp;#x3C;index&gt;.png&lt;/code&gt; and references that path in the log entry. For anything with a UI, this is the difference between trusting a green test and seeing the rendered result.&lt;/p&gt;
&lt;p&gt;Screenshots also feed back into the loop. When the agent debugs a regression, it uses earlier screenshots as a reference for what the UI looked like before the change. That makes the screenshot folder both an audit artifact for you and a memory aid for the next fresh-context iteration.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;timing-metrics-per-step-and-per-iteration&quot;&gt;Timing metrics per step and per iteration&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Ralph times each iteration and each step inside it. After every iteration it prints the iteration duration, the delta against the previous iteration (green when faster, red when slower, in a stock-ticker style), a running average, and the total elapsed time. It also breaks the iteration down by step, sorted by time spent, so you can see that an iteration spent most of its minutes in &lt;code dir=&quot;auto&quot;&gt;Testing&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;Debugging&lt;/code&gt; rather than &lt;code dir=&quot;auto&quot;&gt;Implementing&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At the end of the run it prints session totals across all iterations. Timing is a cheap, powerful signal: iterations that keep getting longer, or that spend a growing share of time in &lt;code dir=&quot;auto&quot;&gt;Debugging&lt;/code&gt;, are a thrashing loop telling on itself before it blows your budget. Watching that trend is the core of &lt;a href=&quot;https://ralphloop.sh/blog/ai-agent-cost-control/&quot;&gt;cost control for autonomous AI coding agents&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here is how the surfaces sit around a single iteration of the loop.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
    Agent[&quot;Agent runs (fresh context)&quot;] --&gt; Stream[&quot;stream-json output&quot;]
    Stream --&gt; Live[&quot;Live: spinner step + rolling preview&quot;]
    Stream --&gt; Detect[&quot;detect_step: Thinking / Implementing / Testing ...&quot;]
    Stream --&gt; Hist[&quot;.agent/history/ITERATION-(session)-(n).txt&quot;]
    Agent --&gt; Shots[&quot;.agent/screenshots/TASK-(id)-(index).png&quot;]
    Agent --&gt; Log[&quot;.agent/logs/LOG.md (newest first)&quot;]
    Agent --&gt; Commit[&quot;git commit per task&quot;]
    Detect --&gt; Timing[&quot;Per-step and per-iteration timing&quot;]
    Agent --&gt; Promise{&quot;Promise tag?&quot;}
    Promise --&gt;|&quot;none&quot;| Next[&quot;Next iteration&quot;]
    Promise --&gt;|&quot;BLOCKED or DECIDE&quot;| Notify[&quot;Desktop notification + sound&quot;]
    Promise --&gt;|&quot;COMPLETE&quot;| Exit[&quot;exit 0&quot;]&lt;/pre&gt;
&lt;div&gt;&lt;h2 id=&quot;git-history-as-the-audit-trail&quot;&gt;Git history as the audit trail&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The strongest observability surface is one you already know how to read. Ralph follows one rule: one task per invocation. The agent completes exactly one task, commits, and stops, then the next iteration starts fresh. It never batches several tasks into a single commit. That discipline, covered in depth in the pillar on &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-coding-agent-overnight/&quot;&gt;running an agent overnight&lt;/a&gt;, means the git log is a clean, chronological record of the whole run, one commit per finished task.&lt;/p&gt;
&lt;p&gt;So your morning review is a normal code review:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# what landed overnight, one line per task&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--oneline&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--since=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;12 hours ago&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# the full diff for a single suspicious task&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;show&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;commit&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# everything since you walked away&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;diff&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HEAD@{12.hours.ago}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Small commits keep each diff reviewable, which is the whole reason the one-task rule exists. A loop that commits 40 tiny, well-scoped changes is auditable. A loop that drops one giant commit at the end is not. Git history is also the agent’s memory layer: each fresh-context iteration reads recent commits, the task list, and the progress log to reorient, rather than carrying a bloated transcript forward.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;notifications-when-the-agent-needs-you&quot;&gt;Notifications when the agent needs you&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Most of a loop runs without you. The two moments you actually need to know about are when the agent gets stuck or hits a fork it cannot resolve alone. Ralph surfaces both through promise tags the agent emits in its final message:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; means every task is done. The loop exits with code &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; means the agent needs human help. The loop exits with code &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt; means it needs a decision you have to make. The loop exits with code &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hitting the iteration cap without completing exits with code &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;. The full mechanics of how these signals stop a run, and why a loop should stop on an explicit promise rather than a vibe, are in the guide to &lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-completion-promise/&quot;&gt;completion promises and exit codes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For observability, the important part is that &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; are not silent. When either fires, Ralph plays a notification sound and sends a desktop notification (via &lt;code dir=&quot;auto&quot;&gt;osascript&lt;/code&gt; on macOS, &lt;code dir=&quot;auto&quot;&gt;notify-send&lt;/code&gt; on Linux, or PowerShell on Windows) with the reason or the question. You can leave a loop running in another workspace and trust that your machine will get your attention the moment a human is actually needed, instead of finding a stalled run an hour later. When you do get pulled in, you often do not need to stop the loop at all. You can redirect it with a &lt;a href=&quot;https://ralphloop.sh/blog/steering-a-running-ai-agent/&quot;&gt;STEERING.md file that injects work into a running agent&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-to-read-the-signals-and-catch-a-stuck-loop-early&quot;&gt;How to read the signals and catch a stuck loop early&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Visibility only helps if you know which patterns mean trouble. Here is how the surfaces combine into early warnings.&lt;/p&gt;
&lt;p&gt;Iteration time climbing without new commits. If durations grow but &lt;code dir=&quot;auto&quot;&gt;git log&lt;/code&gt; shows no new tasks landing, the agent is spinning. The timing line and the commit log together catch this faster than either alone.&lt;/p&gt;
&lt;p&gt;A step breakdown dominated by &lt;code dir=&quot;auto&quot;&gt;Debugging&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;Testing&lt;/code&gt;. A healthy iteration spends real time in &lt;code dir=&quot;auto&quot;&gt;Implementing&lt;/code&gt;. When the per-step breakdown is mostly &lt;code dir=&quot;auto&quot;&gt;Debugging&lt;/code&gt; across several iterations, the agent is fighting the same failure. Open the latest &lt;code dir=&quot;auto&quot;&gt;.agent/history/&lt;/code&gt; file to see which one.&lt;/p&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;Waiting&lt;/code&gt; step on an unattended run. &lt;code dir=&quot;auto&quot;&gt;Waiting&lt;/code&gt; means the agent is asking for input. With nobody at the keyboard, that iteration will not progress. This is a prompt problem: the task lacks a clear completion criterion, so the agent does not know it is allowed to finish.&lt;/p&gt;
&lt;p&gt;A &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; notification. This is the agent doing the right thing. It hit a wall it cannot or should not pass alone, emitted the promise, and stopped with a non-zero exit code. Read the reason in the notification and in the on-screen message, fix or decide, then run &lt;code dir=&quot;auto&quot;&gt;./ralph.sh&lt;/code&gt; again to resume.&lt;/p&gt;
&lt;p&gt;A flat &lt;code dir=&quot;auto&quot;&gt;LOG.md&lt;/code&gt;. If the progress log stops getting new entries while the run is still going, the agent is not completing tasks. Cross-check against the live step and the history file to see where it is stuck.&lt;/p&gt;
&lt;p&gt;When the signals point somewhere you need to inspect directly, get inside the sandbox. Ralph runs each agent in an isolated Docker Sandbox, and you can open a shell in the running box to re-run a failing command, read the logs from inside, or check the working tree:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;#x3C;sandbox-name&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Print the exact sandbox name for your project with &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --print-name&lt;/code&gt;. Between the live stream, the per-iteration history, the progress log, the screenshots, the timing metrics, the git log, and the notifications, an autonomous run stops being a black box. You get a system you can audit while it runs and after it finishes, which is the only honest basis for letting an agent code unattended.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What does observability mean for an autonomous coding agent?&lt;/h3&gt; &lt;p&gt;It means every iteration leaves a trail you can read and a live signal you can watch. For a Ralph loop that is a parsed live stream with step detection, a clean per-iteration transcript in .agent/history/, a running progress log in .agent/logs/LOG.md, screenshots per task in .agent/screenshots/, timing metrics per step and per iteration, and one git commit per task. Together they let you audit a run while it happens and after it finishes.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Where does Ralph store per-iteration logs and history?&lt;/h3&gt; &lt;p&gt;Each iteration writes its full output with ANSI codes stripped to .agent/history/ITERATION-&amp;#x3C;session&gt;-&amp;#x3C;n&gt;.txt, where session is a YYYYMMDD-HHMMSS stamp taken at the start of the run so new runs never overwrite old history. The higher-level run journal lives in .agent/logs/LOG.md, which the agent appends to per task with a date, a summary, and a screenshot path, newest entry first.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I watch what an AI agent is doing in real time?&lt;/h3&gt; &lt;p&gt;Ralph reads the agent stream-json output line by line and shows a spinner with the current step name plus a dimmed rolling preview of the latest line. The step comes from a classifier that maps output to fourteen named steps such as Thinking, Implementing, Testing, and Committing. To follow the full transcript live, run tail -f on the .agent/history/ file for the current iteration.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I know when an autonomous agent needs me?&lt;/h3&gt; &lt;p&gt;The agent emits a promise tag in its final message. BLOCKED means it needs help and exits with code 2, DECIDE means it needs a decision and exits with code 3, COMPLETE means it finished and exits with code 0, and hitting the iteration cap exits with code 1. When BLOCKED or DECIDE fires, Ralph plays a sound and sends a desktop notification with the reason, so you can leave the loop running and still be pulled in only when a human is required.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I catch a stuck or thrashing agent loop early?&lt;/h3&gt; &lt;p&gt;Watch three things together. Iteration time climbing with no new commits in git log means the agent is spinning. A per-step breakdown dominated by Debugging across iterations means it is fighting the same failure. The Waiting step on an unattended run means the task lacks a clear completion criterion. When any of these show up, open the latest .agent/history/ file or shell into the sandbox with sbx exec to see exactly where it is stuck.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>autonomous-agents</category></item><item><title>Running the Cursor CLI Agent in a Loop</title><link>https://ralphloop.sh/blog/run-cursor-cli-agent-loop/</link><guid isPermaLink="true">https://ralphloop.sh/blog/run-cursor-cli-agent-loop/</guid><description>Point Ralph at the headless cursor-agent with one flag, log in once inside the sandbox, pass a model after the separator, and review a finished diff in the morning.

</description><pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;To run the Cursor CLI agent in an autonomous loop, point Ralph at it with one flag: &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --agent cursor&lt;/code&gt;. Ralph wraps the headless &lt;code dir=&quot;auto&quot;&gt;cursor-agent&lt;/code&gt; in print mode, starts it with a fresh context window every iteration, and keeps re-running it against your task list until the work is done or you hit the iteration cap. Log in once inside the sandbox, pass Cursor’s own flags after a &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; separator, and review a finished diff in the morning.&lt;/p&gt;
&lt;p&gt;This is the Cursor-specific walkthrough in the larger guide to &lt;a href=&quot;https://ralphloop.sh/blog/agentic-coding-clis/&quot;&gt;agentic coding CLIs&lt;/a&gt;. The loop mechanics are identical to &lt;a href=&quot;https://ralphloop.sh/blog/run-claude-code-in-a-loop/&quot;&gt;running Claude Code in a loop&lt;/a&gt;; only the agent binary and its flags change.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;run-the-cursor-cli-agent-in-a-loop-with-one-flag&quot;&gt;Run the Cursor CLI agent in a loop with one flag&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph is a Bash script you point at a project. The default agent is Claude, so you switch to Cursor explicitly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That runs 10 iterations, the default. Tune the count when you want a longer unattended run or a single dry run:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 50 iterations&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# exactly one iteration (good for a smoke test)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# explicit cap&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--max-iterations&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The short form &lt;code dir=&quot;auto&quot;&gt;-a&lt;/code&gt; works too: &lt;code dir=&quot;auto&quot;&gt;./ralph.sh -a cursor -n 5&lt;/code&gt;. Supported agents are &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt; (default), &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt;, so the same harness drives any of them.&lt;/p&gt;
&lt;p&gt;Under the hood, Ralph builds a &lt;code dir=&quot;auto&quot;&gt;cursor-agent&lt;/code&gt; command and runs it inside a Docker Sandbox. The expansion for &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --agent cursor&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-cursor-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$PROMPT_CONTENT&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;-p&lt;/code&gt; flag (long form &lt;code dir=&quot;auto&quot;&gt;--print&lt;/code&gt;) is Cursor’s headless mode. It prints the agent’s responses to the console for scripts and non-interactive use, and the &lt;a href=&quot;https://cursor.com/docs/cli/reference/parameters&quot;&gt;Cursor CLI parameter reference&lt;/a&gt; is clear that print mode still has access to all tools, including write and shell. That is exactly what a loop needs: an agent that can edit files and run commands without a person in the chair. Print mode reads the prompt, does the work, prints a final message, and exits, and that exit is what lets Ralph treat each iteration as a discrete unit.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;set-up-and-log-in-to-cursor-inside-the-sandbox&quot;&gt;Set up and log in to Cursor inside the sandbox&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Cursor runs inside an isolated Docker Sandbox, not on your host, so it needs credentials in that environment. Start by dropping Ralph into your project:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;npx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@pageai/ralph-loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This adds the &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; script and the &lt;code dir=&quot;auto&quot;&gt;.agent/&lt;/code&gt; directory that holds the prompt, the task list, and the logs. Then authenticate once with the login action:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This prints the login command for every supported agent, highlights the one for Cursor, and then drops you into the sandbox shell. Inside, you authenticate Cursor once. Run &lt;code dir=&quot;auto&quot;&gt;cursor-agent login&lt;/code&gt; and follow the prompt, or provide a key through the &lt;code dir=&quot;auto&quot;&gt;CURSOR_API_KEY&lt;/code&gt; environment variable. Confirm the session with &lt;code dir=&quot;auto&quot;&gt;cursor-agent status&lt;/code&gt;. The credential persists in that named sandbox, so later runs attach to the same box and start already logged in.&lt;/p&gt;
&lt;p&gt;Each agent gets its own deterministic sandbox name, derived from the agent slug, the project directory, and a hash of the absolute path:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;project-dir&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For Cursor that is &lt;code dir=&quot;auto&quot;&gt;ralph-cursor-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/code&gt;. Print the exact name for your project without starting a run:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Per-agent names matter because they keep state separate. Your Cursor sandbox and your Claude sandbox never share credentials, history, or installed tools, so you can run both against the same repo without one clobbering the other. If Cursor is not authenticated when the loop starts, Ralph detects the auth failure, stops, and tells you to run &lt;code dir=&quot;auto&quot;&gt;./ralph.sh --login --agent cursor&lt;/code&gt;. No silent thrashing on a box that can never make progress.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;pass-a-model-and-flags-after-the--separator&quot;&gt;Pass a model and flags after the — separator&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Everything to the right of Ralph’s own &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; separator is forwarded straight to the agent. For Cursor, Ralph inserts those arguments right after &lt;code dir=&quot;auto&quot;&gt;-p&lt;/code&gt;, before the prompt. So this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;expands to:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-cursor-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$PROMPT_CONTENT&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;--model&lt;/code&gt; flag picks the model for the run. Do not guess at model names: run &lt;code dir=&quot;auto&quot;&gt;cursor-agent models&lt;/code&gt; (or pass &lt;code dir=&quot;auto&quot;&gt;--list-models&lt;/code&gt;) inside the sandbox to print the exact identifiers Cursor accepts, then pass one of those. The separator works for any valid &lt;code dir=&quot;auto&quot;&gt;cursor-agent&lt;/code&gt; flag, not just the model. A few you will reach for in a loop:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# pick a model (list them first with cursor-agent models)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# never pause to approve a command (alias: --yolo)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--force&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# emit newline-delimited JSON events instead of plain text&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--output-format&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;stream-json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The rule to remember: everything left of &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; configures Ralph (agent, iteration count, login). Everything right of &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; configures the agent. Keep them on the correct side and the loop behaves.&lt;/p&gt;
&lt;p&gt;Two of those flags deserve a note. &lt;code dir=&quot;auto&quot;&gt;-f&lt;/code&gt; (long form &lt;code dir=&quot;auto&quot;&gt;--force&lt;/code&gt;, alias &lt;code dir=&quot;auto&quot;&gt;--yolo&lt;/code&gt;) tells Cursor to allow commands unless a rule explicitly denies them, which is what keeps an unattended run from stalling on an approval prompt nobody is there to answer. And &lt;code dir=&quot;auto&quot;&gt;--output-format&lt;/code&gt; (which only works alongside &lt;code dir=&quot;auto&quot;&gt;--print&lt;/code&gt;) switches the stream from &lt;code dir=&quot;auto&quot;&gt;text&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;json&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;stream-json&lt;/code&gt;, useful when a downstream CI step parses events with &lt;code dir=&quot;auto&quot;&gt;jq&lt;/code&gt;. Ralph already records each iteration’s cleaned output to &lt;code dir=&quot;auto&quot;&gt;.agent/history/&lt;/code&gt; and the running log to &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt;, so you get a per-iteration trail regardless of which format you pick.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-happens-each-iteration&quot;&gt;What happens each iteration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Ralph’s loop is the Bash loop Geoffrey Huntley described in the &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;original Ralph writeup&lt;/a&gt;. Each pass is mechanical and identical:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the highest-priority incomplete task in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Work the steps in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run tests, linting, and type checking.&lt;/li&gt;
&lt;li&gt;Complete the task, take a screenshot, update the task status, and commit.&lt;/li&gt;
&lt;li&gt;Repeat until all tasks pass or the iteration cap is reached.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The critical part is that each iteration spawns a fresh &lt;code dir=&quot;auto&quot;&gt;cursor-agent -p&lt;/code&gt; with a clean context window. The agent does not carry a bloated, hours-long transcript from one task to the next. It reads the current state from disk, does one task, and exits. This is why the loop deliberately does not use Cursor’s own &lt;code dir=&quot;auto&quot;&gt;--resume&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;--continue&lt;/code&gt; session flags: continuity is a liability here, not a feature.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
    Start([&quot;./ralph.sh --agent cursor&quot;]) --&gt; Pick[&quot;Pick top task from .agent/tasks.json&quot;]
    Pick --&gt; Spawn[&quot;sbx run cursor . -- -p (fresh context)&quot;]
    Spawn --&gt; Work[&quot;cursor-agent reads state, edits files, runs commands&quot;]
    Work --&gt; Verify[&quot;Run tests, lint, type check, screenshot&quot;]
    Verify --&gt; Commit[&quot;Commit and update task status&quot;]
    Commit --&gt; Check{&quot;Promise tag emitted?&quot;}
    Check --&gt;|&quot;none&quot;| Pick
    Check --&gt;|&quot;COMPLETE&quot;| Done([&quot;exit 0, all tasks done&quot;])
    Check --&gt;|&quot;BLOCKED or DECIDE&quot;| Stop([&quot;exit 2 or 3, wants a human&quot;])&lt;/pre&gt;
&lt;p&gt;The filesystem and git history are the memory layer. Progress lives in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt;, per-task spec files, and the git log, not in a chat transcript. That separation of thinking (ephemeral, per iteration) from state (durable, on disk) is what keeps a fresh-context agent oriented across dozens of iterations. The deeper version of this idea is in the guide to &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-coding-agent-overnight/&quot;&gt;running an AI coding agent overnight&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A loop also needs a stop condition that is a signal, not a vibe. Cursor emits a semantic promise tag in its final message, and Ralph reads it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; means every task is finished.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; means the agent needs human help.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt; means it needs a decision you have to make.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those map to exit codes: &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; for COMPLETE, &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; for hitting MAX_ITERATIONS, &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt; for BLOCKED, and &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt; for DECIDE. Wire those into a wrapper script or a CI step and you get clean branching: ship on &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;, page yourself on &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt;, extend the cap on &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One rule keeps the whole thing reliable: one task per invocation. Cursor completes exactly one task, commits, and stops. It never batches several tasks into a single iteration, which is what keeps each commit small, each diff reviewable, and each context window focused.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;isolate-cursor-in-a-docker-sandbox-and-review-the-diff-in-the-morning&quot;&gt;Isolate Cursor in a Docker Sandbox and review the diff in the morning&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;An autonomous agent that can write files and run shell commands is exactly as dangerous as the permissions it inherits. Run &lt;code dir=&quot;auto&quot;&gt;cursor-agent --force&lt;/code&gt; on your laptop and it can touch your SSH keys, your environment variables, and anything else your user can reach. The fix is not to make the agent more timid. The fix is to change the blast radius.&lt;/p&gt;
&lt;p&gt;Ralph runs each agent inside a Docker Sandbox, an isolated microVM managed by the &lt;code dir=&quot;auto&quot;&gt;sbx&lt;/code&gt; CLI. Inside that boundary, Cursor runs in its &lt;code dir=&quot;auto&quot;&gt;--force&lt;/code&gt; (YOLO) mode without you previewing every command, because the sandbox is the boundary the agent cannot cross. The microVM has its own kernel, an isolated filesystem, and a network that is deny-by-default.&lt;/p&gt;
&lt;p&gt;Cursor also ships its own internal sandbox, toggled with &lt;code dir=&quot;auto&quot;&gt;--sandbox enabled&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;--sandbox disabled&lt;/code&gt;. Running a second sandbox inside the microVM buys you nothing, because the microVM already contains the agent, and it can introduce nested-isolation friction. So a practical pairing for a loop is to let the microVM do the isolating and let Cursor focus on the coding:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--force&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--sandbox&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;disabled&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;When the agent needs a package, the deny-by-default network blocks it until you allow the domain:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;policy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;allow&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-cursor-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;registry.npmjs.org&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That is a feature. The agent can install what the task needs without a path to exfiltrate your source or reach arbitrary hosts. The full argument, including how the microVM compares to a hand-rolled container, lives in &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-agents-in-docker-sandboxes-safely/&quot;&gt;how to run AI coding agents in Docker sandboxes safely&lt;/a&gt;, and the &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/&quot;&gt;Docker Sandboxes documentation&lt;/a&gt; covers the policy model in detail.&lt;/p&gt;
&lt;p&gt;This is the payoff of looping overnight. Because every task is committed separately, the morning review is a git review, not an archaeology dig:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--oneline&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;diff&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main...HEAD&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;You read the commits in order, eyeball the screenshots the agent captured, and accept or revert. The work arrived as small, verified, individually committed units, so a single bad task is one revert, not a tangled mess you have to unwind by hand.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;verify-every-iteration-with-the-test-stack&quot;&gt;Verify every iteration with the test stack&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A loop is only as good as its feedback. If Cursor cannot tell whether its change worked, it will happily mark a broken task done and move on. The repo mantra is blunt: if you didn’t test it, it doesn’t work.&lt;/p&gt;
&lt;p&gt;Ralph assumes a verification stack and runs it inside step three of every iteration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vitest for unit tests.&lt;/li&gt;
&lt;li&gt;Playwright for end-to-end tests.&lt;/li&gt;
&lt;li&gt;TypeScript for type checking.&lt;/li&gt;
&lt;li&gt;ESLint for linting.&lt;/li&gt;
&lt;li&gt;Prettier for formatting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most projects wire these into npm scripts the agent calls during each iteration:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;test&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;vitest run&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;test:e2e&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;playwright test&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;typecheck&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;tsc --noEmit&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;lint&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;eslint .&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;format&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;prettier --check .&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Because print mode has full tool access, Cursor can run those commands itself, read the failures, and fix them before it commits. A failed check sends the agent back to fix the work rather than forward to the next task. Screenshots add a second channel: for UI work, a passing suite is necessary but not sufficient, so the agent captures the rendered state as visual evidence you can review later.&lt;/p&gt;
&lt;p&gt;Because every iteration starts fresh, verification is also how the next iteration learns what the last one did. The agent does not remember the previous run. It reads the test results, the updated task status, and the new commits, then decides what is next. That feedback loop is the whole point, and it generalizes across agents. The same verification discipline applies when you &lt;a href=&quot;https://ralphloop.sh/blog/run-gemini-cli-in-a-loop/&quot;&gt;run the Gemini CLI in a loop&lt;/a&gt; or any other CLI in this family.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;inspect-and-debug-the-cursor-sandbox&quot;&gt;Inspect and debug the Cursor sandbox&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When a run stalls or a task keeps failing, get inside the box. The sandbox is a normal container you can poke at. List what exists:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Open a shell in the Cursor sandbox and look around:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-cursor-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;From there you can check the working tree, re-run a failing test by hand, confirm auth with &lt;code dir=&quot;auto&quot;&gt;cursor-agent status&lt;/code&gt;, or read &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt; and the per-iteration logs in &lt;code dir=&quot;auto&quot;&gt;.agent/history/&lt;/code&gt;. Reattach to a sandbox session with:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-cursor-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Most stalls trace back to one of three things: Cursor was never authenticated inside the box, a network policy is blocking an install, or the prompt in &lt;code dir=&quot;auto&quot;&gt;.agent/PROMPT.md&lt;/code&gt; lacks a clear completion criterion. The sandbox shell shows you which one it is. If you need to redirect a running loop without killing it, edit &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt;. Ralph folds that critical work into the next iteration before resuming the normal task list. That is steering, not stopping, and it keeps momentum while you correct course.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;putting-it-together&quot;&gt;Putting it together&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A real Cursor loop, start to finish, is three commands:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 1. authenticate once (creates the sandbox, you log in inside it)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 2. confirm the sandbox name for network policies and debugging&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--print-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 3. run the loop with a model and force mode, inside the sandbox boundary&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cursor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--force&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--sandbox&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;disabled&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That is the Cursor CLI agent running unattended: fresh context per iteration, state on disk, write access granted because the microVM is the real boundary, and a hard stop on a completion promise. Define your tasks in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;, write a clear &lt;code dir=&quot;auto&quot;&gt;.agent/PROMPT.md&lt;/code&gt;, start the loop, and read the commits in the morning.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;How do I run the Cursor CLI agent in a loop?&lt;/h3&gt; &lt;p&gt;Use Ralph and pass the agent flag: ./ralph.sh --agent cursor. Ralph wraps the headless cursor-agent in print mode, runs it inside a Docker Sandbox, starts a fresh context window each iteration, and repeats until every task in .agent/tasks.json is done or the iteration cap is reached. The default is 10 iterations; raise it with -n 50.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I pass a model to the Cursor agent through Ralph?&lt;/h3&gt; &lt;p&gt;Put it after the -- separator. Anything to the right of -- is forwarded to cursor-agent, so ./ralph.sh --agent cursor -- --model auto runs cursor-agent in print mode with that model and the Ralph prompt. Run cursor-agent models or --list-models inside the sandbox to see the exact model identifiers Cursor accepts.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I log in to Cursor inside the sandbox?&lt;/h3&gt; &lt;p&gt;Run ./ralph.sh --login --agent cursor. It drops you into the sandbox shell where you run cursor-agent login, or you can set the CURSOR_API_KEY environment variable. The credential persists in that named sandbox, named ralph-cursor-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;, so future runs attach to the same box already logged in.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Is it safe to run the Cursor agent in --force or --yolo mode?&lt;/h3&gt; &lt;p&gt;It is unsafe on your laptop and reasonable inside a sandbox. Ralph runs cursor-agent in an isolated Docker Sandbox microVM with network denied by default, so force mode lets the agent run commands without pausing for approval while staying unable to touch your real files or exfiltrate data. The sandbox is the boundary, not the agent.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How does the loop know when to stop?&lt;/h3&gt; &lt;p&gt;Cursor emits a promise tag in its final message. COMPLETE stops the loop with exit code 0, BLOCKED exits with 2, and DECIDE exits with 3. Hitting the iteration cap without completing exits with 1. You branch on those exit codes in a wrapper script or CI.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>agentic-clis</category></item><item><title>How to Write the PROMPT.md File That Drives a Ralph Loop</title><link>https://ralphloop.sh/blog/ralph-loop-prompt-file/</link><guid isPermaLink="true">https://ralphloop.sh/blog/ralph-loop-prompt-file/</guid><description>PROMPT.md is the instruction sent to the agent on every iteration. Here is how to structure it so a fresh-context agent reorients from disk and ships one verified task at a time.

</description><pubDate>Fri, 06 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;PROMPT.md&lt;/code&gt; is the instruction the loop sends to the agent on every single iteration. It is not setup documentation and it is not a one-time kickoff message. It is the text a fresh agent reads at the top of each pass, with zero memory of the last one, so it has to make that agent reorient from disk and start working within seconds.&lt;/p&gt;
&lt;p&gt;Get it right and a stateless agent reads its task list, picks one task, verifies the result, and commits, over and over, until the work is done. Get it wrong and the loop drifts: the agent batches tasks, forgets where state lives, or never signals that it finished. This post shows exactly what to put in &lt;code dir=&quot;auto&quot;&gt;.agent/PROMPT.md&lt;/code&gt;, with an annotated example you can copy.&lt;/p&gt;
&lt;p&gt;If the loop itself is new to you, start with &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;what the Ralph technique is&lt;/a&gt; and come back. The pattern was popularized by Geoffrey Huntley, who framed &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;Ralph as a Bash loop&lt;/a&gt; that builds software while you sleep. The prompt file is the steering wheel of that loop.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-does-promptmd-matter-so-much-in-a-ralph-loop&quot;&gt;Why does PROMPT.md matter so much in a Ralph loop?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The defining property of a Ralph loop is that each iteration starts the agent with a clean context window. There is no carryover transcript. The agent that runs iteration 31 knows nothing about iterations 1 through 30 except what it can read from disk. This is deliberate. It avoids context rot, where a long session drifts and the agent loses the plot as old output piles up in the window. For the deeper version of this idea, see &lt;a href=&quot;https://ralphloop.sh/blog/context-engineering-for-long-running-agents/&quot;&gt;context engineering for long-running agents&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The trade for that clean window is memory. A fresh agent remembers nothing, so the filesystem and git history become the memory layer. &lt;code dir=&quot;auto&quot;&gt;PROMPT.md&lt;/code&gt; is the bridge between the two. It is the only text guaranteed to be in front of the agent at the start of every pass, and its whole job is to point an amnesiac agent at where its memory lives and tell it what to do next.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; re-sends the same &lt;code dir=&quot;auto&quot;&gt;PROMPT.md&lt;/code&gt; on each iteration. (Anthropic shipped an official Claude Code plugin that does this with a Stop Hook that re-injects the prompt. Our implementation is the hackable &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; script, which loops the agent directly.) Because the text is identical every pass, it has to be written for an agent that is reading it for the first time, every time. That single constraint drives everything else in this post.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-does-every-promptmd-need-to-include&quot;&gt;What does every PROMPT.md need to include?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A working prompt file does four jobs. Miss any one of them and the loop stalls in a predictable way.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;tell-the-agent-where-its-state-lives&quot;&gt;Tell the agent where its state lives&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A fresh agent cannot remember what it did, so the first thing the prompt must do is hand it a map of disk. Reference the files by path so the agent reads them before doing anything:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.agent/prd/SUMMARY.md&lt;/code&gt;: the condensed description of what is being built. This is the “why.”&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;: the task lookup table, the list of work with &lt;code dir=&quot;auto&quot;&gt;passes&lt;/code&gt; flags.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;: the full spec for a single task, including its steps and acceptance criteria.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt;: the running log of past iterations, newest at the top, so the agent can see what already happened.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt;: critical work injected mid-run that the agent handles before resuming the task list.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;.agent/STRUCTURE.md&lt;/code&gt;: the current directory layout, so the agent does not reinvent paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The agent reorients by reading these in order. The summary tells it the goal, the log tells it the recent past, the task table tells it what is left.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;enforce-one-task-per-iteration&quot;&gt;Enforce one task per iteration&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This is the rule that makes the whole thing reliable. The agent picks the highest-priority task with &lt;code dir=&quot;auto&quot;&gt;passes: false&lt;/code&gt;, works only that task, commits, and stops. It never batches. Batching is the most common way a loop goes off the rails, because a single commit that touches five tasks is impossible to verify and impossible to revert cleanly. One task per invocation keeps each step small enough to test and small enough to bisect later. The whole rule is worth its own read in &lt;a href=&quot;https://ralphloop.sh/blog/one-task-per-iteration/&quot;&gt;one task per iteration&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;define-how-the-agent-verifies-its-own-work&quot;&gt;Define how the agent verifies its own work&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A loop is only as good as its verification, because tests are what gate progress without you in the chair. The prompt has to spell out the stack the agent runs before it commits: Vitest for unit tests, Playwright for end to end, &lt;code dir=&quot;auto&quot;&gt;tsc&lt;/code&gt; for types, ESLint for lint, Prettier for format. For UI work, the prompt should require a Playwright smoke test and a saved screenshot so the agent has visual ground truth. The repo mantra is blunt: if you didn’t test it, it doesn’t work.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;tell-the-agent-how-to-signal-completion&quot;&gt;Tell the agent how to signal completion&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The loop stops on an explicit signal, not a vibe. The prompt defines the promise tags the agent emits so &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; knows what happened:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;TASK-{ID}:DONE&amp;#x3C;/promise&gt;   one task finished, stop this iteration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;         all tasks finished, exit the loop&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;   needs human help&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;  needs a decision&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Those tags map to exit codes, which is what makes the loop scriptable in CI or a cron job:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$?&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;# 0 COMPLETE, 1 MAX_ITERATIONS, 2 BLOCKED, 3 DECIDE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The per-task &lt;code dir=&quot;auto&quot;&gt;DONE&lt;/code&gt; tag is what ends a single iteration. The &lt;code dir=&quot;auto&quot;&gt;COMPLETE&lt;/code&gt; tag is what ends the entire run. Both belong in the prompt, and the loop logic depends on the agent emitting them exactly. The full mechanics live in &lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-completion-promise/&quot;&gt;completion promises and exit codes&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-concrete-annotated-promptmd-structure&quot;&gt;A concrete annotated PROMPT.md structure&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here is the shape of a real &lt;code dir=&quot;auto&quot;&gt;.agent/PROMPT.md&lt;/code&gt;, trimmed to the load-bearing parts. Notice how it front-loads the one-task rule, points at state with &lt;code dir=&quot;auto&quot;&gt;@&lt;/code&gt; file references, lays out a numbered task flow, then closes with hard rules and help tags.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&gt; ONE TASK PER INVOCATION. Complete one task from @.agent/tasks.json,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&gt; commit, output &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;promise&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;TASK-{ID}:DONE&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;promise&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;, and STOP.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Overview&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;You are implementing the project described in @.agent/prd/SUMMARY.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Required Setup&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Run &lt;/span&gt;&lt;span&gt;`npm run dev`&lt;/span&gt;&lt;span&gt; (as a background process) in the &lt;/span&gt;&lt;span&gt;`src`&lt;/span&gt;&lt;span&gt; directory.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Before Starting&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Check @.agent/STEERING.md for critical work. Handle it in sequence,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;remove when done, then proceed to tasks.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Task Flow&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;1.&lt;/span&gt;&lt;span&gt; Pick the highest-priority task with &lt;/span&gt;&lt;span&gt;`passes: false`&lt;/span&gt;&lt;span&gt; in tasks.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;2.&lt;/span&gt;&lt;span&gt; Read the full spec: .agent/tasks/TASK-{ID}.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;3.&lt;/span&gt;&lt;span&gt; Check existing structure in @.agent/STRUCTURE.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;4.&lt;/span&gt;&lt;span&gt; Implement step by step and write a unit test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;5.&lt;/span&gt;&lt;span&gt; UI tasks: Playwright smoke test, save a screenshot, verify it&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;6.&lt;/span&gt;&lt;span&gt; Run eslint --fix, prettier --write, and e2e for affected files&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;7.&lt;/span&gt;&lt;span&gt; Run tsc and unit tests project-wide&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;8.&lt;/span&gt;&lt;span&gt; All tests must pass. Broke an unrelated test? Fix it first.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;9.&lt;/span&gt;&lt;span&gt; Set &lt;/span&gt;&lt;span&gt;`passes: true`&lt;/span&gt;&lt;span&gt; in tasks.json for the completed task&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;10.&lt;/span&gt;&lt;span&gt; Log entry to .agent/logs/LOG.md (date, summary, screenshot path)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;11.&lt;/span&gt;&lt;span&gt; Commit using the Conventional Commit format&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Rules&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Only ONE task per invocation. After committing, output the DONE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;promise and STOP. Do NOT read the next task.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Kill background processes before the promise tag.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; No git push.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; When ALL tasks pass, output &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;promise&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;COMPLETE&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;#x3C;/&lt;/span&gt;&lt;span&gt;promise&lt;/span&gt;&lt;span&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; and nothing else.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;## Help Tags&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; BLOCKED: environment issues you cannot fix from the sandbox&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; DECIDE: a real decision a human has to make&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A few things are doing the heavy lifting here, and they are worth calling out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The very first line is the one-task rule in a blockquote. It is the first and last thing the agent sees, because batching is the failure mode that wrecks the most runs.&lt;/li&gt;
&lt;li&gt;Every state file is referenced with &lt;code dir=&quot;auto&quot;&gt;@&lt;/code&gt; so the agent loads it. The prompt does not paste the contents inline. It points, and the agent reads fresh each pass.&lt;/li&gt;
&lt;li&gt;The task flow is numbered and verification is steps 6 through 8, before the commit at step 11. Verification is not optional and it is not last.&lt;/li&gt;
&lt;li&gt;The &lt;code dir=&quot;auto&quot;&gt;Before Starting&lt;/code&gt; step makes &lt;code dir=&quot;auto&quot;&gt;STEERING.md&lt;/code&gt; the first thing checked, so you can steer a running loop by editing one file mid-run.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is how &lt;code dir=&quot;auto&quot;&gt;PROMPT.md&lt;/code&gt; feeds each iteration. The same text re-enters a fresh context every pass, and the agent rebuilds its understanding from disk before touching code:&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
    P[&quot;PROMPT.md (re-sent every iteration)&quot;] --&gt; S[&quot;ralph.sh starts an iteration&quot;]
    S --&gt; R[&quot;Fresh-context agent reads PROMPT.md&quot;]
    R --&gt; O[&quot;Reorient from disk: SUMMARY.md, tasks.json, LOG.md, STEERING.md&quot;]
    O --&gt; T[&quot;Pick one task with passes:false&quot;]
    T --&gt; W[&quot;Implement, run tests, lint, types&quot;]
    W --&gt; G{&quot;All gates pass?&quot;}
    G --&gt;|&quot;No&quot;| B[&quot;Fix next pass, or emit BLOCKED&quot;]
    G --&gt;|&quot;Yes&quot;| C[&quot;Commit, set passes:true, emit DONE&quot;]
    C --&gt; Q{&quot;All tasks done?&quot;}
    Q --&gt;|&quot;No&quot;| P
    Q --&gt;|&quot;Yes&quot;| D[&quot;Emit COMPLETE, loop exits 0&quot;]
    B --&gt; P&lt;/pre&gt;
&lt;p&gt;The arrow from the “No” branches back up to &lt;code dir=&quot;auto&quot;&gt;PROMPT.md&lt;/code&gt; is the entire point. Nothing carries over in the agent’s head. The prompt and the files on disk are what survive between passes.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-do-you-switch-modes-by-editing-promptmd&quot;&gt;How do you switch modes by editing PROMPT.md?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The default &lt;code dir=&quot;auto&quot;&gt;PROMPT.md&lt;/code&gt; is written for implementation: build the next task, test it, commit. But the loop is mode-agnostic. The agent does whatever the prompt tells it to, so you change the loop’s behavior by editing one file. The state files, the one-task rule, and the promise tags stay the same. Only the task flow changes.&lt;/p&gt;
&lt;p&gt;A few practical modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implementation (default): pick a &lt;code dir=&quot;auto&quot;&gt;passes: false&lt;/code&gt; task, build it, verify, commit. This is the shape shown above.&lt;/li&gt;
&lt;li&gt;Refactor: keep behavior identical, improve structure. The task flow becomes “refactor one module, prove behavior is unchanged by running the existing tests, commit.” The acceptance gate is that no test changed and all of them still pass.&lt;/li&gt;
&lt;li&gt;Review: read the diff from the last run and flag issues instead of writing features. The task flow becomes “read recent commits, check against the PRD and the code standards, write findings to a file, commit the report.” No production code changes.&lt;/li&gt;
&lt;li&gt;Test backfill: write missing tests for code that already exists. The task flow becomes “pick an untested module, write unit and e2e coverage, confirm the suite passes, commit.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because the mode lives entirely in &lt;code dir=&quot;auto&quot;&gt;PROMPT.md&lt;/code&gt;, you can keep a few prompt variants in version control and swap them in for a run. Run an implementation pass overnight, then point the same loop at a review prompt in the morning to audit what it built. The loop machinery in &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; does not change. You are only changing the instruction.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-are-the-common-promptmd-mistakes&quot;&gt;What are the common PROMPT.md mistakes?&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Most broken loops trace back to a prompt that breaks one of the four jobs above. These are the ones that show up most.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;writing-a-prompt-that-is-too-vague&quot;&gt;Writing a prompt that is too vague&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;“Build the feature and make it good” gives a fresh agent nothing to act on. It does not know which task, where the spec is, or what “good” means. A vague prompt produces a vague run: the agent improvises, picks an arbitrary task, skips verification, and you wake up to a branch you cannot trust. Be concrete. Name the files, name the rule, name the gates. The agent can only be as precise as the prompt.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;assuming-the-agent-remembers-anything&quot;&gt;Assuming the agent remembers anything&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This is the subtle one. It is tempting to write “continue where you left off” or “you already set up the database.” A fresh-context agent did not leave off anywhere and does not remember setting anything up. Every instruction that assumes memory is an instruction the agent cannot follow. Write the prompt as if the agent has never seen the project, because on every iteration, it has not. Push all state to disk and point the prompt at it.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;letting-the-agent-batch-tasks&quot;&gt;Letting the agent batch tasks&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;If the prompt does not aggressively enforce one task per iteration, a capable agent will try to be helpful and knock out three tasks in one pass. That produces a fat commit that mixes concerns, fails verification in a way that is hard to localize, and cannot be reverted without losing good work. The fix is to repeat the one-task rule at the top and in the rules section, and to require the &lt;code dir=&quot;auto&quot;&gt;DONE&lt;/code&gt; promise plus a hard stop after the commit. When batching does slip through, it is usually one of the documented &lt;a href=&quot;https://ralphloop.sh/blog/ralph-loop-failure-modes/&quot;&gt;Ralph loop failure modes&lt;/a&gt;, and the fix is structural rather than throwing more iterations at it.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;skipping-the-verification-gate-or-the-completion-signal&quot;&gt;Skipping the verification gate or the completion signal&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;A prompt with no test gate degrades the loop toward one-shot prompting, because there is nothing to stop a bad commit from landing. A prompt with no completion signal means the loop runs to its iteration cap even when the work is done, burning tokens for nothing. Always specify the verification stack and always specify the promise tags. Those two pieces are what let you start a run and walk away.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-short-checklist-before-you-commit-your-promptmd&quot;&gt;A short checklist before you commit your PROMPT.md&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Run through this before you point &lt;code dir=&quot;auto&quot;&gt;ralph.sh&lt;/code&gt; at a real project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the prompt name every state file the agent needs (&lt;code dir=&quot;auto&quot;&gt;SUMMARY.md&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;tasks.json&lt;/code&gt;, the task spec, &lt;code dir=&quot;auto&quot;&gt;LOG.md&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;STEERING.md&lt;/code&gt;)?&lt;/li&gt;
&lt;li&gt;Is the one-task rule stated at the top and enforced in the rules?&lt;/li&gt;
&lt;li&gt;Does it list the exact verification commands and require them before the commit?&lt;/li&gt;
&lt;li&gt;Does it define the promise tags and require a stop after the per-task &lt;code dir=&quot;auto&quot;&gt;DONE&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Does it read cleanly to an agent that has never seen the project before?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If all five are yes, a fresh-context agent can reorient and make progress on every pass. That is the entire job of the file.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;What is PROMPT.md in a Ralph loop?&lt;/h3&gt; &lt;p&gt;It is the instruction the loop sends to the agent on every iteration. The script ralph.sh re-sends the same PROMPT.md each pass, and because each iteration starts with a fresh context window, the prompt has to make a stateless agent reorient from disk and act. It points the agent at its state files, enforces one task per iteration, defines verification, and specifies the promise tags that signal completion.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Where do I put the PROMPT.md file?&lt;/h3&gt; &lt;p&gt;It lives at .agent/PROMPT.md in your project, alongside the other state files the loop reads: tasks.json, the per-task specs in tasks/, prd/SUMMARY.md, logs/LOG.md, and STEERING.md. Running npx @pageai/ralph-loop scaffolds the .agent directory with a default implementation-mode prompt you can edit.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I change what the loop does without changing the script?&lt;/h3&gt; &lt;p&gt;Edit PROMPT.md. The loop is mode-agnostic, so the agent does whatever the prompt tells it. Swap the task flow to refactor, review, or backfill tests while keeping the one-task rule, the state references, and the promise tags the same. The ralph.sh machinery does not change, only the instruction does.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why does PROMPT.md have to repeat the one-task rule?&lt;/h3&gt; &lt;p&gt;Because a capable agent will try to be helpful and batch several tasks into one pass, which produces a fat commit that is hard to verify and hard to revert. Stating the rule at the top and in the rules section, then requiring a DONE promise and a hard stop after the commit, keeps each iteration to a single verified task.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What happens if my PROMPT.md has no completion signal?&lt;/h3&gt; &lt;p&gt;The loop runs until it hits the iteration cap and exits with code 1 (MAX_ITERATIONS) even when the work is actually done, wasting tokens. Always define the promise tags so the agent can emit COMPLETE when all tasks pass, which exits the loop with code 0, and BLOCKED or DECIDE when it needs a human.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>ralph-technique</category></item><item><title>Cost Control for Autonomous AI Coding Agents</title><link>https://ralphloop.sh/blog/ai-agent-cost-control/</link><guid isPermaLink="true">https://ralphloop.sh/blog/ai-agent-cost-control/</guid><description>An autonomous loop burns money when it thrashes. Cap iterations, match the model to the task, and gate every pass on tests so the loop stops instead of looping forever.

</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An autonomous coding loop burns money when it thrashes: it retries the same broken task, drags a bloated context window from one call to the next, or runs a frontier model on work a cheap model could finish. You control spend with three knobs, not a billing dashboard. Cap the iterations so the run has a hard ceiling. Pick the model per agent so mechanical tasks do not pay frontier prices. Gate every iteration on tests, lint, and type checks so the loop stops working a task it cannot finish instead of grinding on it forever.&lt;/p&gt;
&lt;p&gt;This is the cost-control chapter of the larger guide to &lt;a href=&quot;https://ralphloop.sh/blog/run-ai-coding-agent-overnight/&quot;&gt;running an AI coding agent overnight&lt;/a&gt;. The mechanics below are the same Bash loop &lt;a href=&quot;https://ghuntley.com/ralph/&quot;&gt;Geoffrey Huntley described&lt;/a&gt;, with the spend-shaped edges called out.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-an-autonomous-loop-costs-more-than-one-prompt&quot;&gt;Why an autonomous loop costs more than one prompt&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;A single prompt costs one call. A loop costs one call per iteration, and a badly built loop multiplies that in two ways.&lt;/p&gt;
&lt;p&gt;First, runaway iterations. If nothing tells the loop to stop, it keeps spawning the agent. Ten iterations is fine. Two hundred iterations on a task that was already done at iteration three is two hundred calls you paid for and zero you needed.&lt;/p&gt;
&lt;p&gt;Second, context bloat. A naive long-running agent keeps appending to one conversation. Tokens accumulate, every turn re-sends the whole transcript, and the per-call cost climbs as the session drags on. This is also where quality falls apart, because the agent loses the plot in a wall of stale context. The Ralph technique fixes both at once: each iteration starts the agent with a fresh context window and reads state from disk. Cost per iteration stays flat instead of growing with session length. The deeper version of that argument is in the writeup on &lt;a href=&quot;https://ralphloop.sh/blog/what-is-the-ralph-technique/&quot;&gt;what the Ralph technique is&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So cost control is not a separate feature you bolt on. It falls out of building the loop correctly. The rest of this post is the specific knobs.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;cap-iterations-with--n-once-and-max-iterations&quot;&gt;Cap iterations with -n, —once, and —max-iterations&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The iteration count is your budget dial. It is the hard ceiling on how many agent calls a single run can make. Ralph defaults to 10:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 10 iterations, the default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Set the cap explicitly when you want a longer unattended run or a tighter leash:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 50 iterations for an overnight run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# the long form does the same thing&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--max-iterations&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;For a smoke test, run exactly one iteration. This is the cheapest possible way to check that the agent authenticates, reads your prompt, picks up a task, and produces a sane diff before you commit to a long run:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# exactly one iteration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Treat &lt;code dir=&quot;auto&quot;&gt;--once&lt;/code&gt; as your dry run. Spend one call, read the diff and the log, and only then scale the count up. A cheap probe at the start saves you from discovering at iteration 40 that the prompt was wrong.&lt;/p&gt;
&lt;p&gt;The cap matters because it converts an open-ended process into a bounded one. Without it, “run until done” can mean “run until your bill scares you.” With it, the worst case is exactly the number of iterations you authorized. When the loop hits the cap without finishing, it exits with code &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; (MAX_ITERATIONS). That is a signal, not a failure: read the log, decide whether to raise the cap, and resume.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;match-the-model-to-the-task&quot;&gt;Match the model to the task&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The second knob is model choice, and it is where most of the savings live. Frontier models cost more per token than smaller ones. Plenty of agent work does not need a frontier model: renaming symbols, wiring up boilerplate, fixing a failing lint rule, applying a mechanical refactor across files. Running those on a cheap model and saving the expensive model for genuinely hard reasoning is the single biggest lever on a long run.&lt;/p&gt;
&lt;p&gt;Ralph forwards anything after the &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; separator straight to the agent, so you choose the model with the agent’s own flag:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Codex on a specific model&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gpt-5.5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# Gemini on the pro model&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-a&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gemini&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The rule to remember: everything left of &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; configures Ralph (which agent, how many iterations, login). Everything right of &lt;code dir=&quot;auto&quot;&gt;--&lt;/code&gt; configures the agent (model, approval mode, and so on). Keep them on the correct side and the loop behaves.&lt;/p&gt;
&lt;p&gt;This works across the supported agents, which are &lt;code dir=&quot;auto&quot;&gt;claude&lt;/code&gt; (default), &lt;code dir=&quot;auto&quot;&gt;codex&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;copilot&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;cursor&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;gemini&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;opencode&lt;/code&gt;. Each has its own sandbox and its own credentials, so you can keep separate runs for separate model tiers. A practical pattern: point a cheap-model loop at a queue of mechanical tasks, and a frontier-model loop at the hard architectural ones. The per-agent CLIs are covered in detail in the &lt;a href=&quot;https://ralphloop.sh/blog/run-codex-cli-in-a-loop/&quot;&gt;Codex loop walkthrough&lt;/a&gt; and the &lt;a href=&quot;https://ralphloop.sh/blog/run-claude-code-in-a-loop/&quot;&gt;Claude Code loop walkthrough&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Two things make model selection safe rather than risky. The work is decomposed into small tasks (see below), so a cheaper model is asked to do something small enough that it can actually succeed. And every iteration is verified, so if the cheaper model produces a broken change, the gate catches it instead of letting it ship.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;verification-gates-stop-the-loop-from-thrashing&quot;&gt;Verification gates stop the loop from thrashing&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The most expensive failure mode is not a model that costs too much per call. It is a loop that keeps calling the agent on a task it cannot complete. Verification gates are what turn that infinite drip into a clean stop.&lt;/p&gt;
&lt;p&gt;Every iteration runs the verification stack as step three of the loop:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the highest-priority incomplete task in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Work the steps in &lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run tests, linting, and type checking.&lt;/li&gt;
&lt;li&gt;Complete the task, take a screenshot, update the task status, and commit.&lt;/li&gt;
&lt;li&gt;Repeat until all tasks pass or the iteration cap is reached.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The stack Ralph assumes is Playwright for end-to-end tests, Vitest for unit tests, TypeScript for type checks, ESLint for linting, and Prettier for formatting. The repo mantra is blunt: if you didn’t test it, it doesn’t work.&lt;/p&gt;
&lt;p&gt;Here is why this is a cost control and not just a quality control. A task only flips to done when its checks pass. If the change is broken, the gate fails, the task stays open, and the agent gets honest feedback to fix it on the next pass. Without gates, the agent can mark a broken task done and move on, or worse, keep editing the same file with no signal about whether it is closer or further from working. That second case is the thrash: real calls, real tokens, zero progress. Gates give the loop a definition of progress, so a task either advances toward passing or you find out fast that it is stuck.&lt;/p&gt;
&lt;p&gt;When a task genuinely cannot be finished, you do not want the loop to spend the rest of its cap discovering that one iteration at a time. The agent emits a promise tag instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; means every task is finished.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;BLOCKED:reason&amp;#x3C;/promise&gt;&lt;/code&gt; means it needs human help.&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;DECIDE:question&amp;#x3C;/promise&gt;&lt;/code&gt; means it needs a decision from you.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those map to exit codes: &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; for COMPLETE, &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt; for MAX_ITERATIONS, &lt;code dir=&quot;auto&quot;&gt;2&lt;/code&gt; for BLOCKED, and &lt;code dir=&quot;auto&quot;&gt;3&lt;/code&gt; for DECIDE. A &lt;code dir=&quot;auto&quot;&gt;BLOCKED&lt;/code&gt; or &lt;code dir=&quot;auto&quot;&gt;DECIDE&lt;/code&gt; exit ends the run early instead of burning the remaining iterations. You spend on the calls that made progress and stop on the call that hit a wall. The full treatment of why an autonomous agent needs this feedback is in &lt;a href=&quot;https://ralphloop.sh/blog/verification-loops-for-ai-agents/&quot;&gt;verification loops for AI agents&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;atomic-tasks-keep-each-iteration-cheap&quot;&gt;Atomic tasks keep each iteration cheap&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Cost per iteration tracks how much the agent has to read and reason about in that single call. A vague, sprawling task forces the agent to load a lot of context, take many steps, and produce a large risky diff that is more likely to fail verification and get retried. A small, well-scoped task is cheap to load, fast to finish, and easy to verify.&lt;/p&gt;
&lt;p&gt;So the breakdown of work is itself a cost lever. Ralph follows one rule per invocation: the agent completes exactly one task, commits, and stops. It never batches several tasks into a single iteration. That keeps each commit small, each diff reviewable, and each context window focused on one thing.&lt;/p&gt;
&lt;p&gt;The state lives on disk, not in chat history. A task lookup table (&lt;code dir=&quot;auto&quot;&gt;.agent/tasks.json&lt;/code&gt;) points to individual task specs (&lt;code dir=&quot;auto&quot;&gt;.agent/tasks/TASK-{ID}.json&lt;/code&gt;), and the running log lives in &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt;. Because progress is on the filesystem and in the git log, a fresh-context agent reorients at the start of every iteration without re-reading a giant transcript. That is what keeps the token cost of iteration 40 the same as iteration 2.&lt;/p&gt;
&lt;p&gt;The completion promise is the other half of this. When the work is genuinely done, the agent emits &lt;code dir=&quot;auto&quot;&gt;&amp;#x3C;promise&gt;COMPLETE&amp;#x3C;/promise&gt;&lt;/code&gt; and the loop exits with code &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;, even if you authorized 50 iterations and it finished in 12. You only pay for the iterations the work actually needed. The cap is the ceiling; the completion promise is the early exit. Together they bound spend from both ends.&lt;/p&gt;
&lt;pre dir=&quot;ltr&quot;&gt;flowchart TD
    Start([&quot;./ralph.sh -n 50&quot;]) --&gt; Cap{&quot;Iteration &amp;#x3C; cap?&quot;}
    Cap --&gt;|&quot;no&quot;| MaxOut([&quot;exit 1: MAX_ITERATIONS, stop spending&quot;])
    Cap --&gt;|&quot;yes&quot;| Pick[&quot;Pick one atomic task from .agent/tasks.json&quot;]
    Pick --&gt; Spawn[&quot;Spawn agent with fresh context and chosen model&quot;]
    Spawn --&gt; Work[&quot;Read state from disk, do one task&quot;]
    Work --&gt; Gate{&quot;Tests, lint, type check pass?&quot;}
    Gate --&gt;|&quot;no&quot;| Log[&quot;Log failure, keep task open&quot;]
    Log --&gt; Cap
    Gate --&gt;|&quot;yes&quot;| Commit[&quot;Commit, update task status, screenshot&quot;]
    Commit --&gt; Promise{&quot;Promise tag?&quot;}
    Promise --&gt;|&quot;COMPLETE&quot;| Done([&quot;exit 0: done early, no wasted calls&quot;])
    Promise --&gt;|&quot;BLOCKED or DECIDE&quot;| Stop([&quot;exit 2 or 3: stop and ask a human&quot;])
    Promise --&gt;|&quot;none&quot;| Cap&lt;/pre&gt;
&lt;p&gt;Read the diagram as a spend story. Three paths end the run: the cap is hit, the work completes, or the agent asks for help. None of them let the loop drip calls into a task that is going nowhere.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;watch-the-cost-per-iteration&quot;&gt;Watch the cost per iteration&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;You cannot control what you cannot see, so the last piece is the per-iteration trail. Ralph records each iteration’s cleaned output to &lt;code dir=&quot;auto&quot;&gt;.agent/history/&lt;/code&gt; and appends to the running log at &lt;code dir=&quot;auto&quot;&gt;.agent/logs/LOG.md&lt;/code&gt;. That history is your audit log for spend: one entry per agent call, in order, with what the agent did and whether the task advanced.&lt;/p&gt;
&lt;p&gt;Reading the history tells you the things that actually drive cost. How many iterations did it take to close each task? Is one task failing verification over and over and eating calls? Did the run hit the cap with work still open, or did it exit early on a completion promise? An iteration that did real work and committed is money well spent. A run of iterations that all touch the same failing task is the thrash you want to catch and fix, usually by tightening the task spec or the prompt in &lt;code dir=&quot;auto&quot;&gt;.agent/PROMPT.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When you need more than the log, get inside the box. Each agent runs in its own Docker Sandbox with a deterministic name, &lt;code dir=&quot;auto&quot;&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;project-dir&gt;-&amp;#x3C;hash8&gt;&lt;/code&gt;. List them and open a shell:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# list sandboxes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# shell into the running sandbox to inspect logs and history&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-&amp;#x3C;agent&gt;-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;From there you can read &lt;code dir=&quot;auto&quot;&gt;.agent/history/&lt;/code&gt;, re-run a failing test by hand, and figure out why a task is stalling before it costs you more iterations. The &lt;a href=&quot;https://docs.docker.com/ai/sandboxes/&quot;&gt;Docker Sandboxes documentation&lt;/a&gt; covers the sandbox model in full, and the broader practice of making a long run auditable is the subject of &lt;a href=&quot;https://ralphloop.sh/blog/observability-for-ai-coding-agents/&quot;&gt;observability for AI coding agents&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If the history shows the loop heading the wrong way and you do not want to kill it, edit &lt;code dir=&quot;auto&quot;&gt;.agent/STEERING.md&lt;/code&gt;. Ralph folds that critical work into the next iteration before resuming the task list. Steering mid-run is cheaper than letting a misdirected loop spend its whole cap and then starting over.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;a-cost-aware-run-end-to-end&quot;&gt;A cost-aware run, end to end&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Put the knobs together and a deliberate run looks like this:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 1. cheap probe: one iteration to confirm the setup is sane&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--once&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 2. mechanical backlog on a cheaper model, bounded cap&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;./ralph.sh&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--agent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;codex&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-n&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--model&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gpt-5.5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# 3. read the per-iteration trail to see where the calls went&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sbx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-it&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ralph-codex-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bash&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;# then read .agent/logs/LOG.md&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The probe spends one call to de-risk the run. The bounded loop runs an appropriately priced model with a hard ceiling, gates every iteration on tests so it cannot thrash, works one atomic task at a time so each call stays cheap, and exits early on a completion promise if the work finishes ahead of the cap. The history tells you exactly where the money went so the next run is tighter. That is cost control for an autonomous agent: not a billing alert after the fact, but a loop built so the expensive failure modes cannot happen.&lt;/p&gt;
&lt;section aria-label=&quot;Frequently asked questions&quot;&gt; &lt;h2&gt;Frequently asked questions&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;How do I limit how much an autonomous coding agent can spend?&lt;/h3&gt; &lt;p&gt;Cap the iterations. The iteration count is the hard ceiling on agent calls per run. Ralph defaults to 10; set it with ./ralph.sh -n 50 or --max-iterations 5, and use ./ralph.sh --once for a one-call dry run. When the loop hits the cap it exits with code 1 instead of running forever.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Should I run a cheaper model for an AI coding agent loop?&lt;/h3&gt; &lt;p&gt;Yes, for mechanical work. Renames, boilerplate, lint fixes, and routine refactors do not need a frontier model. Pass the model after the -- separator, for example ./ralph.sh --agent codex -- --model gpt-5.5, and save the expensive model for genuinely hard reasoning. Small tasks plus verification gates make the cheaper model safe to use.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;What stops an agent loop from thrashing on a task it cannot finish?&lt;/h3&gt; &lt;p&gt;Verification gates and promise tags. Every iteration runs tests, lint, and type checks, so a task only flips to done when its checks pass. If the agent gets stuck it emits BLOCKED or DECIDE, which exits the run early (codes 2 and 3) instead of spending the rest of the iteration cap on a task going nowhere.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;Why does a fresh context window per iteration save money?&lt;/h3&gt; &lt;p&gt;A single long conversation re-sends a growing transcript on every turn, so the per-call cost climbs as the session drags on. The Ralph technique starts each iteration with a clean context and reads state from disk (.agent/tasks.json, the log, git history), so the token cost of iteration 40 is about the same as iteration 2.&lt;/p&gt; &lt;/div&gt;&lt;div&gt; &lt;h3&gt;How do I see where the cost went in an autonomous run?&lt;/h3&gt; &lt;p&gt;Read the per-iteration trail. Ralph writes each iteration&apos;s output to .agent/history/ and appends to .agent/logs/LOG.md, one entry per agent call. Shell into the sandbox with sbx exec -it ralph-&amp;#x3C;agent&gt;-&amp;#x3C;project&gt;-&amp;#x3C;hash8&gt; bash to read it. Look for tasks that fail verification repeatedly, since those are the calls that cost money without making progress.&lt;/p&gt; &lt;/div&gt; &lt;/section&gt; 
&lt;aside aria-label=&quot;Get started with Ralph Loop&quot;&gt; &lt;h2&gt;Run your own Ralph loop&lt;/h2&gt; &lt;p&gt;Ralph is a hackable script you point at your project. Install it and let an agent work through your task list.&lt;/p&gt; &lt;pre&gt;&lt;code&gt;npx @pageai/ralph-loop&lt;/code&gt;&lt;/pre&gt; &lt;div&gt; &lt;a href=&quot;https://www.npmjs.com/package/@pageai/ralph-loop&quot;&gt;
Install from npm
&lt;/a&gt; &lt;a href=&quot;https://github.com/pageai-pro/ralph-loop&quot;&gt;Star on GitHub&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=3TL8Ez66I3o&quot;&gt;Watch the walkthrough&lt;/a&gt; &lt;/div&gt; &lt;/aside&gt;</content:encoded><category>autonomous-agents</category></item></channel></rss>