runo materializes, for each git branch, a remote environment on an AWS EC2
VM: your code, services, a seeded database and a public URL — with your coding
agent (claude/codex) running on the VM. Your laptop stays out of the
execution path: with two environments up, local docker ps is empty.
runo new checkout-fix # branch task/checkout-fix + remote env (~1.5min with pool)
runo agent claude # Claude Code ON the VM — close the lid, it keeps working
runo url --open # see the app running on its public URL
runo validate # lint/tests on the VM + downloadable evidence
runo ship "feat: ..." --validate --destroy # pull → commit → push → PR with evidence, then teardownProvider v1 is AWS EC2, behind a RuntimeProvider interface — nothing
outside src/provider/aws.ts knows about AWS, by design.
git clone https://github.com/kodustech/runo.git
cd runo && ./install.shinstall.sh installs bun if missing, links the runo binary and runs
runo setup — a guided check of every prerequisite (tooling, AWS or
control-plane access, agent credentials validated against the API, recipe,
speed-ups), telling you exactly what to fix and fixing what it can. Run
runo setup again anytime; it exits non-zero while something is missing.
Local prerequisites:
- Bun ≥ 1.3, git, ssh/scp/rsync (macOS/Linux have them)
- Valid AWS credentials (
aws sts get-caller-identitymust answer) - Agent credentials:
CLAUDE_CODE_OAUTH_TOKEN(subscription — runclaude setup-tokenonce on your laptop) orANTHROPIC_API_KEY/OPENAI_API_KEY, via env vars or~/.kodus/agent.env(KEY=valueper line, chmod 600). Your laptop's interactive OAuth login does not travel to the VM.
Environment variables:
| Var | Default | Effect |
|---|---|---|
RUNO_HOME |
~/.runo |
state root (registry, worktrees, SSH keys). Parallel installs MUST use distinct RUNO_HOMEs |
RUNO_AWS_REGION |
sa-east-1 |
EC2 region (us-east-1 is ~40% cheaper if latency is acceptable) |
RUNO_DEBUG |
— | 1 prints stack traces |
| Command | What it does |
|---|---|
runo setup |
guided doctor: checks tooling, AWS/control-plane access, agent credentials (validated against the API), recipe; fixes what it can |
runo init [--force] |
inspects the repo and proposes .kodus/workspace.yaml |
runo new <name> |
branch task/<name> + worktree + runo up |
runo up [--branch B] [--here] |
materializes the remote env (idempotent: existing → resume/reconcile); --here uses the CURRENT working tree as sync anchor (Orca/worktree tools) |
runo agent <claude|codex> [args…] |
agent session ON the VM in tmux (Ctrl+B D detaches without killing it; running again reattaches), cwd in the repo, auth injected |
runo validate [step] |
runs validate: on the VM; downloads JSON+MD evidence; exit ≠ 0 on failure |
runo pull |
rsync VM → local worktree (commit/push happen locally) |
runo ship ["msg"] [--validate] [--destroy] |
the finish flow in one command: pull (tolerant of suspended envs) → commit → push → open the PR via gh. --validate only ships green and embeds the evidence in the PR body; --destroy tears the env down after |
runo push [--restart] |
rsync local worktree → VM; --restart restarts run: services (compose dev with watch hot-reloads by itself) |
runo url [--open] |
public URL of the public service (current IP + port) |
runo tunnel [port…] |
forwards localhost:<port> → VM (frontends behave exactly like local dev); defaults to the recipe's public ports |
runo logs [service] [-f] |
remote logs per service |
runo exec -- <cmd> |
arbitrary command on the VM, cwd in the repo |
runo ls |
envs: branch, state, URL, uptime, instance |
runo suspend / runo resume |
stop/start the EC2 instance (stopped costs no compute; the IP changes and runo redetects it) |
runo bake [--rm] |
bakes an AMI with provisioning done (~10-12min, once): subsequent runo up boot in ~1-2min instead of ~6. --rm removes image/snapshot |
runo pool [n] |
warm pool: n provisioned, stopped instances (EBS only, ~US$3/mo each); runo new claims one — adjusts type/disk while stopped and starts it (~40-60s). No arg shows status; 0 drains |
runo destroy [--all] |
terminates the instance (EBS included), removes worktree and registry; --all also removes keypair + SG |
version: 1
setup:
- bun install
files:
copy: [.env] # untracked files copied from the local working tree to the VM
services:
db:
image: postgres:16 # image: → docker compose generated by runo
port: 5432
env: { POSTGRES_PASSWORD: runo }
api:
run: bun run dev # run: → process on the VM (tmux), logs in ~/.runo/logs/
port: 3000
public: true # opens the port on the SG → http://<ip>:3000
health: /health # HTTP health check performed FROM OUTSIDE (validates SG + service)
data:
migrate: bun run db:migrate
seed: bun run db:seed
validate:
- name: lint
run: bun run lint
- name: test
run: bun test
limits:
instance: t3.medium
disk: 30gb
idle_suspend: 10m # default 10m; "2h" or "off" — idle auto-suspend
spot: true # ~70% cheaper; AWS interruption = stop (EBS survives,
# runo resume restarts). Automatic on-demand fallback.
# Spot skips the warm pool and hibernation.Passthrough mode (repos with their own compose). Multi-file overlays,
interpolation env (with ${RUNO_PUBLIC_IP} / ${RUNO_PUBLIC_IP_DASHED}
substituted at up time — dashed form for nip.io-style wildcard DNS) and an
explicit public_port are supported:
services:
compose:
files: [docker/compose.yml, docker/compose.preview.yml]
profiles: [back, front]
env: { PREVIEW_DOMAIN: "${RUNO_PUBLIC_IP_DASHED}.nip.io" }
public: gateway
public_port: 80Git submodules are shipped automatically (recursively): each submodule's content is archived from an already-populated checkout at the exact commit the superproject records — fully offline, still tracked-files-only.
Simple passthrough (single compose file):
version: 1
setup:
- pnpm install
files:
copy: [.env]
services:
compose:
file: docker-compose.dev.yml
profiles: [] # optional; the repo's compose runs AS-IS on the VM
public: kodus-api # service whose published port becomes the public URL
health: /health/simple # optional; without it the check is TCP
health_timeout: 1200 # heavy apps need more than the 120s default on first boot
data:
migrate: pnpm run migration:run
seed: pnpm run seed
validate:
- name: lint
run: pnpm run lint
limits:
instance: m7i-flex.xlarge # non-burstable: heavy builds never throttle on CPU credits
disk: 100gbservices.compose is mutually exclusive with image:/run:. Since the VM is
dedicated to the env (exclusive Docker daemon), fixed container_names and
published ports from the repo cannot collide by construction — no DinD, no
compose overrides.
If another tool already owns your worktrees (Orca, git worktree by hand),
skip runo new entirely: run runo up --here inside the worktree and that
directory becomes the env's sync anchor — runo push/pull sync it, and
runo destroy terminates the instance but never touches the directory.
Orca post-create hook (one line — every new worktree gets a remote env):
# copy untracked requirements first if your recipe needs them, e.g.:
# cp ~/dev/my-repo/.env .
runo up --here- Agent ON the VM (
runo agent claude|codex) — the brain runs there, in a tmux session: watch it live in your terminal, detach (Ctrl+B D or close the laptop) and the agent keeps working;runo agentreattaches with scrollback. Subscription auth viaclaude setup-token(CLAUDE_CODE_OAUTH_TOKEN) or an API key. - Local agent driving the VM — your local agent (with your subscription)
treats the env's worktree as a regular local repo: edit files →
runo push [--restart]→ change live on the public URL →runo validate/logs/execto verify. Zero credentials in the cloud.
- One EC2 instance per env (Ubuntu 24.04 resolved via SSM Parameter Store;
cloud-init installs docker+compose, node 22, bun, pnpm, tmux, rsync and the
claude/codexCLIs). Tags:Name=runo-<hash6>-<slug>,runo:env,runo:managed=true,runo:home-hash. - Code travels via
git archive HEAD(tracked files only) +files.copyfor untracked files (.env).git init + commiton the VM so the agent can use git there. Zero git credentials in the cloud — changes come back viaruno pull(rsync); commit/push happen on your laptop. - One local worktree per env (
$RUNO_HOME/worktrees/<repo>-<slug>) is the sync anchor: upload source, pull destination. Branch already checked out in your main working tree → clear error suggestingruno new. - Agent auth: env vars →
~/.kodus/agent.env(global, independent of RUNO_HOME). Credentials travel via a chmod-600 file over scp, never via argv/logs. - Health checks from outside: HTTP 200 on
health:(or TCP) against the public IP — validates the security group and the service in one shot. Failure failsruno upwith the service's log tail. - Suspend/resume: a stopped instance costs no compute (EBS only, cents/day). EBS survives stop/start — database data persists. The public IP changes on every resume; runo redetects and updates registry/URL.
- Idle auto-suspend (
limits.idle_suspend, default 10m): a watchdog ON the VM (1/min cron) shuts the instance down after N minutes without activity — internal shutdown becomes "stopped" (= suspend). Activity = established SSH session, load ≥ 0.20, network traffic (>100KB/min — includes someone using the public URL) or CPU from claude/codex processes (a detached agent that is working counts as active; an agent idle at the prompt does not). - Baked image (
runo bake): a temporary VM runs full cloud-init,cloud-init clean, stop,CreateImage.runo upthen uses that AMI (no heavy user-data) and cold boot drops from ~6min to ~1-2min. Idle cost: just the snapshot (~US$1.4/mo for 30GB). Re-baking replaces the previous image. - Warm pool (
runo pool <n>): pre-provisioned instances wait STOPPED.runo newclaims one: retag → adjust instance type (ModifyInstanceAttribute) and grow the disk (ModifyVolume+ growpart on boot) if the recipe asks for more →StartInstances. Env ready in ~40-60s. The cost guardrail only counts RUNNING instances. - Hibernation: envs launch with
HibernationOptions+ encrypted root.runo suspendhibernates (RAM → EBS);runo resumeprobes the services first and, if they answer (hot return), restarts nothing — the app comes back in the exact state it was. Graceful fallback to plain stop/boot when hibernation is unavailable. Idle auto-suspend uses plain stop (the decision comes from inside the VM, which has no AWS credentials — by design). - Spot (
limits.spot: true): persistent spot instance with interruption behavior stop — an AWS interruption is, in practice, an unplanned auto-suspend (EBS intact,runo resumerestarts; IP changes as always).runo destroycancels the spot request BEFORE terminating (otherwise AWS would launch a replacement).runo lsmarks(spot). Reference: t3.xlarge sa-east-1 ~US$0.08-0.11/h (vs 0.27 on-demand). - Local registry:
$RUNO_HOME/envs.json. - Cost guardrails: max 3 RUNNING instances per install (scoped by the
runo:home-hashtag); everything taggedruno:managed=true.
| Operation | Time |
|---|---|
runo push (hot loop: edit → live) |
~1-3s |
runo new with warm pool + baked AMI |
~1m30 to green health |
runo new cold (no bake/pool) |
~6-7min |
runo resume after hibernation |
~1min, services already hot |
runo suspend (hibernating 4GB RAM) |
~1m30 |
runo bake (one-time) |
~11min |
For solo use, the CLI talks to AWS directly with your local credentials. For teams, run runo-server: it holds the AWS credentials and the SSH keys; developers authenticate with a token and need zero cloud credentials.
dev laptop (CLI, no AWS creds) runo-server (control plane)
RemoteProvider ─── HTTP/WS ───► token auth → EC2 provider + SSH keys
RUNO_SERVER + RUNO_TOKEN per-user env registry (platform view)
Operator (platform team), on a machine that has AWS credentials:
RUNO_HOME=~/.runo-server \
RUNO_SERVER_TOKENS="alice:tok-a,bob:tok-b" \
bun server/main.ts --port 7777Developers:
export RUNO_SERVER=https://runo.internal.example.com
export RUNO_TOKEN=tok-a
runo new my-task # same CLI, same flow — AWS stays server-side- Every provider operation (create/suspend/exec/upload/…) goes through the server; long steps stream live output back to the terminal.
- Ingress — one hostname per env (
RUNO_INGRESS_DOMAIN=envs.example.com- wildcard DNS
*.envs.example.com→ the server): every env getshttp://<slug>.envs.example.com, extra services athttp://<service>--<slug>.envs.example.com. Solves parallel envs (no port collisions), shareable links, and frontend API-base assumptions. Suspended env → friendly 503 telling you toruno resume. v0 proxies HTTP only (no WebSocket/HMR — useruno tunnelfor that); terminate TLS in front.
- wildcard DNS
- Web panel: open
http://<server>:7777/in a browser, paste your token — live list of every env (owner, repo@branch, instance, state, public IP) with auto-refresh and a destroy action. Same port, nothing else to deploy. GET /v1/envs(?live=1for instance state) is the same data as JSON.- The recipe stays committed in each repo: platform team writes it once by PR, devs never touch infra config.
runo agentover the control plane uses an experimental WebSocket TTY tunnel; headless usage (runo agent claude -- -p …) works everywhere.- Org policies (
$RUNO_HOME/policies.yamlon the server, hot-reloaded — seeserver/policies.example.yaml): instance-type allowlist, max disk, max envs per user, org-wide running ceiling, and an env TTL with automatic destruction (sweeper at startup + every 10min). The recipe in each repo decides the machine for that workload; policies are the platform team's ceilings on what any recipe/user may ask for. Violations failruno newinstantly with an actionable message — before anything touches the cloud.GET /v1/policiesshows the active policy set. - v0 is single-process with token auth and JSON state — put TLS/VPN (ALB, Caddy, Tailscale) in front before exposing it beyond localhost. SSO and a richer panel are the next iterations.
- The public URL is IP:port, no TLS, no auth. Anyone who finds the IP can
reach the service. Therefore: ALWAYS use a development
.env— never production credentials. DNS/TLS/auth come in later phases. - The SG opens 22 and the public ports to
0.0.0.0/0. The SSH key is an ed25519 generated by runo ($RUNO_HOME/ssh/), never reused. - Agent API credentials live on the VM at
~/.runo/agent.env(600) while the env exists;runo destroytakes the EBS with it (DeleteOnTermination).
Downloaded to <worktree>/.kodus/evidence/<sha>.json + <sha>.md + logs/:
a schema with repo/branch/sha/env/startedAt/finishedAt/status/steps[]/urls —
the future contract with Kody (review-time runs the SAME validation).