Running an agent once is easy. Running it in a loop overnight, with rate limits and costs and failures piling up, is where most people’s bash script stops being enough.
The Bash Loop Everyone Writes
Matt Pocock showed how to stream Claude Code while running AFK Ralph. The canonical ralph.sh sits at 17k stars on GitHub, which tells you how many people are reaching for the same pattern. You’ve probably written (or copied) something like this yourself:
for ((i=1; i<=10; i++)); do
claude --print "do the task"
done
This works until you hit the blank screen. --print gives you no streaming output, so your terminal sits there dark and you have no idea whether Claude is working, stuck, or wedged on a prompt it’ll never escape from.
So you upgrade: stream-json piped through jq, tee to capture the final result, grep to filter the noise, temp files to check for completion markers. Twenty lines of shell just to see what’s happening. Matt’s article is a well-crafted solution to a real problem.
His closing line says it all:
My hope is that relatively soon I’ll be able to delete this article because Claude Code will have shipped a feature that allows you to stream the responses while still capturing the final output.
That feature already exists. It’s called Juggle:
CLI Ralph Loops with Good UX
One Command
juggle loop "fix the failing tests" -n 5
That runs Claude Code five times with a fresh context each iteration, streams the output in real-time, captures results, logs every run, and stops when done. If you’ve been writing for loops around claude --print, this is the drop-in replacement.
Loop Mode
Every flag here solves a problem the bash loop handles badly, or doesn’t handle at all.
# Run 3 iterations
juggle loop "fix the auth tests and add more test coverage" -n 3
# Stop when a condition is met
juggle loop "keep refactoring" "See the @journal.md for the progress. Update it at the end" --stop-when "test -f DONE"
# Run every 5 minutes indefinitely
juggle loop "review open issues" --delay 5
# Compose prompts from files and inline text
juggle loop @tdd @fix "broken email validation" -n 5
# Log everything, resume after crash
juggle loop @task.md --log run.jsonl --resume
Each flag replaces something you’d otherwise bolt onto the bash loop:
--stop-whenreplaces grep-for-magic-string-in-temp-file--delayreplacessleepwith proper signal handling--log+--resumereplacesteeplus manual restart counting@fileresolution pulls prompts from your$JUGGLE_PROMPTSlibrary, so you can compose them like lego pieces
Rigid vs. Flexible
The bash loop locks you in. Changing the prompt means editing the script. Switching from Claude Code to Codex means rewriting it, because the binary’s different, the flags are different, and the output format is different. Adding a code review step between iterations means more pipes, more temp files, more things that’ll break at 2am.
Juggle keeps the loop, the prompt, and the agent separate. Changing the task is a matter of swapping the positional argument:
# Generic loop
juggle loop @tdd "take the top task from jira and implement it"
# Watch for issues loop
juggle loop @subagents "Review the logs via the mcp server and report any issues" --delay 30m
Bring your own task storage. A directory of markdown files, a GitHub issue list, a database, a sticky note on your monitor: Juggle doesn’t care where your tasks come from, as long as you feed them in as prompts. A loop should free you up, not lock you into someone else’s workflow.
Different agent? One flag:
# Claude Code → Codex, same guards, same hooks, same logging (where the features are supported)
juggle loop @task.md --provider codex
Juggle maps its flags to each provider’s native equivalents automatically, so --allowed-tools, --max-turns, and --max-cost all keep working regardless of which agent is running.
Queue Mode
A bash loop can wait for work (with inotifywait, polling scripts, CI webhooks) but typically doesn’t. Queue mode bakes waiting in.
# Watch a directory for task files
juggle queue @rules.md --watch ./tasks/
# Run on a fixed interval
juggle queue "check health" --every 30s --now
# HTTP trigger endpoint
juggle queue @rules.md --serve :8080 --id myapp
# Combine: watch + interval + manual trigger
juggle queue @rules.md --watch ./tasks/ --every 5m --id myapp
Triggers are composable. Watch, interval, HTTP, and manual triggers (juggle trigger myapp "do this") all work together in a single command. juggle trigger can be called from anywhere else (CI pipelines, git hooks, cron jobs) to kick off a session on demand. Add --workers 4 --dashboard for parallel processing with a TUI overview.
The Guards
This is the part bash loops really don’t handle. When things go wrong at 2am, a plain script keeps going regardless: burning tokens, hammering into rate limits, compounding one failure into ten.
Rate limiting. Juggle detects 429s, applies exponential backoff, and retries the same iteration. --max-wait caps how long it’ll wait before giving up.
Quota exhaustion. When you’ve burned through your API allowance, Juggle sleeps until the window resets, then continues where it left off.
Cost control. --max-cost 5.00 stops the loop when cumulative spend hits the threshold. No surprises on your next invoice.
Failure modes. --on-failure stop|continue|retry lets you halt, skip, or retry with backoff. --max-failures 3 stops after N consecutive crashes. The default is to stop after three, which is usually what you want.
The guards are on out of the box. You don’t configure them into existence, you just tune the ones you care about with flags.
Lifecycle Hooks
Control what happens before, during, and after each iteration, both at the shell level and inside the agent session itself.
juggle loop @task.md \
--cmd-before "git pull" \
--cmd-after "git add -A && git commit -m 'iteration done'" \
--agent-after "summarize what you changed" \
-n 10
Shell hooks (--cmd-before, --cmd-after) run system commands. Agent hooks (--agent-pre, --agent-before, --agent-after, --agent-post) inject prompts at each phase of the loop. The --stop-when flag is itself a hook that checks a shell condition after every iteration; exit zero means stop.
For more complex workflows, with multiple agents and commands orchestrated through lifecycle events, juggle pipeline defines the full graph in a TOML file. That’s a topic for another post.
What Juggle Doesn’t Do
- No PRD opinions. You bring the prompt.
- No state between iterations. Fresh context every time. If you want to persist learnings across iterations, tell the agent to write to a file in the prompt. Your session, your rules.
- No agent replacement. It runs your agent (Claude, Codex, Gemini, OpenCode, or custom via
--agent-cmd). - No project footprint. No config files, no state directories, just a binary.
Juggle is a runner. Not a framework, not an opinionated workflow, just the thing that calls your agent in a loop and keeps the house from burning down while it does.
Get Started
# macOS
brew tap ohare93/tap && brew install juggle
# Windows
scoop bucket add ohare93 https://github.com/ohare93/scoop
scoop install juggle
# Linux
curl -sSL https://raw.githubusercontent.com/ohare93/juggle/main/install.sh | bash
# Or, with Go
go install github.com/ohare93/juggle/cmd/juggle@latest
Then go to bed:
juggle loop "find one part of the code to improve. Commit when done. Good night, have fun."
No count, no limit. An agent pottering around improving your codebase while you sleep, streaming its output, logging every run, and handling rate limits and crashes on its own.
CLI Ralph Loops with Good UX