Coding agents
froog prompt gives a coding agent in the sandbox a task and prints its work (its text on stdout, tool calls on stderr) until the turn ends:
$ froog prompt my-box "make the failing tests pass"
$ froog prompt my-box "now add a test for the edge case" # same conversation: it remembers
$ froog prompt my-box "add a /health endpoint" --agent codex --new
$ froog prompt my-box "review the diff" --model opus --effort high --fast
$ froog prompt my-box "why does this crash?" --attach error.png --attach trace.log
Ten harnesses are installed in every sandbox. Pick one with --agent (API: agent), a model with --model and a reasoning level with --effort. The model and effort are passed straight to the harness. froog prompt --list-models (API: GET /v1/provider-models, no key needed) prints each harness's models, effort levels and which have fast mode. froog prompt my-box --list-models adds the models OpenCode and pi list in that sandbox with its keys.
--agent | Harness | Key or login | --model · --effort |
|---|---|---|---|
claude (default) | Claude Code | ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, a Claude subscription or a login | yes · low … max |
codex | OpenAI Codex | OPENAI_API_KEY, a ChatGPT subscription or a login | yes · minimal … xhigh |
gemini | Gemini CLI | GEMINI_API_KEY or a Google login | yes · no |
opencode | OpenCode | the key of the provider you use | yes (provider/model) · no |
pi | pi | the key of the provider you use | yes · off … max |
amp | Amp | AMP_API_KEY (an access token) | no · no |
cursor | Cursor CLI | CURSOR_API_KEY | yes · no |
prime | Prime Agent | the key of the provider you use (e.g. ANTHROPIC_API_KEY, PRIME_API_KEY) | yes · off … max |
kimi | Kimi Code | MOONSHOT_API_KEY or kimi login (a Kimi Code plan) | yes · low … max |
mistral | Mistral Vibe | MISTRAL_API_KEY or vibe --setup | yes · no |
When a harness has neither its key nor a login in the sandbox, the prompt is refused before it runs with 400 provider_not_configured, saying how to set it up. Give agents their API keys once, as environment variables, and every sandbox has them:
$ froog env set ANTHROPIC_API_KEY # prompts for the value; for Claude Code
$ froog env set OPENAI_API_KEY # for Codex
Or connect a subscription, or sign in inside the sandbox once: froog ssh my-box, then run claude (or codex) and follow its login. A login made inside stays on the sandbox's disk, so forks and restarts keep it.
Fast mode. --fast (API fast: true) runs the same model with faster output, billed by the provider at its higher fast rate: Claude Code's Opus fast mode, Codex's fast service tier, Amp's --fast. Models without it (see --list-models) answer 400 fast_mode_not_supported. fast: false forces standard speed.
Conversations
A conversation is a thread of prompts with the agent's memory: each turn continues the harness's own session (Claude Code --resume, codex exec resume, and the same for the others). A prompt continues the sandbox's current conversation (the most recently prompted one). --new starts another. --conversation ID continues a given one. Each prompt prints its conversation id first.
$ froog prompt my-box --new "look into the flaky CI job" # runs beside the others
$ froog conversations my-box # id, when, prompts, running, agent/model, last prompt
$ froog conversations my-box conv_3f9a… # its turns; --delete removes it
$ froog prompt my-box --conversation conv_3f9a… --agent codex "take it from here"
A conversation runs one turn at a time: a second prompt to it waits as queued. Different conversations run at the same time, up to 4 per sandbox. More wait in the queue. A prompt's status is queued, running, done, failed or interrupted. Switching harness in a conversation gives the new one the earlier turns (prompts and answers, up to 16 KB) in its first prompt. The sessions live in the sandbox's home, so conversations survive a stop, start or fork. A running prompt holds off the auto-stop timer. Turns may run up to --timeout seconds (default 3600, at most 21600). Ctrl+C stops following, not the agent: use froog interrupt.
Steer and interrupt. froog steer my-box "skip the integration tests" (API POST /steer) sends a message to the running turn without stopping it. Claude Code takes it in the middle of the turn (native: true, mode native). The other harnesses have no way to take a message mid-turn, so it is queued as the next turn of the same conversation (mode queued, with its queued_prompt_id). late means Claude Code's turn had just ended. With nothing running it is refused with 409 no_running_turn. froog interrupt my-box (API POST /interrupt) stops every running turn and cancels the queued prompts on the sandbox. --conversation ID stops just one. Files the agent changed stay, and the conversation keeps its memory.
Events. froog events my-box --follow prints what every agent on the sandbox does as it happens (--conversation ID for one). Each event has an increasing id, its prompt_id and conversation_id, and a type: status (data.status, and error on failure), text (content), tool_call (name, input), tool_result (output, is_error), steer, log (a line the harness printed that isn't an event) and error. GET /v1/sandboxes/{name}/events and /prompts/{id}/events answer a JSON page (pass the last id as cursor), or keep streaming with Accept: text/event-stream (server-sent events, resumable with Last-Event-ID) or Accept: application/x-ndjson. A prompt's stream ends after its final status. Events are kept 90 days. With monitoring off only status changes are stored: the rest is streamed live and not kept.
Attachments. --attach FILE (repeatable, API attachments: [{name, content_b64}]) sends images, PDFs or any file: up to 12, 25 MB each, 64 MB in total. They are saved in the sandbox under ~/attachments/<prompt id>/ and their paths are added to the prompt, so every harness reads them from disk (Claude Code opens images and PDFs with its Read tool).
From code. POST /v1/sandboxes/{name}/prompts queues a prompt and answers 202 with it (id, conversation_id, status). Follow it with the events or poll GET /prompts/{id} until done is true (result holds the agent's final answer, error and error_code why it failed). POST /v1/sandboxes/{name}/prompt waits and answers {exit_code, stdout, stderr, timed_out, prompt_id, conversation_id, status}, with the final answer in stdout. Both take prompt, agent, model, effort, fast, new, conversation_id, attachments and timeout_secs. The SDKs have sandbox.prompt(…), queue_prompt, watch_prompt(id), conversations(), steer(…) and interrupt().
Subscriptions
Run Claude Code on a Claude Pro or Max plan and Codex on a ChatGPT plan instead of API keys. In the dashboard, open Agents and click Sign in (or froog subscriptions login claude --sandbox my-box). froog runs the vendor's own sign-in in one of your running sandboxes: claude setup-token shows a claude.ai link, and you paste back the code the page shows. codex login --device-auth shows a link and a one-time code to enter there. The result is stored encrypted like your other secrets: the Claude token as the variable CLAUDE_CODE_OAUTH_TOKEN (valid for a year), the ChatGPT login as the secret file ~/.codex/auth.json. Every sandbox gets it at its next start. Sign out (froog subscriptions logout claude, API DELETE /v1/subscriptions/{provider}) removes it. The sign-in leaves nothing in the sandbox it ran in, and gives up after 15 minutes. froog shows only a link to the vendor's own hosts (claude.ai, claude.com, console.anthropic.com, auth.openai.com, chatgpt.com) and stores only what looks like the vendor's credential (a sk-ant-oat… token, a Codex auth.json). Anything else fails the sign-in. Connections and sign-outs are in the audit log.
Limits: the subscription is used only by the vendor's own CLI in your sandboxes, under the vendor's terms for your plan, and your plan's usage limits apply. Each sandbox gets its own copy of the ChatGPT login and Codex refreshes it there. If Codex reports the login expired, sign in again. Anyone with a shell in a sandbox can read these credentials, like any variable: keep them out of sandboxes other people use (--no-environment). They are never proxied. API: GET /v1/subscriptions, POST /v1/subscriptions/{claude|chatgpt}/login {sandbox}, GET …/login (polls it and stores the result), POST …/login/code {code}.
Computer use
Every agent in a sandbox can see and use its Linux desktop (the one froog desktop shows) through an MCP server named computer, registered at every start for every harness (Claude Code, Codex, Gemini CLI, OpenCode, pi, Prime Agent, Kimi Code, Mistral Vibe, Amp and Cursor):froog prompt my-box "open example.com in Chrome and tell me the page title". Open froog desktop to watch it work.
| Look | screenshot (the desktop or one window), get_window_state (one window's screenshot plus its accessibility tree), get_screen_size, list_windows |
| Point and type | click, double_click, right_click, move_cursor, drag, scroll, type_text, press_key (e.g. ctrl+l), hotkey (e.g. ["ctrl", "s"]), set_value |
| Apps and windows | launch_app, kill_app, bring_to_front, set_window_frame, invoke_menu (e.g. File > Save As), close_window |
| Browser | browser_navigate, get_browser_state, browser_click, browser_type, browser_download |
| Clipboard | clipboard_read, clipboard_write |
The desktop starts on the first tool call if it isn't running. All agents on a sandbox share one screen, so give parallel GUI tasks their own sandboxes. The server is ~/.froog/computer/mcp.py (plain Python, stdio), with cdp.py and a11y.py next to it. froog writes the computer entry into each harness's config at every start and leaves your other MCP servers alone. For pi it's mcpServers.computer in ~/.pi/agent/mcp.json with "exposure": "direct", so its tools reach the model like built-in ones. An exposure you set there stays. Prime Agent (mcpServers.computer in ~/.prime/agent/settings.json) reaches MCP servers from its Python kernel, not as tools of their own: its model calls await mcp.call_tool("computer", "screenshot", {}). For Kimi Code it's ~/.kimi-code/mcp.json, and for Mistral Vibe an [[mcp_servers]] entry in ~/.vibe/config.toml (its tools are named computer_screenshot and so on). A computer server you defined in Vibe's config yourself stays. Screenshots are large in a model's context: ask for one window, or use get_browser_state and get_window_state, which return text.
The browser. The browser tools drive Google Chrome (Chromium on Omarchy) on the desktop over the Chrome DevTools Protocol, on port 9222 of 127.0.0.1. The first browser tool starts Chrome if it isn't open, with its own profile, ~/.config/froog-chrome, so sign-ins stay through stops, snapshots and forks. (Chrome opens the port only for a profile other than its default one.) get_browser_state gives the page's URL, title, open tabs and its links, buttons and fields, each with a number. browser_click and browser_type take that number, or a CSS selector, or the element's visible text. The numbers change with each get_browser_state. Clicks and typing go into the page itself, so they work while Chrome's window is behind another one. browser_download fetches a URL, or the file a link or button starts, with the browser's cookies, saves it to ~/Downloads (or directory) and returns the path. It waits up to timeout_seconds (default 120). The port can't be reached from outside the sandbox, but any program in the sandbox can use it, and with it the profile's sign-ins.
Accessibility tree. get_window_state returns a window's screenshot and its accessibility tree (AT-SPI): each element's role, name, value, state and position, with a ref like 0.3.1. Without a window it uses the one with focus. screenshot: false leaves the picture out, and max_nodes (default 300) caps the list. click with a ref, set_value (a text field, slider, spin box or list item) and invoke_menu act through accessibility, so they need no focus and no coordinates. GTK apps (most of XFCE), Qt apps and LibreOffice have a tree. Chrome and Electron apps have one only when started with --force-renderer-accessibility (for web pages, use the browser tools), and Wine apps have none. A program started before the desktop had accessibility on (desktops started before this release) has none until it is restarted. On Ubuntu and Debian a window's screenshot brings the window to the front.
Input without focus. By default, clicks and keys go to the screen like a person's, to whatever window is in front. Give click, double_click, right_click, scroll, type_text, press_key or hotkey a window and that window is brought to the front for the action, and the window that had focus gets it back afterwards. Add "delivery_mode": "background" and the events go to that window directly (X11 synthetic events), without raising or focusing it and without moving the pointer. Many programs ignore synthetic events: Chrome and Firefox, GTK apps (Mousepad, tested) and terminals (XFCE Terminal, tested, xterm unless told otherwise). Classic X programs take them (xedit, tested). The tool can't tell, so check with a screenshot. Omarchy (Wayland) has no background input: the tools answer with an error. Two focus-free ways always work: the browser tools for web pages, and refs from get_window_state.
set_window_frame moves and resizes a window, in screen pixels (on Omarchy it floats the window first, and says so when Hyprland didn't place it). kill_app stops a program by pid, by window or by its exact process name: SIGTERM, then SIGKILL after grace_seconds (default 3). It stops only the sandbox user's programs, and never the agent that calls it.
Customize the agents
Every harness reads its own config from the sandbox's home directory, and froog prompt runs from ~. froog adds three things: the froog skill (what the machine is, how to run servers and long jobs) in each harness's skills folder, a short system prompt for Claude Code, pi and Prime Agent saying they run headless in a sandbox, and the computer MCP server (above). You add the rest:
| Harness | House rules | MCP servers | Skills |
|---|---|---|---|
| Claude Code | ~/CLAUDE.md, ~/.claude/CLAUDE.md | claude mcp add --scope user NAME -- CMD | ~/.claude/skills/NAME/SKILL.md |
| Codex | ~/AGENTS.md, ~/.codex/AGENTS.md | codex mcp add NAME -- CMD | ~/.codex/skills/ |
| Gemini CLI | ~/GEMINI.md, ~/.gemini/GEMINI.md | mcpServers in ~/.gemini/settings.json | ~/.gemini/skills/, ~/.agents/skills/ |
| OpenCode | ~/AGENTS.md, ~/.config/opencode/AGENTS.md | mcp in ~/.config/opencode/opencode.json | ~/.config/opencode/skills/ |
| pi | ~/AGENTS.md, ~/.pi/agent/AGENTS.md | pi mcp add NAME -- CMD, or mcpServers in ~/.pi/agent/mcp.json | ~/.pi/agent/skills/ |
| Prime Agent | ~/AGENTS.md, ~/.prime/agent/AGENTS.md | prime-agent mcp add NAME -- CMD, or mcpServers in ~/.prime/agent/settings.json | ~/.prime/agent/skills/ |
| Kimi Code | ~/AGENTS.md, ~/.kimi-code/AGENTS.md | ~/.kimi-code/mcp.json | ~/.kimi-code/skills/, ~/.agents/skills/ |
| Mistral Vibe | AGENTS.md in a trusted git repository | [[mcp_servers]] in ~/.vibe/config.toml | ~/.vibe/skills/, ~/.agents/skills/ |
| Amp, Cursor | ~/AGENTS.md | see their docs | ~/.agents/skills/ (Amp) |
One ~/AGENTS.md plus one ~/CLAUDE.md covers almost every harness. To give every new sandbox the same rules, put them in your account's secret files, and register MCP servers in the setup script:
$ froog environment add-file AGENTS.md ./house-rules.md
$ froog environment add-file CLAUDE.md ./house-rules.md
A sandbox has no route back to your computer. froog forward NAME PORT --reverse opens one over SSH: a port on your computer answers at 127.0.0.1:PORT inside the sandbox while the command runs (LOCAL:SANDBOX to use another port). Use it for an MCP server or a local model:
$ froog forward my-box 7777 --reverse # leave running: your MCP server on localhost:7777
$ froog exec my-box -- 'claude mcp add --scope user mine --transport http http://127.0.0.1:7777/mcp'
$ froog forward my-box 11434 --reverse # Ollama, for any harness that takes a base URL
--bind ADDR with --reverse sets the address the port listens on inside the sandbox (default 127.0.0.1, anything else needs GatewayPorts clientspecified in the sandbox's /etc/ssh/sshd_config). Both directions reconnect when the tunnel drops, for example after froog stop and froog start bring the sandbox back on a new address: froog forward asks the API for the current address and port, says so when they changed, and tries up to 5 times in a row (waiting 2 to 30 seconds between tries) before it gives up. A failure in the first 5 seconds, like a local port already in use, is reported at once and not retried.
You can also run your agent on your own machine and give it the sandbox as a tool: froog exec and the HTTP API return stdout, stderr and the exit code.