Run Crew on several remote hosts — dev boxes, EC2 instances, home servers — and drive them all from one hub gateway. Multi-instance opens SSH tunnels to each remote, mints short-lived dashboard tokens, and embeds each remote dashboard in a tab strip. The most-recently-used instances stay warm; the rest reconnect on demand.
Opt-in. Off by default.
A remote Crew chat is separate from opening a remote dashboard. Enable multi-instance support in Configuration, then turn on Settings → Developer → Feature Previews → Chat on a crew.
Open the new-chat menu in the sidebar and choose New chat on crew, then choose a connected crew. The transcript stays on your local Crew while each turn runs on the selected crew.
Remote sessions show the server badge and the crew name in the sidebar, so you can identify where their turns run. Crew reconnects automatically on app load and when the tab regains focus. To change that behavior, open Settings → Remote Instances → Auto-connect crews.
kirocrew config set instances.enabled true kirocrew restart
When enabled, the gateway:
SshTunnelManagerframe-src relaxation to the active loopback tunnel ports (so embedded remote dashboards can render — they're otherwise blocked by the strict frame-src 'self' blob:)With the flag off, /api/instances/* returns 403 and the /instances page shows an opt-in hint.
┌── Hub gateway (this host) ──┐ │ /instances page (React) │ │ ├─ tab strip: Home · A · B │ │ ├─ warm <iframe>s per tab │ http://127.0.0.1:<local_port>/?token=… │ └─ Manage panel │ add / connect / diagnose / restart / remove │ │ │ instances/ package │ │ ├─ registry │ ~/.kiro/crew/instances.json │ ├─ port_allocator │ loopback ports from base 7778 │ ├─ token_mint │ ssh <host> kirocrew token → JWT (never logged) │ ├─ ssh_tunnel_manager │ supervised ssh -N -L, probe, self-heal, refresh │ └─ diagnostics │ ssh → remote-dashboard → local-forward ladder └──────────────────────────────┘ │ ssh -N -L 127.0.0.1:<local>:127.0.0.1:<remote> ▼ ┌── Remote gateway ────────────┐ │ kirocrew gateway bound to │ │ 127.0.0.1:<remote_port> │ └──────────────────────────────┘
Each instance you add produces a supervised ssh -N -L child. The tunnel forwards 127.0.0.1:<local> → remote 127.0.0.1:<remote>. The hub mints a dashboard token on the remote over SSH and embeds the remote dashboard in an iframe at the loopback URL.
POST /api/instances/{id}/connect allocates a loopback port, mints a token on the remote over SSH, starts ssh -N -L, waits until the local forward accepts a connection, then returns the live statusinstances.warm_set_cap to a positive value for a fixed limit. Warm instances keep a live tunnel + WebSocket. Connecting beyond the cap lazily evicts the least-recently-used?diagnose=1 runs a failure-probe ladder on demand; POST .../restart restarts the remote gateway over SSHOpen Instances in the dashboard, click Add, fill in:
| Field | Description |
|---|---|
| Name | Any label (e.g. "Dev box 1") |
| SSH host / alias | What you'd type after ssh — see below |
| Remote port | Remote gateway's port (default 7777) |
| Token TTL | Default 20 h |
The ssh_host field accepts:
dev-1.example.com)ec2-user@10.0.1.5)my-ec2)Any option starting with - is rejected (injection guard).
The only per-remote knob is ssh_host. Anything ssh can reach non-interactively works.
Use your SSH config alias or user@hostname. As long as a key in your ssh-agent covers auth, BatchMode succeeds without prompting.
Configure an SSH alias in ~/.ssh/config on the hub, then reference the alias:
Host my-ec2 HostName ec2-1-2-3-4.compute-1.amazonaws.com User ec2-user IdentityFile ~/.ssh/my-key.pem # Optional: reach a private instance through a bastion ProxyJump bastion-host # Or via SSM Session Manager: # ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters portNumber=%p"
Then add an instance with my-ec2 as the SSH host.
Prerequisites on the hub:
ssh-agent holding it (BatchMode won't prompt)kirocrew installed and a gateway running on the EC2 instance's loopback port| Need | Status | How |
|---|---|---|
| Custom login user | ✅ | user@host or ssh-config User |
| FQDN / IP | ✅ | direct ssh_host value |
Identity file (-i) | ⚠️ via ssh config only | IdentityFile in a Host block |
| Non-22 SSH port | ⚠️ via ssh config only | Port in a Host block |
| Bastion / ProxyJump | ⚠️ via ssh config only | ProxyJump / ProxyCommand |
| SSM-only instances | ⚠️ via ssh config only | ProxyCommand with aws ssm start-session |
The Manage panel gives you these per-instance actions:
Every route in /api/instances/* is gated by _guard():
request["user"] (authenticated dashboard session)instances.enabled: trueBeyond the guard:
ssh -N -L 127.0.0.1:<local>:127.0.0.1:<remote>ssh is always invoked with an argv list; ssh_host cannot inject local shell syntaxssh_host and remote_bin rejected if they contain shell metacharacters or lead with -event.origin against the exact http://127.0.0.1:<port> of a currently-warm tunnel before trusting an unread-count messageFor instances.enabled and the other multi-instance settings, see Configuration.
| Symptom | Fix |
|---|---|
/instances shows "multi-instance management is off" | instances.enabled is false — set it and restart |
| Iframe is blank | CSP frame-src relaxation only applies to active tunnel ports; ensure the instance is connected |
| Connect fails with SSH auth error | Refresh your SSH credentials (re-add key to ssh-agent); tunnels self-heal once SSH is restored |
| Connect fails | Use Diagnose — the ladder reports the first broken link (ssh_unreachable, remote_down, or tunnel_down) |
| Instance keeps dropping | Health probe + 2-tier self-heal retry over ~2 min; if it gives up, diagnosis runs automatically. Check the remote gateway and SSH stability |
| An instance silently disappeared from the warm set | LRU-evicted (warm set full). Raise instances.warm_set_cap or reconnect on demand |
Multi-instance