HomeBlog › The two scariest parts of autonomous agents — runaway loops and exposed APIs
Blog

The two scariest parts of autonomous agents — runaway loops and exposed APIs

Jul 12, 20269 min readBy the Rysh team

Everyone I talk to about autonomous agents has the same two fears, and they’re both justified:

  1. The loop runs away. You give an agent a goal, it starts iterating, and it either burns your API budget at 3am or cheerfully does the wrong thing forty times in a row.
  2. The access problem. For an agent to do real work it needs your internal APIs — billing, inventory, the admin service that has no auth story because it was never supposed to leave localhost. Every “just expose an endpoint” answer makes your security team (rightly) say no.

I’ve been building rysh — an agentic terminal multiplexer where every pane can be a shell, an agent, or a chat — and these two fears shaped two features I want to walk through properly, because I think the mechanics matter more than the pitch. As always on dev.to: real commands, and an honest list of what’s not done at the end.

Part 1 — Loop engineering: autonomy with a leash you define

rysh has saved, reusable automations. You write the goal once, then run it with hard limits:

# save a recipe (stored under .rysh/automations/)
##auto web save scout "find the pricing page, extract the plan names and prices"

# run it — with a leash
##auto web run --budget-size 50p --max-duration 10m --takeover-when 80 scout

Three things are going on there, and each one exists because of a real failure mode:

Hard budgets, not vibes. --budget-size, --max-iterations, and --max-duration are ceilings, not suggestions — when a ceiling is hit, the loop stops. Budgets are measured in context tokens with units I’m unreasonably fond of: pages, books, and shelves. A page is 1,000 tokens, a book is 200 pages, a shelf is 20 books — so --budget-size 50p is a 50k-token run and 3b (the default) is 600k. “How much should this cost?” becomes a number you set, not a surprise you discover. There’s also --passes N for fixed-round runs, --while-duration / --while-budget for open-ended-but-bounded work, and --dry-run to see what a recipe would do before it does it.

LLM-judged loop conditions. The loop doesn’t just count iterations — after each pass, a judge step evaluates whether the goal is actually met. “Run until the tests pass” means something; “run 10 times and hope” doesn’t.

Graceful takeover, not a wall. --takeover-when 80 means: when 80% of any ceiling is consumed, the run switches to a takeover leg — a wrap-up prompt you wrote (in the recipe’s takeover_prompt) takes over the remaining budget to finish cleanly: save partial results, report what’s missing, print where it stopped. The agent lands the plane instead of hitting a wall mid-task. (Plain-language guardrails — “stop on any login wall or rate limit” — live in the recipe prompt itself, where the agent obeys them per-step.)

The same engine fans out and schedules:

# run one recipe across a list
##auto web run --each "site-a.com,site-b.com,site-c.com" scout

# put it on a schedule — cron can fire ANY rysh input: prompts, @agent calls, shell, automations
##cron add nightly-scout "0 2 * * *" --pane scout-pane --mode rysh "##auto web run scout"
##cron next     # when does it fire?
##cron logs     # what happened last night?

And because interruptions happen, automations (and agents generally) checkpoint:

@@researcher stop        # interrupt mid-run — state is preserved
@@researcher continue    # resume from the checkpoint, not from zero

Recipes are files (this is where loop engineering gets real)

The one-liner save is just the front door. A recipe is a markdown file with YAML frontmatter under .rysh/automations/ — versionable, reviewable, PR-able like everything else in your repo. Here’s a real one from my own setup (lightly trimmed): a scout that browses Instagram for podcast-guest candidates on a topic, discovery-only, and saves a shortlist to a results folder.

# .rysh/automations/webs/guest-scout.md
---
description: Discover potential podcast guests on Instagram, scoped to a topic.
  Discovery-only (never messages/follows/likes); saves a reviewed shortlist.
web_profile: ig-podcast        # persistent, pre-authenticated browser profile
url: https://www.instagram.com/
args: [topic]                  # run as: ##auto web run guest-scout tango
output_dir: guest-scout/results
loop:
  do:                          # the INNER loop — one working session
    interval: 30
    max_iterations: 300
    max_duration: 7m
    auto_continue: true
    budget:
      size: 3b                 # 3 "books" = 600k tokens, hard ceiling
      watch:
        takeover_when: 90      # at 90% consumed, switch to the wrap-up leg
        takeover_prompt: >
          The discovery budget is used up — stop browsing now. Save the
          shortlist as-is, mark it PARTIAL at the top, print the file path
          and what to cover next time.
  while:                       # the OUTER loop — repeat sessions until the GOAL holds
    enabled: true
    max_iterations: 5
    max_duration: 40m
    budget: 15b
    prompts:
      until: >
        The saved shortlist contains at least 12 strong candidates, each with
        a handle, a why-they-fit line, and a public contact or an explicit
        "on-platform only" note — with no duplicate handles.
      iterate_with: >
        Continue scouting for podcast guests about {{args}}, same rules as
        before (discovery only; stop on any login wall or rate limit). The
        current shortlist is seeded above: find NEW candidates it is missing,
        merge, and save back to the same file.
