From 4de73b2841e498a11537f0cd6748de88f20db5ff Mon Sep 17 00:00:00 2001 From: Artemii Amelin Date: Thu, 1 Oct 2026 22:52:06 +0200 Subject: [PATCH 1/3] blog: See What a Coding Agent Is Doing Without Opening Its Terminal: Owner-Only Session Summaries in shell.online, After PixelLeak --- ...opening-its-terminal-owner-only-session.svg | 8 ++++++++ src/data/blogPosts.json | 10 ++++++++++ ...ening-its-terminal-owner-only-session.astro | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 public/blog/banners/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.svg create mode 100644 src/pages/blog/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.astro diff --git a/public/blog/banners/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.svg b/public/blog/banners/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.svg new file mode 100644 index 00000000..a9ebb314 --- /dev/null +++ b/public/blog/banners/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.svg @@ -0,0 +1,8 @@ + + + + SECURITY / OCT 01 2026 + See What a Coding Agent Is DoingWithout Opening Its Terminal:Owner-Only Session Summaries inshell.online, After PixelLeak + See what a coding agent is doing from the session... + pilotprotocol.network + diff --git a/src/data/blogPosts.json b/src/data/blogPosts.json index 6e823f5e..803f045b 100644 --- a/src/data/blogPosts.json +++ b/src/data/blogPosts.json @@ -1768,5 +1768,15 @@ "tags": ["shell-online", "security", "end-to-end-encryption", "coding-agents", "secrets"], "banner": "banners/end-to-end-encrypted-terminal-sharing-has-a-second-secret-on-the-host-shell-onli.svg", "iso_date": "2026-09-30" + }, + { + "slug": "see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session", + "title": "See What a Coding Agent Is Doing Without Opening Its Terminal: Owner-Only Session Summaries in shell.online, After PixelLeak", + "description": "See what a coding agent is doing from the session list: new shell.online summaries are sealed to the owner and unreadable by the service.", + "date": "Oct 1", + "category": "Security", + "tags": ["shell-online", "coding-agents", "confidential-computing", "prompt-injection", "terminal-sharing"], + "banner": "banners/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.svg", + "iso_date": "2026-10-01" } ] diff --git a/src/pages/blog/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.astro b/src/pages/blog/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.astro new file mode 100644 index 00000000..c30d97e4 --- /dev/null +++ b/src/pages/blog/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.astro @@ -0,0 +1,18 @@ +--- +import BlogLayout from '../../layouts/BlogLayout.astro'; + +const bodyContent = `

There are two ways to see what a coding agent is doing while you are away from it: open its terminal, or read whatever artifact it left for you. A report this week shows how the second goes wrong. On September 29, Glow Labs published its PixelLeak research: more than 13,000 internal images sitting openly on GitHub, put there by developers at over 300 organizations across 900+ repositories. Developers asked an agent to prove a visual change worked. It could not attach a screenshot to a private pull request from a command line, and in the report's words: "The agents figured out that they could make the image available to the human reviewer by hosting it in an adjacent public repo." Coverage on October 1 lists what was legible in those pictures, including customer billing records, treasury consoles, and unreleased features.

Whatever an agent produces so a human can check on its work is data, and somebody has to decide who can open it before the agent picks the most convenient place to put it. On October 1 we merged a change to shell.online that makes that decision in code for one such artifact: a short summary of each session in the session list, sealed so that only the session's owner can read it.

What merged on October 1, and what has not shipped

shell.online gives any terminal process a browser link (the basics are in our introduction to the live terminal link). On a machine linked to an account (linking is optional), the web app lists your sessions. Pull request 275 adds a card to that list, and a follow-up merged the same day makes the whole row or board card the target. Hover it, or long-press it on a phone, and a few lines say what the session last did, with a state such as "Waiting for input".

This is on main, not in a tagged release. The latest published CLI is v0.24.1, the changelog's Unreleased section is still empty, and the README says outright that a merged change is not proof that every hosted component or running CLI host has been updated.

Summaries are off for every session until its owner turns them on, either with the Summaries toggle in the web app or from the host:

shell claude
+shell list
+shell permissions <session-id> --summaries=true

The switch is independent of the other automation consents. A comment in the migration that adds it gives the reason: consenting to daily briefings is not consenting to terminal output leaving the host.

See what a coding agent is doing from its own transcript, with no model call

For Claude Code the host does not read the screen and does not run a model. When a linked machine starts a plain shell claude session, the CLI adds two things to the launch: a fresh --session-id, and a --settings value that installs a SessionStart hook. The Claude Code hooks reference says SessionStart fires on startup, resume, /clear, compaction and forks, and hands the hook a session_id and a transcript_path. Our hook is one fixed command that writes that input to a private file (mode 0600) in the local session directory. That is how a summary follows the conversation across /clear and /resume.

The binding is strict. A transcript path reported by the hook is accepted only when it is exactly that conversation's .jsonl file under the Claude projects directory and a regular file, so hook input cannot point the reader anywhere else.

Once a turn ends, the host reads the tail of the transcript and takes the latest AI title and the latest assistant text. Only text blocks are read, so thinking and tool calls never enter a summary, and a turn that is still running is skipped. The result is reduced to plain text, capped at 480 characters with an 80-character title, and sealed on the machine to the owner's account vault key: an ephemeral ECDH P-256 exchange, HKDF with SHA-256, then AES-256-GCM. The accounts service stores an envelope it cannot open.

Launches with --print or a subcommand such as claude mcp produce no summary. On Windows, where the hook has no POSIX shell to run in, sessions bind only through --session-id.

Every other process needs an attested enclave, and today's build refuses to use one

A dev server or a training job has no transcript. Summarizing it takes a model, which means terminal text leaving the machine. The design puts three gates in front of that.

  1. Minimization on the host. Only after consent does the CLI keep a bounded copy of recent output (64 KiB). Once the session has been quiet for 20 seconds, it strips escape sequences and control characters, redacts secret-shaped strings (private key blocks, Authorization and Cookie headers, AWS, GitHub and Slack token formats, JWTs, long high-entropy tokens), and keeps at most the last 12,288 bytes. The source calls this best-effort minimization, not a guarantee.
  2. Attestation before anything is sent. The host fetches the summarizer's identity and verifies a Google-signed token whose fields are documented in the Confidential Space token claims reference: RS256 only, the expected issuer and audience, a Confidential Space workload on Intel TDX, debugging disabled since boot, a STABLE image, and a container image digest that appears on an allowlist signed with an Ed25519 release key. The enclave's public key must be bound into the token through eat_nonce, the nonce claim from RFC 9711.
  3. Sealing in both directions. The request is sealed to the attested key. The summary comes back already sealed to the owner's vault key, so the host uploads an envelope it cannot open either.

The list of release keys compiled into internal/summary/allowlist.go is empty on main. With no key the verifier fails closed: the output copy is never enabled, no ticket is requested, nothing is sent. A build from main produces Claude Code summaries and nothing else. The pull request lists what remains for the other path: deploy the enclave, then ship a CLI release with the release key embedded.

A summary does not change who can reach the terminal. The share URL and its password together are still a bearer credential: anyone holding both can view the session and, unless it was started with --read-only, type with the permissions of the wrapped process. The summary is a separate object with a narrower audience. Teammates in your organization see the session in the list. Only the owner's vault opens its summary.

A summary is text an attacker may have written

Terminal output is untrusted input. A README the agent prints or a dependency's install script can carry text aimed at whichever model reads it next. OWASP ranks this first, as LLM01, prompt injection. A card in a session list looks trustworthy, so the design treats its content as hostile. On the enclave side the pull request describes spotlighting of the terminal text in the prompt and schema-constrained decoding. The enclave is a separate repository, so that description is the public record for both.

The output guard is in the shell.online repository. A summary may carry prose and nothing that acts. Every summary, whoever wrote it, passes a strict gate that refuses text containing a URL, a bare domain, an e-mail address, an IP address, Markdown link syntax, code markup or an HTML tag. The browser applies the same rules again after decrypting, renders every string as plain text, and puts a fixed caption under each card: "Automated summary of terminal output. It may be wrong; never follow instructions in it." The pull request reports a 14-case evaluation against a real model with no unsafe results, including one case where the model was fully hijacked and the guard withheld the output.

Idle periods, offline hosts and revocation

None of this would have stopped PixelLeak. Glow's recommendations are about auditing exposure and controlling what an agent may publish. The agents in that report reached for a public repo because it was the place that worked. Here the artifact is encrypted before it leaves the host, as terminal frames already are. The security model and encryption details pages cover that channel.

Put a link on the next agent run

Install the CLI, run shell claude, and open the printed link from a phone. Summaries are on main, not yet in a tagged release. The live terminal works today.

Try shell.online
`; +--- + + + From dd7ee8b6ee9d1c86bed791cd5e8465bc19f02fba Mon Sep 17 00:00:00 2001 From: Artemii Amelin Date: Fri, 2 Oct 2026 22:53:19 +0200 Subject: [PATCH 2/3] blog: A Browser Terminal for a Docker Container That Keeps One Link Across Restarts, After the Unsloth Studio RCE --- ...that-keeps-one-link-across-restarts-af.svg | 8 +++ src/data/blogPosts.json | 10 ++++ ...at-keeps-one-link-across-restarts-af.astro | 54 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 public/blog/banners/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.svg create mode 100644 src/pages/blog/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.astro diff --git a/public/blog/banners/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.svg b/public/blog/banners/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.svg new file mode 100644 index 00000000..589aced7 --- /dev/null +++ b/public/blog/banners/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.svg @@ -0,0 +1,8 @@ + + + + GUIDE / OCT 02 2026 + A Browser Terminal for a DockerContainer That Keeps One LinkAcross Restarts, After theUnsloth Studio RCE + A browser terminal for a Docker container with... + pilotprotocol.network + diff --git a/src/data/blogPosts.json b/src/data/blogPosts.json index 803f045b..a7524d4a 100644 --- a/src/data/blogPosts.json +++ b/src/data/blogPosts.json @@ -1778,5 +1778,15 @@ "tags": ["shell-online", "coding-agents", "confidential-computing", "prompt-injection", "terminal-sharing"], "banner": "banners/see-what-a-coding-agent-is-doing-without-opening-its-terminal-owner-only-session.svg", "iso_date": "2026-10-01" + }, + { + "slug": "a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af", + "title": "A Browser Terminal for a Docker Container That Keeps One Link Across Restarts, After the Unsloth Studio RCE", + "description": "A browser terminal for a Docker container with shell.online: one encrypted link across restarts, no SSH keys inside, and why the Unsloth RCE makes that matter.", + "date": "Oct 2", + "category": "Guide", + "tags": ["shell-online", "docker", "terminal-sharing", "ml-security", "end-to-end-encryption"], + "banner": "banners/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.svg", + "iso_date": "2026-10-02" } ] diff --git a/src/pages/blog/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.astro b/src/pages/blog/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.astro new file mode 100644 index 00000000..3576e2ab --- /dev/null +++ b/src/pages/blog/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.astro @@ -0,0 +1,54 @@ +--- +import BlogLayout from '../../layouts/BlogLayout.astro'; + +const bodyContent = `

On September 29, Pillar Security published its analysis of a code-execution flaw in Unsloth Studio, the UI for the open-source Unsloth fine-tuning library. Selecting a model in the picker was enough to run Python from that model's repository on the machine hosting Studio. The fix shipped in version 2026.6.9. What stays relevant after the patch is where the code ran: under the developer's own account, next to the developer's keys. This post covers one way to give that kind of work a smaller home: a browser terminal for a Docker container, built on shell.online, our browser link for any terminal process, that keeps one encrypted link across restarts and needs no SSH key to reach.

+
+

The Unsloth Studio bug ran with the developer's own permissions

+

The mechanism is short. Per Pillar, Studio's backend probed a model's capabilities by loading its config.json through the Transformers AutoConfig loader, with trust_remote_code defaulting to true. A config.json can carry an auto_map field pointing at Python files in the same repository, and with that flag on, Transformers imports them. InfoWorld's report quotes Pillar researcher Ariel Fogel: "Reading the model's config.json was enough to trigger the exploit; the backend never loaded the weights or ran inference."

+

Hugging Face documents the flag as an explicit opt-in. The Transformers guide to loading custom models says to "take extra precaution when loading a custom model" and recommends pinning a specific revision when you do. Studio made that choice on the user's behalf during what looked like a metadata read.

+

Pillar's impact list is HuggingFace tokens, SSH keys, cloud credentials, proprietary training data, and model artifacts. That list is simply what any process started by a developer account can open, and nothing on it is specific to Unsloth. Upgrading closes this bug and leaves the general problem where it was: the next tool that executes something from a repository you only meant to inspect gets the same reach.

+
+
+

A browser terminal for a Docker container, from the Compose file in the repo

+

The shell.online repository carries a docker-compose.yml in its root that runs the CLI inside a container. It is a client of the hosted relay, a different deployment from the self-hosted relay under standalone/. The Docker workspace guide gives the whole setup:

+
git clone https://github.com/TeoSlayer/shell.online.git
+cd shell.online
+docker compose up --build -d
+docker compose logs shell-online
+

The first launch writes a link and a generated password to the container log. The guide warns that these logs contain credentials, so read them privately and keep them out of shared reports. Open the link in any browser, enter the password, and you get bash in /workspace as a non-root user named shellonline. Frames are encrypted by the CLI in the container and decrypted in the browser, and the relay forwards ciphertext. That is the same model as every other share, described in our original walkthrough of the link-and-password design.

+

The service definition is short enough to read in full. These are the parts that decide what the container can touch:

+
restart: unless-stopped
+volumes:
+  - shell-online-state:/var/lib/shell-online
+  - shell-online-workspace:/workspace
+

Two named volumes, no ports section, no bind mount of the host filesystem. The container dials out to the relay and nothing listens for inbound connections. The runtime stage of the Dockerfile is Alpine 3.22 plus bash, ca-certificates, and tini, so there is no SSH daemon to configure. The common ways a remote dev container ends up holding host credentials (a mounted ~/.ssh, a forwarded agent, an authorized_keys file) have no job to do here. Code that runs in this container can read the workspace, the session's own state, and whatever you type or paste into it later.

+

Two limits to know before planning around it. The image has no Python and no CUDA. A fine-tuning stack means editing the runtime stage yourself, since the root Dockerfile builds only the Alpine image. Also, the Compose file on main currently names image tag 0.22.0 and passes the same string as the build version, while the latest CLI release is v0.25.0. Check that tag against the release notes if you need a newer feature inside the container.

+
+
+

What a restart keeps and what it discards

+

The entrypoint script ends with one line:

+
exec shell --foreground --persistent "$state_file" "$@"
+

The state file is /var/lib/shell-online/session.json on the state volume. It records a host token, the URL fragment carrying the key-derivation salt, the encryption key, the browser password, and whether the session is read-only and encrypted. The session ID is computed from the host token (SHA-256 over a fixed prefix and the token, truncated, base64url-encoded), which is why the URL comes out identical on every start. On resume the relay compares the stored hash of that token in constant time and requires the read-only and encryption flags to match the originals. A mismatch gets a 403.

+

Docker's documentation defines the unless-stopped restart policy as the always policy with one exception: a container you stopped stays stopped, even after the daemon restarts. So an orderly host reboot, a daemon restart, or typing exit in the browser shell all lead back to the entrypoint and the same state file. The URL and password are unchanged, and the people holding them do not need a new link. The relay keeps a persistent identity for up to 30 offline days, and the Docker guide notes that a saved identity can be revived from its state volume even after that retention expires. An ordinary disconnected share can recover its link for 12 hours.

+

What does not come back is the process. Each start runs a new bash. The README puts it in one sentence: "A saved link is not a saved process." Files in /workspace survive because they sit on the second volume. A job that was running when the container stopped is gone. The CLI reference also flags the unclean case: a crash that leaves a local record or control socket blocks the resume until you review and recover it explicitly. Those records live under /tmp inside the container, separate from the state volume that holds the identity.

+

Two more behaviors are fixed. Persistent sessions are always end-to-end encrypted, because --no-e2ee cannot be combined with --persistent. And if you set SHELL_ONLINE_E2EE_PASSWORD to something other than the stored password, the entrypoint exits with status 64 and tells you to restore the original, so a typo in an environment file cannot quietly break the existing URL.

+
+
+

The one credential that does live inside the container

+

The state volume is owned by the same user that runs your shell. A malicious auto_map module executed in this workspace would find no SSH key and no cloud CLI profile, but it could read session.json and the password file beside it, and with them the link to this very terminal. It could also read /workspace and any token you exported in that shell, and the container has outbound network access because it has to reach the relay. Docker narrows what a hostile process can reach. It does not make hostile code safe to run.

+

The URL and password together are a bearer credential. Anyone holding both can view this terminal and type in it as the container user. Send them through separate channels, and treat the state volume and its backups as secrets.

+

If the link leaks, run shell list inside the container to get the session ID, then shell password rotate with that ID. Per the CLI help, rotation generates a fresh password and URL salt without restarting the process and disconnects current viewers, and the host rewrites session.json with the new salt, key, and password. Changing the environment variable alone does not rotate stored state. Rotation cannot erase output somebody already saw, a limit the security model page states directly. For a clean break, a new state volume gives a new identity and a new URL.

+
+

Put the next unfamiliar repo in a container with a link

Clone the repo, run docker compose up --build -d, and read the link and password from the container log. The workspace and the link survive restarts, and your SSH keys never enter the picture.

Try shell.online
`; +--- + + + From 9afc38d9b2f3b81bd162c31d16dbf3a4255e2cef Mon Sep 17 00:00:00 2001 From: Artemii Amelin Date: Mon, 5 Oct 2026 20:38:17 +0200 Subject: [PATCH 3/3] blog: Share a Windows Terminal Session in the Browser: Codex on ConPTY, Without OpenSSH or WSL --- ...-browser-codex-on-conpty-without-openssh.svg | 8 ++++++++ src/data/blogPosts.json | 10 ++++++++++ ...rowser-codex-on-conpty-without-openssh.astro | 17 +++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 public/blog/banners/share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh.svg create mode 100644 src/pages/blog/share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh.astro diff --git a/public/blog/banners/share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh.svg b/public/blog/banners/share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh.svg new file mode 100644 index 00000000..afa013f5 --- /dev/null +++ b/public/blog/banners/share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh.svg @@ -0,0 +1,8 @@ + + + + GUIDE / OCT 05 2026 + Share a Windows Terminal Sessionin the Browser: Codex on ConPTY,Without OpenSSH or WSL + Share a Windows terminal session in the browser... + pilotprotocol.network + diff --git a/src/data/blogPosts.json b/src/data/blogPosts.json index a7524d4a..cccfa772 100644 --- a/src/data/blogPosts.json +++ b/src/data/blogPosts.json @@ -1788,5 +1788,15 @@ "tags": ["shell-online", "docker", "terminal-sharing", "ml-security", "end-to-end-encryption"], "banner": "banners/a-browser-terminal-for-a-docker-container-that-keeps-one-link-across-restarts-af.svg", "iso_date": "2026-10-02" + }, + { + "slug": "share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh", + "title": "Share a Windows Terminal Session in the Browser: Codex on ConPTY, Without OpenSSH or WSL", + "description": "Share a Windows terminal session in the browser with shell.online: Codex on native ConPTY, no OpenSSH server or WSL, and the Windows-only limits.", + "date": "Oct 5", + "category": "Guide", + "tags": ["shell-online", "windows", "conpty", "codex", "terminal-sharing"], + "banner": "banners/share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh.svg", + "iso_date": "2026-10-05" } ] diff --git a/src/pages/blog/share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh.astro b/src/pages/blog/share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh.astro new file mode 100644 index 00000000..6ca32173 --- /dev/null +++ b/src/pages/blog/share-a-windows-terminal-session-in-the-browser-codex-on-conpty-without-openssh.astro @@ -0,0 +1,17 @@ +--- +import BlogLayout from '../../layouts/BlogLayout.astro'; + +const bodyContent = `

OpenAI published Codex CLI 0.160.1 on October 5, and its one listed fix is for Windows: SYSTEMROOT, TEMP and TMP are now preserved when Codex launches a remote stdio MCP server with explicitly configured remote environment variables. Version 0.160.0, four days earlier, fixed Windows sandbox PowerShell fallbacks and long-path permission repairs. Before that, 0.159.2 on September 29 stopped console windows flashing when Codex starts background processes and sandboxed commands. OpenAI's Windows documentation for Codex describes running natively in PowerShell with a Windows sandbox instead of requiring WSL or a virtual machine. With the agent running on the Windows desktop itself, the practical question is how to share a Windows terminal session in the browser when that desktop is at the office and you are somewhere else.

Codex and shell.online need the same Windows component

The same OpenAI page says that on Windows 10, Codex depends on modern console support, including ConPTY, and that in practice version 1809 or newer is required. Microsoft's reference for the CreatePseudoConsole function lists the same floor: Windows 10 October 2018 Update (version 1809). shell.online has the same requirement for the same reason. Running shell help windows prints this line in its platform table:

Windows:     386, amd64, arm64 (Windows 10 1809+; native ConPTY)

A pseudoconsole lets one program host a console application without a console window. The host creates the input and output channels, starts the child process attached to them, and reads back UTF-8 text interleaved with virtual terminal sequences. Microsoft's guide to creating a pseudoconsole session points out that the host may relay those channels over a network so that another machine does the presenting.

That is the design of shell.online, a browser link to any terminal process, on Windows. In cmd/shell/process_windows.go the CLI opens a pseudoconsole, sizes it to the session grid, and starts your command inside it. Terminal payloads are encrypted on the PC before a relay forwards them, and a browser holding the link and the password decrypts and draws them. Codex is started the way any terminal would start it.

How to share a Windows terminal session in the browser

Two commands in PowerShell:

irm https://shell.online/install.ps1 | iex
+shell codex

The installer reads the processor architecture, downloads shell-windows-amd64.exe (or the 386 or arm64 build) together with the SHA256SUMS manifest, and stops if the SHA-256 of the binary does not match. It copies shell.exe to %LOCALAPPDATA%\\Programs\\shell.online. If that directory is not on PATH, it prints the commands that would add it and leaves PATH alone. It also sends one request reporting how the install went and which binary it was; setting SHELL_ONLINE_INSTALL_REPORT=0 skips that.

The second command starts Codex in a detached background process and prints a link, a ten-character password and a QR code. Open the link on a phone, enter the password, and you are looking at the same Codex session and can type into it. The rest of the command reference applies unchanged: shell list shows the sessions on the PC, shell attach <ID> joins one from a local console (Ctrl-X, then D detaches), and shell kill -- <ID> stops one. Running shell with no command starts pwsh.exe if it is on PATH, then powershell.exe, then whatever COMSPEC names. The link, password and QR flow itself is described in our first post about the tool, written when encryption was still an opt-in flag. It is on by default now.

Compare the usual route. Microsoft's OpenSSH Server guide for Windows asks for an account in the built-in Administrators group, the OpenSSH Server optional feature on Windows 10 and 11, a started sshd service, and an inbound firewall rule named OpenSSH-Server-In-TCP on port 22. After all of that, the PC still has to be reachable through whatever NAT it sits behind. The shell CLI needs outbound HTTPS and WebSockets, so there is no Windows service to enable and no inbound rule to add. Machines that can dial out but cannot be dialed are the same problem Pilot Protocol handles for agent-to-agent traffic with STUN, hole-punching and an encrypted relay fallback. Here the other end is a browser, so a relay carries everything.

What the Windows build does differently

The Windows-specific code is a handful of files ending in _windows.go in the MIT-licensed repository. Three differences are worth knowing before you rely on it.

  • Local control is a named pipe. shell attach and shell kill reach a session over \\\\.\\pipe\\shell-online-<ID>, which local_transport_windows.go creates with a protected DACL granting access to SYSTEM, the built-in Administrators group and your own user SID. A --persistent state file gets the same DACL, because Unix permission bits mean nothing on Windows. Read that list literally: another administrator on the same PC can open the pipe.
  • Stopping a session stops the process tree. The command is assigned to a job object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, so closing the job terminates everything Codex spawned along with Codex. The source notes one gap: a very short command can exit before Windows allows the assignment.
  • Windows has no SIGWINCH, so the size of a local console is polled every 250 ms instead of arriving as a signal.

What does not work on Windows yet

There is no service installer. On macOS and Linux, agreeing to browser-started sessions during shell auth also installs the machine daemon as a background service, so an idle machine stays reachable after a restart. On Windows, the installer function returns an error whose text is "installing a background service is not supported on this platform yet". The daemon still starts whenever you run a shell command on a linked machine that has remote start allowed, and its control channel is a named pipe with your user SID in the name, so two people signed in to one PC each get their own. It does not come back by itself after a reboot. The comment in service_other.go explains the choice: we would rather say so than ship a scheduled task nobody has run.

Commands typed into the web app are handled differently too. A line with no shell metacharacters runs directly as an argument list. Anything else is handed to powershell.exe -NoLogo -NoProfile -Command. The backslash is on the metacharacter list, so on Windows any command containing a path such as C:\\work\\build.ps1 takes the PowerShell route, and -NoProfile means the aliases and functions in your profile are not loaded.

Two smaller limits. The SessionStart hook that keeps a Claude Code session summary bound across /clear and /resume relies on a POSIX shell, so it is unavailable on Windows and sessions there bind only through --session-id. And test coverage is uneven: the CI workflow runs the Go test suite on a windows-latest runner, including a test that starts cmd.exe in a real pseudoconsole and one for list, attach and stop, and then exercises the installer's checksum verification. The platforms page states the result plainly: native runtime checks on amd64, with the 386 and arm64 builds build-verified only.

The process and its pseudoconsole stay on the PC, so a PC that goes to sleep takes the session offline. The CLI and the browser retry temporary connection failures automatically, and an ordinary disconnected share can recover its link for 12 hours. Set the power plan accordingly before leaving an agent running overnight.

Who can type into the PC

The link and the password together are a bearer credential. Anyone holding both can view the session and, unless it is read-only, type into it with the operating-system permissions of the wrapped process. The QR code contains the password as well, so keep it out of screenshots.

This matters more with an agent behind the link. Codex's Windows documentation says agent mode uses the sandbox to block filesystem writes outside the working folder and to prevent network access without your explicit approval. shell.online adds no sandbox of its own, and a viewer who can type can answer an approval prompt. If the person on the other end only needs to watch, start the session read-only:

shell --read-only codex

Per the security page, input is then rejected by both the relay and the CLI, and a modified browser cannot turn the session interactive. If a password has travelled further than intended, shell password rotate <ID> disconnects existing viewers and changes the password and salt while Codex keeps running.

Terminal traffic is end-to-end encrypted by default. The key is derived from the password in the CLI and in the browser, and the URL carries only a random salt. The relay still sees metadata, including connection IPs, timing, encrypted sizes and the terminal grid dimensions. Starting a session with --no-e2ee gives up the content protection and leaves only HTTPS and WSS on each hop, which exposes terminal contents to the relay.

Put a browser link on a Windows terminal

Run the PowerShell installer, then shell codex, or shell alone for a plain PowerShell session. It needs Windows 10 version 1809 or newer and outbound HTTPS.

Try shell.online
`; +--- + + +