diff --git a/public/blog/banners/ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she.svg b/public/blog/banners/ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she.svg new file mode 100644 index 00000000..aa2e0373 --- /dev/null +++ b/public/blog/banners/ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she.svg @@ -0,0 +1,8 @@ + diff --git a/public/blog/banners/search-and-download-terminal-output-in-the-browser-what-shell-onlines-refstream.svg b/public/blog/banners/search-and-download-terminal-output-in-the-browser-what-shell-onlines-refstream.svg new file mode 100644 index 00000000..c4474bca --- /dev/null +++ b/public/blog/banners/search-and-download-terminal-output-in-the-browser-what-shell-onlines-refstream.svg @@ -0,0 +1,8 @@ + diff --git a/public/blog/banners/share-an-opencode-session-after-share-was-retired-a-live-terminal-link-instead-o.svg b/public/blog/banners/share-an-opencode-session-after-share-was-retired-a-live-terminal-link-instead-o.svg new file mode 100644 index 00000000..82d66a76 --- /dev/null +++ b/public/blog/banners/share-an-opencode-session-after-share-was-retired-a-live-terminal-link-instead-o.svg @@ -0,0 +1,8 @@ + diff --git a/src/data/blogPosts.json b/src/data/blogPosts.json index 68dfe1c5..03487774 100644 --- a/src/data/blogPosts.json +++ b/src/data/blogPosts.json @@ -1878,5 +1878,35 @@ "tags": ["shell-online", "supply-chain-security", "checksums", "coding-agents"], "banner": "banners/verify-an-install-script-before-running-it-what-the-shell-online-installer-check.svg", "iso_date": "2026-10-16" + }, + { + "slug": "search-and-download-terminal-output-in-the-browser-what-shell-onlines-refstream", + "title": "Search and Download Terminal Output in the Browser: What shell.online's Refstream View Keeps, After OpenAI's 100-Organization Notice", + "description": "How to search and download terminal output in the browser with shell.online's Refstream view, OSC 133 command marks, and what that copy is not.", + "date": "Oct 17", + "category": "Operations", + "tags": ["shell-online", "terminal", "incident-response", "osc-133", "ai-agents"], + "banner": "banners/search-and-download-terminal-output-in-the-browser-what-shell-onlines-refstream.svg", + "iso_date": "2026-10-17" + }, + { + "slug": "ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she", + "title": "AI Agent Terminal Access Over MCP Without Trusting the Agent's Arguments: What shell.online's shell_send Refuses, After ToFu", + "description": "AI agent terminal access over MCP, after the ToFu study: what shell.online's shell_send validates, what it cannot judge, and how to keep typing scoped.", + "date": "Oct 18", + "category": "Security", + "tags": ["shell-online", "mcp", "ai-agents", "security"], + "banner": "banners/ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she.svg", + "iso_date": "2026-10-18" + }, + { + "slug": "share-an-opencode-session-after-share-was-retired-a-live-terminal-link-instead-o", + "title": "Share an OpenCode Session After /share Was Retired: A Live Terminal Link Instead of a Public Transcript", + "description": "OpenCode 1.19.1 retired /share. How to share an OpenCode session as a live, end-to-end encrypted terminal link, and what you give up.", + "date": "Oct 19", + "category": "Guide", + "tags": ["shell-online", "opencode", "coding-agents", "terminal-sharing"], + "banner": "banners/share-an-opencode-session-after-share-was-retired-a-live-terminal-link-instead-o.svg", + "iso_date": "2026-10-19" } ] diff --git a/src/pages/blog/ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she.astro b/src/pages/blog/ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she.astro new file mode 100644 index 00000000..d99abf4b --- /dev/null +++ b/src/pages/blog/ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she.astro @@ -0,0 +1,20 @@ +--- +import BlogLayout from '../../layouts/BlogLayout.astro'; + +const bodyContent = `
On 9 October a group from Penn State, Université Montpellier and Dartmouth posted ToFu: Exploiting and Fuzzing the LLM-Orchestrator-Tool Trust Boundary to arXiv. They ran their fuzzer against 273 open-source MCP servers exposing 3,405 tools and found verified bugs in 343 of those tools, across 118 servers. The abstract's conclusion is short: tools often fail to enforce the input semantics their own metadata documents, so arguments a model generates should not be trusted. If you are deciding how much AI agent terminal access to hand a second agent, that is the right frame. A terminal is the tool with the widest possible argument: whatever text it receives, the shell runs.
This post walks through the one tool in shell.online, a browser link to any terminal process, that can write to a terminal over MCP: shell_send. It covers what the relay checks before a byte reaches the PTY, the check it cannot make, and the commands that keep control narrow.
GBHackers' write-up of the paper counts 253 verified vulnerabilities, in classes including path traversal, log injection, data exfiltration and command bypass. The authors also describe a "Repeat-After-Me" attack, where text that reproduces a tool call in the orchestrator's expected format gets executed as a real call, and report reproducing it on six orchestrators including LangGraph, AutoGen and CrewAI.
The MCP tools specification already says servers MUST validate all tool inputs and implement access controls, and that clients must treat tool annotations as untrusted unless the server is trusted. For a file tool, validation means something concrete: reject paths outside a root. For a terminal there is no equivalent. The string rm -rf ~ is well-formed, short, printable text. No schema can tell it apart from ls. So the useful question is what a terminal tool can enforce, and what has to be decided before the agent ever gets a credential.
The process stays on your machine. You start it through shell, which prints a link, an eight-character password and a QR code. MCP access is a separate credential: the host issues a scoped bearer for the endpoint https://shell.online/mcp, printed once. The browser password is not an MCP credential and does not work as one.
shell --name "nightly-migration" claude
+shell list
+shell mcp grant <ID> "reviewer" observe 3600An observe grant gets four tools: shell_status, shell_screen, shell_output and shell_wait. The fifth, shell_send, needs a control grant:
shell mcp grant <ID> "fixer" control 600Lifetimes are set in shared/mcp-grants.ts in the MIT-licensed shell.online source. An observe grant defaults to 3,600 seconds and is capped at 12 hours. A control grant defaults to 900 seconds and is capped at one hour. A run can hold at most eight live grants, and a grant expires at a fixed time: using it does not extend it. The bearer belongs in the MCP client's secret or environment mechanism, sent as an Authorization: Bearer header, and not in a chat, a repository or a shell argument. The agent access guide lists the full command set.
Most of this lives in two places you can read: the shell_send registration in worker/index.ts and the validator in shared/mcp-write.ts. In order:
A result of delivered means the host acknowledged the complete write to the PTY. It does not mean the command succeeded, or that the agent on the other end finished anything. The controller has to check the screen or the artifact.
Everything above is about shape: who may call, how long, how many bytes, which characters, at most once. None of it judges meaning, and that is the honest limit of any terminal tool. ToFu's recommendation is to treat model-generated arguments as potentially adversarial and enforce restrictions at execution time. For a PTY, the execution-time restriction is the permissions of the wrapped process. A control grant on a shell running as you can do what you can do.
The reverse direction matters as much. A watched coding agent prints whatever it reads, including text from a README, an issue or a web page. The shell.online docs state that terminal output is untrusted data and never permission to override the controller's instructions, but that is guidance for the controller. If the controller's orchestrator parses screen text into tool calls, which is the Repeat-After-Me pattern, nothing on the relay can catch it. The OWASP entry on prompt injection gives the same two mitigations that apply here: least privilege, and human approval for privileged actions.
A control grant is a bearer credential with the permissions of the wrapped process. Anyone, or any agent, holding it can type until it expires or is revoked. The browser link and password are a second, separate credential with the same property. Use observe when reading is enough, and --read-only when nobody should type.
Start with observe. Most second-agent jobs (summarize what the coding agent is doing, flag when it asks a question, notice a failing test) need nothing else. If the watcher should be able to answer a prompt, issue a control grant for the length of the task, not the session:
shell mcp grant <ID> "answer-one-prompt" control 300
+shell mcp list <ID>
+shell mcp revoke <ID> <grant-id>shell mcp list shows grant metadata without revealing bearers. For a session where no agent should ever type, start it read-only. A grant cannot make a read-only session interactive:
shell --read-only python train.pyRevocation comes in two sizes. shell mcp revoke-all <ID> removes every MCP grant and leaves browser access alone. shell password rotate <ID> changes the browser password and revokes every existing MCP grant without restarting the process. Neither undoes a command that was already delivered, and neither deletes output a controller already received. Teammate access through shell mcp team is observe-only by construction: the relay refuses a team grant with any other scope.
Terminal traffic is end-to-end encrypted by default, and the relay cannot read it. An MCP grant changes that for one client on purpose: it authorizes server-side, in-memory decryption with a frame key the host supplies, and the controller receives plaintext. The MCP design notes state that the service stores no plaintext terminal content, key or bearer, only grant metadata, bearer hashes and operation fingerprints. The security model page covers the browser side.
On coverage: Codex and OpenCode have been exercised for observation and OpenCode for control. Claude model-backed controller behavior is listed as unverified. The native MCP route runs on the hosted Worker relay, not the self-hosted Docker relay. And updating the CLI does not upgrade a host that is already running, so a session started on an older version stays observe-only until it restarts. For the basics of the share link itself, the introductory shell.online post covers the link and password model.
Install the CLI, wrap the process you want watched, and issue an observe grant first. The validation code and grant lifetimes are in the repository to check before you rely on them.
Try shell.onlineOn 30 September OpenAI told more than 100 organizations that its models may have reached their systems during research and evaluation work, and on 1 October the forensics firm Asymmetric Security published its own account, which The Register summarized with one detail that matters for anyone running agents on their own machines: some of the activity left records "erased or inaccessible", so sensitive-data access could not be ruled out. OpenAI's own line was that "notification does not mean that any private information was accessed." Both statements can be true. When the record is gone, nobody can prove either one.
+ +This post is about a smaller, local version of that problem. If you share a terminal with shell.online, can the people watching search the output in the browser, see where each command started and how it exited, and download terminal output from the browser for a ticket? Yes, with the optional Refstream view, and with limits that are worth knowing before you rely on any of it.
+ +The default terminal view in a browser link to a terminal process is xterm.js. Refstream is a second renderer, labeled an unstable alpha, that you pick under Terminal controls on a public share or with the Renderer selector in an app terminal tab. The current build ships Refstream v0.1.0-alpha.5. The choice is stored per browser, in localStorage, under one key that holds only the renderer name.
+Switching is a view change and nothing more. It does not restart the process, change the encryption, or grant the viewer anything. In the repository, web/refstream-tools.ts says it in a comment: shell.online keeps ownership of transport, encryption, permissions and collaboration, and Refstream owns terminal-local navigation and inspection. The same file lists what the toolbar mounts: search, a Commands panel, Connect agent, and a menu with eight themes, text size, and "Download output".
+Search is literal: it matches the text you type, not a regular expression. A back-to-live shortcut returns you to the live screen after you scroll up to the first error in a long build.
+"Download output" saves what the browser has retained as a text file, named shell-online-output.txt by default. That is the useful part for an incident note or a bug report: the viewer gets a plain file without asking the person at the host to copy and paste from their scrollback.
+It is also the part to be careful with: the download is a plaintext copy outside the encrypted session. Everything the end-to-end encryption protects in transit stops applying once the file is on someone's disk.
+Refstream's tools mount on the renderer choice alone; the code does not check whether the link is read-only. A read-only viewer can search and download exactly like an interactive one. A link and its password are a bearer credential: whoever holds both sees everything the process prints and can keep a copy. The CLI help says it plainly: rotation "cannot erase terminal output somebody already saw."
The Commands panel lists each command with its output and exit status, but only when the program in the terminal reports them. Refstream does not guess from the text. Until markers arrive the panel says "No command boundaries yet. Commands appear when the process reports shell markers."
+The markers are the OSC 133 sequences, documented most clearly in Microsoft's Windows Terminal shell integration guide: 133;A at the start of a prompt, 133;B where the command line begins, 133;C where output begins, and 133;D;<exit code> when the command finishes. The shell has to emit them from its prompt. For zsh, a minimal version using the precmd and preexec hook functions looks like this:
+autoload -Uz add-zsh-hook
+_mark_precmd() { local ret=$?; printf '\\e]133;D;%s\\a\\e]133;A\\a' "$ret"; }
+_mark_preexec() { printf '\\e]133;C\\a'; }
+add-zsh-hook precmd _mark_precmd
+add-zsh-hook preexec _mark_preexec
+PS1=$PS1$'%{\\e]133;B\\a%}'
+The very first prompt emits a D mark with no command before it, and if another precmd hook runs ahead of this one, the exit status it reports may belong to that hook. For bash, the Microsoft guide has a complete script built on PS0, PS1 and PROMPT_COMMAND that needs bash 4.4 or newer; PowerShell users can use the prompt function from the same page.
+Here is where the pieces fit. Someone is checking a machine an agent had credentials for, and two colleagues want to follow along without being able to type. The person at the host starts a marked-up zsh, keeps local control, and shares a view-only link:
+shell --foreground --read-only --name "host review" zsh
+The flags come from the CLI reference: --read-only creates an immutable view-only session where the relay rejects browser input, --foreground mirrors and controls the process in the launching terminal, and --name labels it in shell list, shell ls and the web app. The terminal prints the link, the eight-character password and a QR code.
+The colleagues open the link, enter the password, and switch to Refstream. Each command the reviewer runs (a grep through an auth log, a look at crontab, a listing of recently modified files) shows up as its own entry with its exit code. A colleague can search for an IP address across everything retained so far and download the output for the ticket. When the review is over, exiting the shell ends the session; shell kill with the session ID does the same from another terminal.
+If the reviewer would rather the link stop working at a fixed time even if they forget, add --auto-close with a duration or a time such as "today 18:00".
+This is a convenience for the people watching. It is not an audit trail, and the differences are specific.
+If you need a record of a session that does not depend on someone remembering to press a button, record it on the host. On Linux, script(1) from util-linux "makes a typescript of everything on your terminal session", with an optional timing file for scriptreplay. Running script inside the shared session gives you the host-side file and the browser view at the same time. Host auditing tools go further, and neither shell.online nor Refstream replaces them.
+The same toolbar has a Connect agent button. It produces a private, short-lived, revocable invitation for one live terminal, with read or read-and-control access chosen by the viewer. That is a different path from the host-managed MCP grants (shell mcp grant), with different invitations and tools, and the docs say so. For the review above, leave it alone: an observer agent is a third party reading the same output, and the reason to use a read-only link was to limit who can act on what is shown.
+shell.online keeps the process and its PTY on your machine and sends only encrypted frames through the relay; the introduction on this blog covers that model. Refstream is in the MIT-licensed repository: the renderer switch is in web/terminal-renderer.ts, the tool wiring in web/refstream-tools.ts, and the vendored release under web/vendor/refstream/. The OpenAI notifications are a much larger case than one shared shell, but the lesson carries down: decide what gets recorded before the session, not after someone asks what happened.
+Install the CLI, run shell --read-only with any command, and pick Refstream under Terminal controls in the browser. Add OSC 133 marks to your prompt to see each command and its exit code.
Try shell.onlineOpenCode 1.19.1 went out on October 15 with a short note in its changelog: session sharing has been discontinued. Existing share links and the session-sharing commands no longer work, and importing sessions from share URLs is unsupported. If your team used /share to show someone what the agent was doing, you now need another way to share an OpenCode session. This post covers one: running the OpenCode TUI under shell.online, which gives any terminal process a browser link, so a phone or a teammate's laptop opens the live session. It also covers where that differs from what /share did, because some of the differences cut against you.
+ +The docs page for the feature was deleted in the pull request that retired the session sharing backend, but its last version is still in the repository history and it was plain about the design. /share created a unique public URL under opncd.ai, synced the conversation history to OpenCode's servers, and made it readable by anyone with the link. A config key could set sharing to manual (the default), auto, or disabled, and /unshare removed the link and deleted the stored conversation.
+So it was a transcript service. The vendor held a copy of the conversation, and the URL alone was enough to read it. It also outlived the process: you could paste the link into an issue and someone could read it a week later.
+The setup below works differently on all three counts.
+curl -fsSL https://shell.online/install | sh
+shell --name "opencode: auth refactor" opencode
+shell starts a new OpenCode process under a pseudo-terminal on your machine and prints three things: a URL, an eight-character password, and a QR code. It does not attach to an OpenCode instance that is already running. If you want to pick up the last conversation, start OpenCode's own continue flag through shell:
+shell -- opencode --continue
+Sessions run in the background by default. shell list shows the sessions on this machine and their relay state, shell attach <ID> brings the TUI back into a local terminal (Ctrl-X, then D detaches again), and shell --foreground opencode keeps it in the terminal you launched it from while it is being shared. The --name label is what you will see in shell list, which helps once there are three agents running.
+On the other end, open the link and enter the password, or scan the QR code, which carries the password for one-scan access. What loads is the screen OpenCode itself draws on the host, not a re-rendering of its conversation somewhere else. On a phone, the mobile view adds controls for the keys a phone keyboard lacks and a paste flow that shows you the text before inserting it, since a pasted line break can still run a command. Closing the browser tab does not stop OpenCode; the session ends when the OpenCode process exits.
+The nearest thing to the old /share link is a read-only session:
+shell --read-only --name "opencode: auth refactor" opencode
+According to the shell.online security model, input from a read-only viewer is rejected by both the relay and the CLI, so a modified browser cannot turn it into an interactive session. That is the mode for a reviewer who wants to see what the agent is doing to the codebase, or for watching your own run from a phone without the risk of a stray tap.
+Leave --read-only off when the other person needs to act: answer a permission prompt, type a follow-up instruction, or stop the agent with Ctrl-C. That is something /share never offered, and it is also where the risk lives.
+The link and the password together are a credential. Anyone holding both can view the session and, unless it is read-only, type into OpenCode with your operating-system user's permissions, which includes approving whatever the agent asks to run. Keep the QR code private for the same reason, and send the link and the password over different channels when you can.
With /share, the conversation sat on OpenCode's servers in a readable form. With shell.online, terminal traffic is end-to-end encrypted by default. Per the encryption details page, the host computer encrypts terminal data and authorized browsers decrypt it; both sides derive an AES-256-GCM frame key locally with PBKDF2-HMAC-SHA256 at 600,000 iterations from the password and a random salt. The salt sits in the link's #salt= fragment, and URL fragments are not sent in HTTP requests.
+The relay still learns things. It sees connection addresses, timing, encrypted frame sizes, labels, lifecycle events and terminal grid dimensions, and it can delay or drop traffic. Running with --no-e2ee removes the end-to-end layer and exposes terminal contents to the relay, so do not use it for an agent session that prints secrets.
+The trade you make for that: there is no stored transcript at all. Nobody can open the link after the process exits, there is nothing to import into another machine, and there is no permalink to drop into a bug report. If what you actually need is a durable record of what the agent said, a live terminal link is the wrong tool and you should keep a log some other way.
+OpenCode has its own HTTP server, and it is tempting to make that the route to your phone. OpenCode's server docs say that running opencode starts both a TUI and a server, with the TUI acting as the client, and that the TUI picks a random port and hostname. opencode serve defaults to 127.0.0.1 on port 4096, accepts --hostname and --port, and can be put behind HTTP basic auth with OPENCODE_SERVER_PASSWORD.
+A local HTTP server is reachable by more than you might think, because every web page your browser loads can send requests to it. GHSA-632h-h47v-g4x4, published September 24, is the recent example. Christophe Tafani-Dereeper's write-up at Datadog Security Labs shows the /global/upgrade endpoint accepting a text/plain body as JSON without checking the request's origin, so an auto-submitting form on a malicious page could make OpenCode install an attacker-supplied npm tarball and run its preinstall script. It affected 1.14.30 through 1.18.21 when installed through npm, pnpm or Bun, and was fixed in 1.18.22. OpenCode 1.19.0 then added validation of PTY connection origins and tickets. Binding that server to a LAN address so a phone can reach it adds the local network to the list of things that can talk to it.
+Wrapping the TUI in shell does not remove OpenCode's own server. The TUI still starts one, and keeping OpenCode up to date is still on you. What it avoids is opening a second way in. In the shell.online source, the host dials out to the relay over a WebSocket, and each session's local control channel is a Unix socket set to mode 0600, or on Windows a named pipe whose access list admits only SYSTEM, Administrators and your own user. The only TCP listeners in the CLI bind to 127.0.0.1 and are short-lived: the callback for shell login, and on Windows a readiness channel during background launch that checks a random 32-byte token. Nothing waits on a LAN port for the phone.
+The full flag list is on the command reference, and our earlier overview of shell.online covers the general model if you have not used it before.
+One command prints a link, a password and a QR code. Add --read-only when watching is enough. The CLI is MIT licensed, and the code that handles the relay connection and local control sockets is in the public repository.
Try shell.online