diff --git a/public/blog/banners/a-browser-terminal-on-a-ros-2-robot-without-a-vpn-and-why-it-should-be-read-only.svg b/public/blog/banners/a-browser-terminal-on-a-ros-2-robot-without-a-vpn-and-why-it-should-be-read-only.svg new file mode 100644 index 00000000..93944960 --- /dev/null +++ b/public/blog/banners/a-browser-terminal-on-a-ros-2-robot-without-a-vpn-and-why-it-should-be-read-only.svg @@ -0,0 +1,8 @@ + diff --git a/public/blog/banners/a-read-only-terminal-link-for-a-training-run-you-check-from-your-phone.svg b/public/blog/banners/a-read-only-terminal-link-for-a-training-run-you-check-from-your-phone.svg new file mode 100644 index 00000000..ec741d9f --- /dev/null +++ b/public/blog/banners/a-read-only-terminal-link-for-a-training-run-you-check-from-your-phone.svg @@ -0,0 +1,8 @@ + diff --git a/public/blog/banners/connect-an-ai-agent-to-a-live-terminal-without-giving-it-the-machine-shell-onlin.svg b/public/blog/banners/connect-an-ai-agent-to-a-live-terminal-without-giving-it-the-machine-shell-onlin.svg new file mode 100644 index 00000000..b28d9df9 --- /dev/null +++ b/public/blog/banners/connect-an-ai-agent-to-a-live-terminal-without-giving-it-the-machine-shell-onlin.svg @@ -0,0 +1,8 @@ + diff --git a/public/blog/banners/self-hosted-terminal-sharing-on-one-docker-box-the-shell-online-relay-without-a.svg b/public/blog/banners/self-hosted-terminal-sharing-on-one-docker-box-the-shell-online-relay-without-a.svg new file mode 100644 index 00000000..deb9b9fa --- /dev/null +++ b/public/blog/banners/self-hosted-terminal-sharing-on-one-docker-box-the-shell-online-relay-without-a.svg @@ -0,0 +1,8 @@ + diff --git a/public/blog/banners/share-a-terminal-session-in-the-browser-without-handing-over-the-filesystem-shel.svg b/public/blog/banners/share-a-terminal-session-in-the-browser-without-handing-over-the-filesystem-shel.svg new file mode 100644 index 00000000..914203be --- /dev/null +++ b/public/blog/banners/share-a-terminal-session-in-the-browser-without-handing-over-the-filesystem-shel.svg @@ -0,0 +1,8 @@ + diff --git a/public/blog/banners/vs-code-1-137-runs-agent-sessions-in-their-own-process-terminal-sharing-without.svg b/public/blog/banners/vs-code-1-137-runs-agent-sessions-in-their-own-process-terminal-sharing-without.svg new file mode 100644 index 00000000..c19fa886 --- /dev/null +++ b/public/blog/banners/vs-code-1-137-runs-agent-sessions-in-their-own-process-terminal-sharing-without.svg @@ -0,0 +1,8 @@ + diff --git a/src/data/blogPosts.json b/src/data/blogPosts.json index 71de0c30..d0110fa9 100644 --- a/src/data/blogPosts.json +++ b/src/data/blogPosts.json @@ -1467,5 +1467,65 @@ "tags": ["shell-online", "terminal-sharing", "end-to-end-encryption", "oauth", "team-collaboration"], "banner": "banners/team-terminal-sharing-sealed-so-the-service-cant-open-it.svg", "iso_date": "2026-09-09" + }, + { + "slug": "vs-code-1-137-runs-agent-sessions-in-their-own-process-terminal-sharing-without", + "title": "VS Code 1.137 Runs Agent Sessions in Their Own Process. Terminal Sharing Without SSH or VPN Does That for a Failing Build", + "description": "How to pair-debug a failing build with terminal sharing without SSH or VPN, and how that compares to the Agent Host in VS Code 1.137.", + "date": "Sep 10", + "category": "Guide", + "tags": ["shell-online", "terminal-sharing", "vscode", "pair-debugging", "ci"], + "banner": "banners/vs-code-1-137-runs-agent-sessions-in-their-own-process-terminal-sharing-without.svg", + "iso_date": "2026-09-10" + }, + { + "slug": "a-read-only-terminal-link-for-a-training-run-you-check-from-your-phone", + "title": "A Read-Only Terminal Link for a Training Run You Check From Your Phone", + "description": "Share a training run with a read-only terminal link: one command, a phone-sized grid, relay-enforced no-input, E2EE, and what happens when the network drops.", + "date": "Sep 11", + "category": "Guide", + "tags": ["shell-online", "read-only-terminal", "training-runs", "remote-monitoring", "mobile-terminal"], + "banner": "banners/a-read-only-terminal-link-for-a-training-run-you-check-from-your-phone.svg", + "iso_date": "2026-09-11" + }, + { + "slug": "self-hosted-terminal-sharing-on-one-docker-box-the-shell-online-relay-without-a", + "title": "Self-Hosted Terminal Sharing on One Docker Box: the shell.online Relay Without a Cloudflare Account", + "description": "shell.online v0.12.2 adds self-hosted terminal sharing: a standalone relay on any Docker host, Caddy TLS, no Cloudflare account, same E2EE.", + "date": "Sep 12", + "category": "Operations", + "tags": ["shell-online", "self-hosting", "docker", "terminal-sharing", "end-to-end-encryption"], + "banner": "banners/self-hosted-terminal-sharing-on-one-docker-box-the-shell-online-relay-without-a.svg", + "iso_date": "2026-09-12" + }, + { + "slug": "share-a-terminal-session-in-the-browser-without-handing-over-the-filesystem-shel", + "title": "Share a Terminal Session in the Browser Without Handing Over the Filesystem: shell.online v0.14 File Access", + "description": "shell.online v0.14 adds opt-in, rooted file access, so you can share a terminal session in the browser and open only the files you chose.", + "date": "Sep 13", + "category": "Security", + "tags": ["shell-online", "terminal-sharing", "coding-agents", "sandbox", "end-to-end-encryption"], + "banner": "banners/share-a-terminal-session-in-the-browser-without-handing-over-the-filesystem-shel.svg", + "iso_date": "2026-09-13" + }, + { + "slug": "connect-an-ai-agent-to-a-live-terminal-without-giving-it-the-machine-shell-onlin", + "title": "Connect an AI Agent to a Live Terminal Without Giving It the Machine: shell.online v0.15 Agent Handoffs", + "description": "How to connect an AI agent to a live terminal with shell.online v0.15: a revocable invitation, explicit completion evidence, and a process that stays yours.", + "date": "Sep 14", + "category": "Engineering", + "tags": ["shell-online", "ai-agents", "terminal", "refstream", "openai"], + "banner": "banners/connect-an-ai-agent-to-a-live-terminal-without-giving-it-the-machine-shell-onlin.svg", + "iso_date": "2026-09-14" + }, + { + "slug": "a-browser-terminal-on-a-ros-2-robot-without-a-vpn-and-why-it-should-be-read-only", + "title": "A Browser Terminal on a ROS 2 Robot Without a VPN, and Why It Should Be Read-Only by Default", + "description": "How to get a browser terminal on a ROS 2 robot with shell.online: one command, no VPN, read-only links, reconnects, and persistent URLs across reboots.", + "date": "Sep 15", + "category": "Guide", + "tags": ["shell-online", "ros2", "robotics", "remote-access", "terminal-sharing"], + "banner": "banners/a-browser-terminal-on-a-ros-2-robot-without-a-vpn-and-why-it-should-be-read-only.svg", + "iso_date": "2026-09-15" } ] diff --git a/src/pages/blog/a-browser-terminal-on-a-ros-2-robot-without-a-vpn-and-why-it-should-be-read-only.astro b/src/pages/blog/a-browser-terminal-on-a-ros-2-robot-without-a-vpn-and-why-it-should-be-read-only.astro new file mode 100644 index 00000000..82607645 --- /dev/null +++ b/src/pages/blog/a-browser-terminal-on-a-ros-2-robot-without-a-vpn-and-why-it-should-be-read-only.astro @@ -0,0 +1,81 @@ +--- +import BlogLayout from '../../layouts/BlogLayout.astro'; + +const bodyContent = `
Agility Robotics unveiled Digit 5 today, and the part of the announcement that matters to anyone who runs robots for a living is not the 50 lb payload. It is the independent safety controller that "oversees Digit's response to the detection of people within an unsafe distance," and the Agility Arc fleet platform that shows operators uptime, throughput, and mean time between incidents. Fleet-scale humanoids get fleet-scale tooling. The ROS 2 robot on your bench gets whatever you can reach it with. This post is about getting a browser terminal on a ROS 2 robot with shell.online: one command on the robot, one link on your phone, no VPN, and a clear account of what that link does and does not expose.
+ +The timing is not only about Digit. The ROS 2 release team shipped Humble Hawksbill Patch Release 15 yesterday, and the current LTS, Lyrical Luth, has been out since May with Ubuntu 26.04 as its Tier 1 platform. Plenty of fleets are mid-migration between the two, and somebody has to look at the actual terminal on the actual robot when a launch file behaves differently than it did in the sim.
+ +shell.online transports terminal bytes. It does not know what ROS is, and it does not need to. The platforms page says it directly: source the ROS environment first, then wrap roscore, roslaunch, ros2 run, ros2 launch, colcon build, or a node with the normal shell command. No bridge node, no DDS discovery across a NAT. The PTY runs on the robot, and the browser sees what a person at the robot's console would see.
+ +The one caveat, also from that page: ROS itself supports fewer OS and architecture pairs than the CLI does. Sharing a terminal does not make an unsupported ROS combination work.
+Install the CLI on the robot's compute. On Linux, including ARM64 boards, the installer is one line and verifies the SHA-256 checksum of the binary it downloads:
+curl -fsSL https://shell.online/install | sh
+Then source your workspace and wrap the launch exactly as you would run it locally:
+source /opt/ros/lyrical/setup.bash
+source ~/ros2_ws/install/setup.bash
+shell ros2 launch my_robot_bringup bringup.launch.py
+The CLI prints a URL, a generated password, and a QR code. Scan the QR with a phone and the same terminal opens in the browser with the launch output streaming. The encryption page explains what the QR carries: the printed URL holds a random salt fragment, the QR adds the password to that fragment, and URL fragments never reach the relay. The CLI and the browser derive the same AES-256-GCM key locally with PBKDF2-HMAC-SHA256 at 600,000 iterations. Frames are encrypted on the robot before they go anywhere.
+ +The launch runs in the background on the robot. If a colleague is standing next to it with a keyboard, they can take over locally without disconnecting the browser viewers:
+shell list
+shell attach <session-id>
+Ctrl-X then D detaches and leaves the process running. Local input and the resulting output stay visible to everyone in a browser, so nobody is typing into a terminal the other person cannot see.
+This is the part to read before you paste the link into a group chat. The URL and password together are a bearer credential. The security model calls it a bearer capability: anyone holding both can open the share, and on an interactive share they can type with the permissions of the wrapped process. On a robot, the wrapped process is usually running as a user that can publish to cmd_vel, restart nodes, or call an e-stop service. A terminal on a robot is a control surface, and the link is the key to it.
+ +Digit 5 solves the physical version of this with hardware: an independent safety controller that acts regardless of what the main software is doing. The equivalent on the terminal side is deciding, before you share, whether anyone on the far end should be able to type at all. For a robot that a support engineer only needs to observe, share the launch read-only:
+shell --read-only ros2 launch my_robot_bringup bringup.launch.py
+Read-only is enforced at the relay, not in the browser. Per the security page, ordinary browser input and confirmed EOF are rejected by the Worker, and DevTools or handcrafted WebSocket frames cannot turn the link into an interactive session. That matters when the process is a launch file holding a motor controller.
+ +An interactive shell.online link on a robot lets anyone who holds the URL and password send keystrokes with the permissions of the process you wrapped. If the goal is to let someone watch a bringup, a colcon build, or a bag replay, share it read-only. Rotate the password with shell password rotate if a link went further than intended; rotation disconnects current viewers without restarting the process.
One more behavior worth knowing: phone viewers change the terminal grid. The mobile page documents it. A desktop-only session uses a 120 by 36 grid, and the moment a phone connects, everyone switches to an 80 by 24 compatibility grid. Read-only phones count too. Wide output or a TUI will reflow when someone opens the link from a phone.
+Robots lose Wi-Fi. They roam between access points, drive into the metal corner of a warehouse, or reboot when a battery swap goes badly. The reliability page is specific about what happens: the CLI and browser reconnect with bounded backoff, a temporary network failure does not stop the process, and the same share link remains recoverable for 12 hours after its host disconnects. A browser tab left open on your desk reconnects when the robot's compute comes back into range.
+ +A reboot is different, because the process itself is gone. An ordinary share closes when its task exits, and rerunning the launch gives you a new URL and password. For a robot that you want reachable at one stable address across restarts, the CLI has a persistent mode:
+shell --persistent /var/lib/robot/shell-state -- ros2 launch my_robot_bringup bringup.launch.py
+The state file stores an owner-only host identity, the password, and the encryption material. Rerunning with that same file restores the same URL and password. Put that command in the robot's startup script and the link you saved last week still works after a power cycle. The shell.online source ships the same mechanism in its published Docker image, where a named volume plus restart: unless-stopped keeps one encrypted link across container restarts. Two edges: the persistent flag cannot be combined with the no-e2ee flag, and shell kill stops the process without deleting the identity file, so the next run reuses it.
+ +For a session that should not live all afternoon, such as a bag replay for a bug report, add an earlier deadline:
+shell --read-only --auto-close 30m ros2 bag play /data/incident_0915
+The auto-close value can only bring the deadline forward. The share still closes the moment the wrapped process exits, whichever comes first.
+If a coding agent runs on the robot's compute, bisecting a launch-file regression after a Humble patch or babysitting a colcon build, it can start the share itself and hand the operator the link without interactive setup, using the JSON mode in the CLI reference:
+shell --read-only --json -- colcon build --symlink-install
+The first JSON event carries the share URL, the password, the session ID, and flags for read-only and encrypted. The installable agent skill spells out the rules: preserve the full URL including its salt fragment, treat URL and password as a bearer secret, prefer read-only when the operator only needs to monitor, and never create a new session just to refresh a link. On a robot, where a stray keystroke has physical consequences, read-only by default is the right instinct for an agent.
+ +None of this changes where the process runs. The launch, the build, the bag replay all stay on the robot's compute with its own environment sourced. The relay sees connection metadata, encrypted frame sizes, timing, labels, and lifecycle events, and nothing of the terminal content. That is the split we described when we first wrote up a live browser link for any terminal, and a robot is where it earns its keep.
+ +Digit 5 is expected to reach early access in the first half of 2027. Most ROS robots today are not humanoids with a fleet dashboard. They are a compute box, a launch file, and a person who needs to see what it printed, from wherever they are, without a VPN and without handing a control surface to everyone who has the link.
+Source your ROS environment, wrap the launch with one command, and open the same terminal from a phone. End-to-end encrypted by default, read-only when you say so, and the process never leaves the robot.
Try shell.onlineHugging Face shipped TRL v1.13.0 on September 10 with a guide that trains one 1,048,576-token sequence per step on a single 8×H100 node. Qwen3-8B, 380 seconds a step, 56.2 GB per GPU, twelve steps. That is a run you start, walk away from, and want to glance at from the couch or the train. A read-only terminal link is the least-privilege way to do that: the process stays on the training box, nobody gets a shell, and the phone shows the same tqdm bar the launching terminal shows. This post walks through the exact command with shell.online, what the viewer sees, and what the link does and does not grant.
+ +The guide launches its example with accelerate. Prefix that launch with the CLI from shell.online and add the read-only flag before the command. The double dash ends option parsing so accelerate's own flags are never mistaken for ours:
+shell --read-only -- accelerate launch \\
+ --config_file examples/sft_qwen3_8b_1m_context/context_parallel_8gpu.yaml \\
+ examples/sft_qwen3_8b_1m_context/sft_qwen3_8b_1m_context.py
+The wrapped command runs in the background inside a local PTY. The URL prints only after the relay connection exists and the task has survived a short startup handshake, so a launch that dies on a bad config path prints its real exit status and no dead link. In an interactive terminal you also get a QR code whose fragment carries the salted URL and the password together, so a phone opens it with one scan. Piped and JSON output stay machine-clean.
+The launching terminal is now free. Use shell list to see the process uptime and the relay's independent online, reconnecting, expired, or unknown state; unknown means the check was inconclusive, not that the link died. To take over locally, shell attach with the session ID puts you back in the PTY without disconnecting any browser, and Ctrl-X then D detaches again. The CLI reference lists every option and the exit codes.
+The guide notes that the first step lands after ten minutes or so of loading and tokenizing. A viewer who opens the link during that window sees the accelerate startup output and then the progress bar updating in place, because the process is writing to a real PTY and tqdm behaves exactly as it does locally.
+Viewers never own PTY sizing. A session with only desktop browsers attached runs on a 120×36 grid. The moment a phone or portrait viewer connects, the relay switches everyone to an 80-column grid and each browser fits that grid to its own screen; the last portrait viewer leaving restores 120×36. Read-only phones count for this too. The mobile behavior page describes the 80×24 compatibility grid; as of v0.11.2 the CLI advertises support for a taller 80×40 grid in its host handshake header, the relay picks 80×40 when the host supports it, and older CLIs still get 80×24. The negotiation lives in pull request #100, which also added two-finger pinch zoom on the terminal alone, from 50% to 150%, without disturbing anyone else's view.
+For a training run, 80 columns matters. The progress line in the guide is short enough to survive the switch unwrapped:
+{'loss': 4.311, 'grad_norm': 29.25, 'num_tokens': 1049000.0, 'epoch': 0.25}
+ 8%|▊ | 1/12 [06:20<1:09:41, 380.10s/it]
+Single-finger gestures scroll terminal history. If output outruns the network or the phone's rendering, stale display work is dropped and sends one authoritative screen snapshot from a bounded local ring buffer, so a returning viewer sees the current screen rather than a replay of every intermediate redraw. The reliability page covers the bounded queues and write timeouts that keep a high-output process from ever blocking on Cloudflare.
+With the read-only flag, ordinary browser input and the confirmed Ctrl-D EOF are rejected by the Worker. Opening DevTools or hand-crafting WebSocket frames cannot turn the link into an interactive session, and the access mode cannot be weakened after creation. That is the property you want when the link is going to sit in a group chat for an hour: the worst a holder can do is watch.
+Watching is still access. The URL and the eight-character password together are a bearer credential. Anyone holding both can read everything the terminal prints, which for a training run means dataset names, checkpoint paths, loss values, and any secret a careless script echoes. Treat the pair like a token, and rotate by starting a new share.
What the relay sees is the other half of the trust model. Terminal payloads are encrypted on the training box with AES-256-GCM before they enter Cloudflare and decrypted in the browser. The printed URL carries only a random salt in its fragment, which never reaches the server, and the CLI and browser derive the same key locally with PBKDF2-HMAC-SHA256 at 600,000 iterations, the key derivation function specified in RFC 8018. Cloudflare still observes connection IPs, timing, encrypted frame sizes, opcodes, access mode, the command label, and lifecycle events, and it does not persist terminal contents. The end-to-end encryption page is explicit that E2EE cannot hide traffic shape or stop the relay from dropping or delaying a valid frame.
+Eight generated base64url characters give 48 bits of entropy, which is fine for a link that lives as long as one task. For a multi-day run, or one whose logs you would not want indexed, set SHELL_ONLINE_E2EE_PASSWORD to a longer unique value before launching and send it separately from the URL. Derivation still happens on the machine. The security model page lays out the full boundary, including the point that an interactive share types with the wrapped process's operating-system permissions, which is exactly why this workflow uses the read-only mode.
+The link can disappear. The process should not. The CLI and browser reconnect with bounded backoff, a temporary network failure never stops the local task, and the same ordinary share link remains recoverable for 12 hours after its host disconnects. A phone that loses signal in a tunnel comes back to the current screen, because the relay keeps the session and the CLI keeps the process.
+Lifetime is bounded by the task. When accelerate exits, the share closes and its server-side state is deleted; there is nothing to clean up. If you want the link to expire before the run does, add an earlier deadline. Relative units and local times both work:
+shell --read-only --auto-close 2h -- accelerate launch ...
+shell --read-only --auto-close "tomorrow 09:00" -- accelerate launch ...
+An auto-close value can only shorten the lifetime, never extend it past task exit, and a missing or invalid value returns status 2 instead of becoming part of the command. Since v0.11.0 long deadlines are honored rather than silently clipped to the relay's rolling 12-hour lease, which matters for exactly this kind of overnight job.
+Two more cases come up with training runs specifically. First, the guide's run reaches 63.6 GB on an 80 GB card and still fails without the expandable-segments allocator setting. When a run dies like that during the handshake, the CLI reports the status and prints no URL, so a monitoring link never points at a corpse. Second, when you resume from a checkpoint after a crash, the persistent mode keeps the same URL and password across relaunches:
+shell --read-only --persistent ~/.shell-train-state -- accelerate launch ...
+The state file stores an owner-only host identity, password, and E2EE material. Whoever already has the link opens the resumed run without a new credential exchange, and shell kill stops the current process without deleting the identity file. The source repository documents this under platforms and in the CLI help, alongside the Docker image that combines the same mechanism with an automatic restart policy.
+None of this needs an account. Linking a machine with shell login is optional and publishes only the share URL, command name, host name, and timing to the web app, never terminal contents or the password. For a solo training run watched from a phone, the plain command above is the whole setup. The broader idea, a browser link to any terminal process, is covered in the introduction to shell.online.
+One command wraps a training launch in a read-only, end-to-end encrypted link that opens on any phone and closes itself when the job ends.
Try shell.onlineOpenAI opened its Agents API to public beta on September 10. The pitch is a managed Codex harness: you supply files, install packages, and add skills, in the announcement's words, "while we provision and manage the sandbox", with partner sandboxes from Cloudflare, E2B, Modal, and others if you would rather pick the container yourself. That is one answer to the question of where an agent should do its work. Today's shell.online release, v0.15.0, is built for the opposite arrangement. The terminal stays on your machine, with your files and your permissions, and you connect an AI agent to a live terminal through a single-use invitation that you can revoke at any moment without stopping the process.
+ +shell.online has always been one command that prints a URL and a password for a running process. The browser view of that process defaults to xterm.js. Since v0.14.0 there has been an optional second renderer, Refstream, which is still labelled an unstable alpha and which changes only the browser view, never the PTY, the relay permissions, or the encryption. Refstream is where the Connect agent button lives.
+The v0.15.0 changelog lists two additions. First, reusable agent connections: a connected agent keeps its handle for follow-up requests, and task IDs, progress, and collected answers survive panel changes and reconnects. The README now says the agent connects once and follows up without pairing again. Second, tab-local session recovery for page reloads. The snapshot expires after four hours, disappears when the tab closes, and never goes to the relay. Recovery also still requires the local process to be alive, because a snapshot restores parser state and task records, not a shell.
+v0.15.1, released the same morning, bumps the vendored renderer to Refstream v0.1.0-alpha.5 and fixes a real bug in that recovery path: a restored snapshot could answer historical terminal capability queries into the live process, which could corrupt tmux input. The README now states the rule directly: a restored browser snapshot cannot answer historical terminal queries into the live process.
+Start with any process you would normally share. A coding agent is the obvious case, but a dev server or a plain shell works the same way.
+shell claude
+# or
+shell npm run dev
+The CLI prints the share URL, the password, and a QR code. Open the link in a browser, open Terminal controls, and switch the renderer to Refstream (unstable alpha). The choice is saved in that browser only. Then choose Connect agent and pick one of two grants, both documented in the CLI reference: Read terminal, which lets the agent read current output, search retained rows, inspect command boundaries, and wait for changes; or Read and control, which additionally lets it type, execute a command against a recent terminal sequence, and send key combinations such as Ctrl-C.
+What you copy is not a password. It is a private invitation plus a standalone Node.js 22 or newer connector URL with a pinned SHA-256 checksum. The invitation is single-use and expires after five minutes. The agent verifies that file against the checksum, then you pass the invitation through its ordinary command tool, and the agent handoff protocol is explicit that it must arrive on stdin or through the connector's hidden prompt, never in command arguments, URLs, or logs.
+node shell-agent.mjs connect
+# invitation pasted on stdin; returns a sessionId
+
+node shell-agent.mjs request SESSION_ID
+{"method":"ask","args":{"kind":"command","prompt":"pwd","taskId":"workdir-1","expectedSequence":12}}
+
+node shell-agent.mjs request SESSION_ID
+{"method":"wait_task","args":{"taskId":"workdir-1","afterRevision":1,"timeoutMs":15000}}
+
+node shell-agent.mjs request SESSION_ID
+{"method":"collect_task","args":{"taskId":"workdir-1"}}
+The sessionId returned by connect is the handle the agent reuses for every request, for status, and for stop. Each handoff carries a stable task ID chosen by the agent. Retrying the same ID and prompt retrieves the existing task instead of running the command twice, and a new handoff is blocked until the previous answer is collected or explicitly cancelled. Collecting an answer leaves the connection open, which is the whole point of the v0.15.0 change.
+The part of the protocol most worth reading is how a task becomes done. The statuses are waiting, needs_attention, completed, collected, and cancelled, and the protocol states plainly that "a sent receipt, output, silence, a redraw, and a background-job acknowledgement do not establish completion." Every final result records where its completion claim came from, and there are only three sources.
+This matters when the wrapped process is an interactive TUI such as Claude Code rather than a shell that emits markers. A generic TUI cannot be treated as a structured conversation API. If the agent collects before there is completion evidence, the protocol stores an unconfirmed, truncated screen excerpt and leaves the task pending. If the shell emits no OSC 133 markers at all, the Commands view stays empty rather than guessing where a command ended.
+A control grant is not a free keyboard. Every remote write requires an expectedSequence taken from a recent read, so a stale view of the screen is rejected. Local keystrokes are tracked before PTY echo, including IME composition, and any intervening human input removes the agent's ownership of a draft. Pressing Enter requires an agent-owned draft. No draft is ever cleared automatically. For an application whose composer state cannot be read from terminal cells, the agent must either wait, or the owner can inspect the input themselves and click the button labelled "I've checked: input is empty", which is a one-time acknowledgement that further input or output invalidates. Colors and cursor placement are display evidence, never permission to type.
+Two boundaries hold regardless of the grant. Revoking access is immediate and does not send Ctrl-C or stop the shared process, so a runaway agent loses the terminal but your job keeps running. And the invitation grants terminal access only. Files stay unavailable unless the host separately started the share with the --files or --files-root flag, the opt-in described in the security model, and even then access is rooted, traversal and symlink escapes are rejected, and printing a filename never reads the disk.
+The access rules underneath are unchanged. The share URL and password together are a bearer credential: anyone holding both can open the terminal, and unless the share was started with the read-only flag, can type with the permissions of the wrapped process. The agent invitation sits on top of that share, is scoped to this one live terminal, and cannot be replayed once used. The hosted grant lasts up to four hours.
+The relay sees encrypted payloads, connection metadata, frame sizes, and timing, as it does for every shell.online session. Connector state on the agent's disk contains only its protected local IPC credentials, never terminal contents, answers, or the private invitation. Task history is bounded to the last 32 records with answers capped at 32,768 characters, and expired task IDs stay reserved across snapshots so an old retry cannot resubmit a command.
+Refstream is an unstable alpha and is labelled that way in the renderer menu. It is not binary-compatible with the xterm.js addon ecosystem, and some private xterm behaviours are not implemented yet. If a TUI renders or handles input incorrectly, switch the renderer back to xterm.js. The process, relay connection, and permissions do not change when you switch.
The contrast with a hosted sandbox is not about which is safer in the abstract. It is about who holds the process. In a managed harness the provider runs the container and you get files, packages, and artifacts back. With a browser link to a terminal that never leaves your machine, you hold the process, the agent gets a scoped and revocable seat at it, and every claim of "done" carries a label saying how it was established. Both shell.online and the Refstream renderer are MIT licensed, so the grant model described here can be checked against the code rather than taken on trust. If you have not used the basic share before, the introduction to the live terminal link covers the one-command flow this builds on.
+One command shares the process. One invitation lets an agent in. One click revokes it, and the job keeps running.
Try shell.onlineCloudflare's status page logged an incident titled "Cloudflare Tunnel Availability Issues" that opened at 21:55 UTC on September 11 and was marked resolved at 08:57 UTC on September 12, roughly eleven hours later, with the impact rated minor and the affected component listed simply as Tunnel. Cloudflare Tunnel is a common way to reach a home lab or a dev box without opening an inbound port: the cloudflared daemon makes outbound-only connections to Cloudflare's network and traffic to your origin rides through that tunnel. It is a good design, and it has one obvious property. When the relay in the middle has a bad night, so does your access path. That is the honest framing for the feature shell.online shipped today in v0.12.2: self-hosted terminal sharing through a relay you run yourself, on an ordinary Docker host, with no Cloudflare account involved.
+ +To be clear about what this post is not claiming: we have no evidence the hosted shell.online relay was affected by that incident, and Cloudflare's status page publishes no detail beyond the title, timing, and component. The point is narrower. A relay is a dependency. The other self-hosting path in the repo is our Cloudflare Workers build deployed under your own account, which still means Cloudflare. The standalone relay needs only Docker, a domain name, and two open ports.
+ +A quick recap for anyone new to the tool. You run one command, for example shell claude, and the CLI prints a URL, a password, and a QR code. Anyone holding both the URL and the password opens the same live terminal in a phone or desktop browser. The process and its pseudo-terminal stay on your machine. The relay's only job is to move frames between the CLI and the browser and to enforce access rules. By default the CLI encrypts every terminal frame before it leaves the machine and the browser decrypts it locally, so the relay carries ciphertext. Our security model page spells out what the hosted relay can still observe: frame type, encrypted size, timing, connection IP, access mode, the command label, and lifecycle events. Terminal contents are not on that list. The URL's #salt= fragment never reaches the relay either, because browsers do not send fragments, and the CLI and browser derive the same AES-256-GCM key locally from the salt and the password with PBKDF2-HMAC-SHA256 at 600,000 iterations.
+The self-hosted relay does not change any of that. The docs put it plainly: terminal frames remain opaque to the relay when the CLI's default end-to-end encryption is used. What changes is who operates the box that sees the metadata, and whose uptime you depend on.
+The standalone deployment lives in the standalone directory of the shell.online repository. It is a single Node.js service speaking WebSockets, keeping session metadata in a local state file, fronted by Caddy for TLS. The published image is ghcr.io/teoslayer/shell.online-relay, built for amd64 and arm64. Requirements from the self-hosting document: Docker Engine with Compose, a public domain, and ports 80 and 443. Point the domain's A or AAAA record at the host first, then:
+git clone https://github.com/TeoSlayer/shell.online.git
+cd shell.online/standalone
+
+SHELL_ONLINE_PUBLIC_URL=https://relay.example.com \\
+SHELL_ONLINE_SITE=relay.example.com \\
+docker compose up -d --build
+Two variables, two jobs. SHELL_ONLINE_PUBLIC_URL is the exact public origin the relay writes into share links and checks browser origins against. SHELL_ONLINE_SITE is the Caddy site address; giving it a hostname is what turns on automatic HTTPS. Caddy's documentation is specific about why the ports matter: the HTTP challenge needs port 80 reachable from outside, the TLS-ALPN challenge needs port 443, and Caddy enables both by default. Leave it at the default of http://localhost and Caddy serves plain HTTP, which is what the local smoke test in the docs relies on:
+docker compose up -d --build
+curl http://localhost/api/health
+The compose file is worth reading before you trust it, and it is short. The relay container runs with a read-only root filesystem, drops all capabilities, sets no-new-privileges, runs as a non-root user created in the Dockerfile, and mounts a small tmpfs at /tmp. Caddy publishes 80, 443, and 443/udp and proxies to the relay on port 8080 through a four-line Caddyfile. The health check hits /api/health every thirty seconds, and Caddy waits for the relay to report healthy before it starts.
+You do not reinstall or rebuild the CLI. The CLI reference lists SHELL_ONLINE_SERVER as the default relay URL and a per-invocation --server flag that overrides it:
+SHELL_ONLINE_SERVER=https://relay.example.com shell claude
+shell --server https://relay.example.com claude
+Everything else behaves as it does against the hosted service. The share URL now points at your domain, the browser page is served by your relay (the Docker build bundles the web client into the image), and read-only mode, --auto-close, --persistent, and the rest all work because the standalone server enforces the same protocol rules. The docs list them: authentication, same-origin browser policy, read-only mode, the input lease that stops two people typing over each other, viewer and frame and traffic limits, slow-client protection, the stable terminal grid, and task-bound expiry. One small tell that this was done properly rather than bolted on: the --no-e2ee output now refers to whatever relay you configured instead of assuming it is Cloudflare.
+The access rule is unchanged too, and it needs saying every time. The URL plus the password is a bearer credential. Anyone who has both can watch the session, and unless you started it with --read-only, can type with the permissions of the wrapped process. Running your own relay does not soften that. What it does give you is a domain you control, so a leaked link at least points at infrastructure you can shut off, and shell password rotate followed by the session ID swaps the password and salt on a running session without killing the process.
+The relay keeps a state file, by default at /var/lib/shell-online/relay.json inside the container, on a named volume called relay-state. According to the self-hosting docs it stores session metadata and host-token hashes so that a client started with --persistent can recover the same identity after the relay restarts. It does not store terminal contents, end-to-end encryption keys, or browser passwords. Back the volume up if you care about persistent links surviving a rebuild, and run exactly one relay replica per volume; this is a single-node deployment by design. Live sockets reconnect after a restart, which matches the behavior documented on our reliability page: the CLI and browser both reconnect with bounded backoff, and a temporary failure does not stop the wrapped process.
+Two things the standalone relay does not include: the optional accounts app (organizations, session vault, browser-started sessions) and the hosted analytics dashboard. Accounts are a separate service that never participates in terminal transport, so a self-hosted relay works without them. If you want the org features against your own relay you deploy the app in the repo's app directory separately; it uses Firebase Authentication and PostgreSQL and is documented in its own README.
Most people should keep using the hosted relay. It is already up, it needs no setup, and the encryption boundary is the same. Self-hosting makes sense in a few specific situations. Your compliance regime wants the relay's metadata (IPs, timing, command labels) inside your own perimeter. Your team already runs a Docker host with a domain and you would rather own one more small service than depend on someone else's status page. Or you were reminded this week that a tunnel provider can have an eleven-hour incident, and you want the remote path to a training box or an unattended coding agent to fail on your terms rather than theirs. In each of those cases the deployment is a clone, two environment variables, and a compose up, and the CLI you already have on every laptop and server keeps working with one variable changed.
+For the background on how the browser link itself works, the earlier post on shell.online as a live terminal browser link covers the session model. We build shell.online at Pilot Protocol, it is MIT licensed, and the standalone server is one readable TypeScript file, so you can check every claim in this post against the source before you run it.
+The hosted service works out of the box. When you need the relay on your own domain, the standalone Docker deployment is in the repo, and the CLI switches with one environment variable.
Try shell.onlineThis summer's coding-agent security research kept landing on the same shape of bug: the agent stays inside its sandbox, writes a file, and something trusted on the host runs that file later. Today we shipped shell.online v0.14, which adds file access to a shared terminal, so the timing calls for a plain description of the design. When you share a terminal session in the browser with shell.online, a viewer gets the live process and nothing else. As of v0.14 you can additionally opt in one directory. This post covers the exact flags, what the browser can and cannot ask for, and why the file layer is read-only and kept apart from the terminal itself. The basics of the live link are in our earlier post on the browser link for any terminal.
+Pillar Security published its Week of Sandbox Escapes series on July 20, covering seven vulnerabilities across Cursor, Codex, Gemini CLI, and Antigravity. Their one-sentence summary of the mechanism: the agent only had to write something that a trusted component outside the sandbox would later run, load, scan, or treat as safe. They sorted the findings into four failure modes, and two of them are about files: workspace configurations that are really executable code, and command allowlists that trust a command name instead of the full invocation.
+On September 2, The Hacker News reported Manifold Security's GitSpawn findings. A repository's own .git/config can set core.fsmonitor to an attacker's command, and the command executes as the user, outside the agent's sandbox and without an approval prompt, the moment an agent refreshes the Git index. Seven agents were affected, Claude Code, Codex, and Cursor among them.
+Then on September 10, Upstarts Media covered Accomplish, a Tel Aviv startup that reported further sandbox escapes to Anthropic, Cursor, and OpenAI. One of the fixes took about 50 days. Co-founder Or Hiltch's line in that piece was that there is a lot of talk about security now and it does not really show in how products get built.
+The thread running through all three is a file in the workspace that was treated as inert when it was not. That is the framing we used for the file layer in v0.14.
+shell --files claude
+shell --files-root ./artifacts python train.py
+File access is off unless one of those flags is present. The first scopes it to the command's working directory. The second picks a different root and implies the first. The CLI opens the root with Go's os.Root, so parent traversal and symlink escapes fail at the filesystem boundary instead of in a string-prefix check. The implementation is short and worth reading: shared_files.go in the shell.online source. Directory listings skip symlinks and anything that is neither a regular file nor a directory, and a read of a device file or other non-regular file is refused.
+The file service understands exactly three request types: capabilities, list, and read. There is no write, no rename, no delete, and no execute. A viewer cannot put a file into the shared root through the browser, which is the whole class of problem in the research above. If the wrapped process writes a file, that is the process acting with its own permissions, exactly as it would with no share running.
+Nothing is pushed. The browser receives no listing and no contents until it asks, and the Files control only appears in the viewer when the host opted in. Reads are pulled by the viewer in 16 KiB chunks, so a slow phone connection cannot apply back-pressure to the PTY. Previews are capped at 16 MiB and downloads at 128 MiB. A file over the cap is refused rather than truncated. The CLI keeps at most eight file requests in flight and answers a ninth with a BUSY error. The full flag list for this release is in the v0.14.0 changelog entry of the shell.online repository.
+Files ride the same end-to-end encrypted WebSocket as the terminal frames. The CLI encrypts the listing and each chunk before they reach the relay, and the browser decrypts with the key derived from the ten-character password the CLI printed. The URL fragment carries only a random salt. What the relay routes is a request ID and an encrypted frame size; it does not see paths, names, or contents. That is the same boundary described on the shell.online end-to-end encryption page for terminal output.
+Because of that, the file flags refuse to combine with --no-e2ee. The CLI exits and says that --files and --files-root require end-to-end encryption. There is no transport-only mode for files.
+The credential model has not changed. Anyone holding both the URL and the password can open the share, and if the share is interactive they can also type with the permissions of the wrapped process. Adding --files widens what a holder of that pair can read, so the decision to opt in a directory deserves the same care as the decision to send someone the password.
+The URL and the password together are a bearer credential. With --files, that credential also reads every regular file under the chosen root. Pick a narrow root such as ./artifacts rather than a home directory, and use shell password rotate to revoke a pair without restarting the process.
A link created with shell --read-only --files can open the opted-in files but still cannot type into the process; browser input is blocked by the server as before. That combination fits a training run. A viewer watches the log and opens the latest metrics file from a phone, and there is no path by which they could send a keystroke.
+v0.14 also adds a renderer dropdown. xterm.js remains the default. The alpha Refstream renderer turns filename-like terminal output into previews backed by the file service, which is handy when a build prints the path of a report. The rule that matters: terminal text is never treated as filesystem authority. A path printed by the process is only a hint the viewer can click. Every open still goes through the same rooted, read-only request path, so a process that prints ../../etc/passwd produces a denial from the CLI, not a preview.
+Refstream also offers a Connect agent invitation, and it is separate from file access. You copy one scoped invitation into an agent chat and choose either read, which lets the agent read, search, and wait for terminal output, or control, which also lets it type, run commands, and send key combinations such as Ctrl-C. The invitation is revocable and bound to one live session. Disconnecting the agent does not stop the terminal, and files stay unavailable unless the host separately started the share with a file flag. The agent skill published at shell.online/skill instructs an agent to enable file sharing only when the operator explicitly asks, and to say which root it shared.
+All of this is in the CLI reference under the files topic, and shell help files prints the same text offline. shell.online is MIT licensed and built by us at Pilot Protocol; the file service, the relay worker, and the browser panel are all in the public repository, so every claim in this post can be checked against the code.
+One command prints a URL and a password. Add --files or --files-root to let viewers open a single directory over the same encrypted channel, read-only, with nothing sent until they ask.
Try shell.onlineVS Code 1.137 shipped on September 9, 2026, and its release notes describe the Agent Host in one sentence: "The agent host lets you connect to the same agent session from multiple VS Code windows." Agent harnesses run in a dedicated process, and every window is a client of that process rather than its owner. The idea underneath is old and good. The thing doing the work should not die when the window that started it closes, and every screen looking at it should be a viewer, not the owner. shell.online applies that same split to any terminal process, and it is why terminal sharing without SSH or VPN works for a build that is failing on one machine while the person who can fix it sits at another.
+ +Under the hood VS Code now talks to the harness over the Agent Host Protocol (AHP), which the concept documentation describes as state-first: "The host is the source of truth," and each client subscribes to channels for sessions, chats, terminals, and changesets. Two properties fall out of that. Multiple clients can observe and control the same session and stay in sync, and a session can keep running when no editor or other client is connected at all.
+Reaching that host from another machine is the part worth reading closely. For remote sessions the Agent Host runs as a standalone process and exposes AHP as JSON-RPC over WebSocket. You start one with the code agent host command, it is protected with a connection token, and you reach it over SSH or a dev tunnel. The August architecture post frames the payoff as running the host next to your workspace on another machine "while you connect from the desktop or web to check progress, review changes, and manage sessions."
+That is a good design for agent sessions. It also only covers agent sessions inside VS Code, and its remote path assumes the other person can be given SSH access or a tunnel. A failing build is often neither of those things. It is a plain process, and the colleague you want looking at it may have no account on the box and no reason to get one.
+The shell.online CLI makes the same split, but smaller and more literal. It owns a pseudo-terminal and the process inside it on your machine. Browsers are viewers of that PTY and nothing more. One command wraps the build:
+curl -fsSL https://shell.online/install | sh
+shell npm run build
+The CLI prints a URL and an eight-character password only after the relay exists and the process has survived a short startup handshake. A build that dies in its first second reports the exit status and prints no link, so nobody is handed a dead URL. The session runs in the background by default and your own terminal comes back. Use shell --foreground when you want the output mirrored locally too, or shell attach with the session ID to take over from your own terminal later; Ctrl-X and then D detaches without stopping the build.
+Send the URL and the password to the colleague. They open it on a laptop or a phone, see the terminal the build is drawing, and can type into it if you left the link interactive. The relay is Cloudflare, but the CLI encrypts each frame with AES-256-GCM before it leaves the machine and the browser decrypts on arrival. The URL carries only a random salt fragment, and both ends derive the key from the password with 600,000 rounds of PBKDF2-HMAC-SHA256, as the E2EE guide spells out. Cloudflare still sees connection IPs, timing, and encrypted frame sizes, and the security page lists that metadata rather than glossing over it. Terminal contents are not persisted server-side.
+The access model is blunt and worth saying out loud: the URL and the password together are a bearer credential. Anyone holding both can view the terminal and, unless the link is read-only, type into it with the permissions of the wrapped process. That is the whole trust model. It is why an interactive build link goes to one named person, not to a channel.
+Often the second person does not need the keyboard. They need to see the last two hundred lines of a linker error, or watch the test runner reach the case that hangs. For that:
+shell --read-only npm run build
+Read-only is enforced at the relay Worker, not by the page in the browser. Ordinary browser input and a confirmed EOF are rejected server-side, so opening DevTools or hand-crafting WebSocket frames does not turn the link into an interactive one. Access mode is fixed when the share is created and cannot be weakened afterwards. If you later decide the viewer should type, you start a new share.
+Read-only also narrows what the bearer credential grants. With both values a viewer can see everything the build prints, and nothing else.
+This is where "the host is the source of truth" earns its keep. The reliability page states the rule directly: relay and browser failures are treated as recoverable display failures, never as permission to terminate local work. The CLI and the browser both reconnect with bounded backoff, and an ordinary share link stays recoverable for 12 hours after its host disconnects. The build keeps running through all of it, because it never depended on the viewer.
+Build output is bursty, which creates a second problem: a viewer can fall behind. The CLI keeps a bounded local ring buffer so a viewer who arrives late or reconnects gets the screen restored, and if live output outruns the network or the browser's rendering, stale display work is dropped and one authoritative screen snapshot is sent instead. PTY reads never block waiting on Cloudflare. Queues are bounded and writes time out, so a process that writes faster than the relay can carry keeps running at full speed locally. The 0.11.0 entry in the changelog of the shell.online repository records a fix in exactly this area: authenticated E2EE recovery snapshots are now preserved during relay backpressure, so large output can no longer eject viewers to the password screen. Builds are the workload that finds that kind of bug.
+Two more lifecycle details matter for a pairing session. When the build exits, the share closes on its own; the link does not outlive the process. If you want a harder deadline, shell --auto-close 2h npm run build closes the share after two hours even if the process is somehow still going. And if the colleague opens the link from a phone, the session grid switches from 120 by 36 to 80 by 24 for everyone until the last phone leaves, because viewers never own PTY sizing. Every viewer sees the same line wrapping in the compiler output.
+The Agent Host carries structured state: chats, changesets, tool approvals, terminals as addressable resources. shell.online carries one PTY and nothing else. It does not know what a build is, cannot show a diff, and has no notion of approving a tool call. What it gives up in structure it gets back in reach. Any process on any machine that can run the static binary qualifies, including the OpenWrt routers and Raspberry Pi boards in the platform matrix, and any viewer with a modern browser, with no account, SSH key, tunnel, or VPN on either side.
+The remote path is the sharpest contrast. A remote Agent Host is reached over SSH or a dev tunnel with a connection token. A shell.online link is reached over HTTPS with a URL and a password, and the relay only ever handles ciphertext. Both are bearer credentials in practice. The difference is what the holder can reach: an SSH session is the whole machine, while a share link is one process, at that process's permissions, and optionally with no keyboard at all.
+Rule of thumb for a failing build: an interactive link to the one person who will type the fix, a read-only link to everyone who just wants to watch it go green, and let the share close itself when the build exits.
The full flag list, including the auto-close grammar and exit codes, is in the CLI reference. The broader case for a live terminal link is in the post that introduced shell.online on this blog.
+One command, one link, one password. The process stays on your machine and the relay never sees plaintext.
Try shell.online