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 @@ + 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 @@ + diff --git a/src/data/blogPosts.json b/src/data/blogPosts.json index 7979c5aa..cb9087f8 100644 --- a/src/data/blogPosts.json +++ b/src/data/blogPosts.json @@ -1778,5 +1778,25 @@ "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" + }, + { + "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/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 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.
+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.
+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 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.
+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.onlineOpenAI 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.
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.
Two commands in PowerShell:
irm https://shell.online/install.ps1 | iex
+shell codexThe 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.
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.
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.
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 codexPer 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.
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