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:

ohare93/juggle

CLI Ralph Loops with Good UX

Go 14

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-when replaces grep-for-magic-string-in-temp-file
  • --delay replaces sleep with proper signal handling
  • --log + --resume replaces tee plus manual restart counting
  • @file resolution pulls prompts from your $JUGGLE_PROMPTS library, 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.

ohare93/juggle

CLI Ralph Loops with Good UX

Go 14