---
You are browsing Instagram looking for potential podcast GUESTS about: **{{args}}**.

If **{{args}}** is empty, ask me which topic to scout and stop — do not guess.

This is **discovery only** — do NOT message, follow, like, or DM. Just read
public pages. If you hit a login wall, captcha, or rate-limit notice: STOP
immediately and report it.

For each candidate collect: handle, why-they-fit, follower count, public
contact (or "none"), and one post that makes them relevant. Aim for 8–12.

When done, write the list as Markdown to {{output_dir}}/<topic>-<date>.md
and print the saved file path here.

Read the loop block again, because the two-level structure is the whole idea:

So the recipe encodes: what to do, when to act, when to stop, how to stop gracefully, what “done” actually means, and how to continue if it isn’t. That’s what I mean by loop engineering — the loop is data, not code, and every safety property is declared next to the goal it protects. {{args}} templating makes one file a family of automations (##auto web run guest-scout tango today, guest-scout "science communicators" tomorrow), and results land in output_dir as dated markdown files you can diff.

##auto isn’t just for browsers — the same save/run/schedule/budget machinery drives task, agent, and code automations (##auto code anchors prompts to a project working directory). Browser automations can run headless (##web headless on), so this works on a server without a desktop session.

The design position: the interesting engineering in agent loops isn’t the loop — it’s the brakes. Budgets, judges, takeover predicates, checkpoints. That’s what makes “let it run overnight” a reasonable sentence instead of a horror-story opener.

Part 2 — Forge: your private API, callable by agents, never exposed

Now the access problem. Say your team has an internal REST service on localhost:8080 — or behind your VPN — and you want agents (yours, or a teammate’s in another rysh session) to call it. The standard options are all bad: expose it publicly behind auth you now have to harden, hand out credentials to every agent runtime, or don’t do it at all.

Here’s the rysh flow. First, turn the spec into tools:

##forge add billing ./billing-openapi.yaml
##integration enable billing
# your agents can now call the billing API as ordinary tools — locally

Forge ingests the OpenAPI spec and generates governed agent tools, plus a standalone MCP server, SDKs (Go/Python/TypeScript/Java), and docs. That alone is useful. But here’s the part I haven’t seen elsewhere — share the API to another session’s agents without exposing it:

# you (the API owner):
##forge share api billing

# your teammate, in their own rysh session:
##forge subscribe billing --scope pane
# their agent now sees the billing operations as normal tools

What actually happens when their agent calls one of those tools:

  1. The subscriber side sends only the operation name + arguments over the rysh upstream (NATS over WebSocket — the same channel used for live pane sharing).
  2. Your session receives the request, checks it against your allowlist, and — this is the key part — executes the real HTTP call locally, from your machine, where localhost:8080 actually resolves.
  3. The response is redacted (secrets stripped) and sent back as the tool result.

The remote agent never learns the API’s address. The API is never bound to a public interface. No credentials leave your machine. The endpoint stays exactly where it was — behind your firewall — and requests tunnel through rysh to be executed by the only party that was already trusted to call it: you.

The safety posture, because this feature is only as good as its defaults:

This path is end-to-end tested and went through an adversarial security audit before I’d let anyone use it — I’ll happily go deeper on the invariants in the comments.

Honest scope note: Forge is OpenAPI-only today. gRPC ingest is a stub and there’s no GraphQL runtime. If your API has a spec, this works now; if not, not yet.

Why these two features are the same feature

Both are answers to the same question: how much power can you give an agent while keeping a human meaningfully in control?

That’s the design thesis behind rysh generally — every tool call visible in the pane, dangerous actions gated behind approval, secrets redacted before anything leaves your session. Agents you can trust because you can see them and bound them, not because a vendor promised.

The stack, briefly

Go; an actor runtime over NATS for all messaging (the tunneling above is “just” the same actor/subject machinery that powers live pane sharing); Bubble Tea TUI. It runs on Anthropic Claude — I’m not claiming a better model; the value is the governance and the surfaces around it. Self-hostable on your own key; the provider layer is pluggable, Claude is the live backend.

What’s not done (don’t believe me if I imply otherwise)

Try it / shape it

Rysh is in private beta — the code isn’t public yet. It’s self-hostable (you run it on your own infra with your own Anthropic key), and access right now is through the design-partner program: free access, a direct line to me, and your feedback sets the roadmap. If you have one internal API you’d love agents to use safely, or one workflow you want to run overnight on a leash, that’s exactly the conversation I want: apply to the design-partner program.

Feedback on the tunneling trust model is especially welcome — poke holes in it in the comments.

Try Rysh

Every pane is a shell and an AI agent — install takes one command.

Get started free →

Related: Keeping AI agents safe: the approval flow · Why every terminal pane should be an AI agent