From 4de73b2841e498a11537f0cd6748de88f20db5ff Mon Sep 17 00:00:00 2001 From: Artemii Amelin Date: Thu, 1 Oct 2026 22:52:06 +0200 Subject: [PATCH 1/2] 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/2] 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
`; +--- + + +