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 @@ + + + + SECURITY / OCT 18 2026 + AI Agent Terminal Access Over MCPWithout Trusting the Agent'sArguments: What shell.online'sshell_send Refuses, After ToFu + AI agent terminal access over MCP, after the ToFu... + pilotprotocol.network + 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 @@ + + + + OPERATIONS / OCT 17 2026 + Search and Download Terminal Output inthe Browser: What shell.online'sRefstream View Keeps, After OpenAI's100-Organization Notice + How to search and download terminal output in the... + pilotprotocol.network + diff --git a/src/data/blogPosts.json b/src/data/blogPosts.json index 68dfe1c5..04054347 100644 --- a/src/data/blogPosts.json +++ b/src/data/blogPosts.json @@ -1878,5 +1878,25 @@ "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" } ] 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.

What ToFu found, and why terminals are the hard case

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.

How AI agent terminal access works in shell.online

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 3600

An 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 600

Lifetimes 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.

What the relay checks before a byte reaches the PTY

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:

  1. The tool only exists for some grants. shell_send is registered when three things are all true: the hosted control gate is on, the grant holds the input scope, and the session's host is new enough to understand control frames. An observe grant never sees shell_send in its catalog, so a call from one fails as an unknown tool rather than as a denied request.
  2. The schema is strict. It accepts exactly text, an optional enter boolean and operation_id. An unknown field is rejected with a JSON-RPC -32602 error before the handler runs, instead of being silently stripped.
  3. The text is bounded and contains no control characters. It must be 1 to 8,192 UTF-8 bytes. NUL, every C0 control except TAB, and every C1 control are refused. That rules out LF, CR, ESC and BEL, so a model cannot smuggle a newline or a raw escape sequence inside the text. The only way to submit a line is the explicit enter flag. There is no shell_key or shell_interrupt tool, and the docs say no raw control-byte workaround is allowed.
  4. Every send carries a UUID v4 operation_id. The same ID with the same arguments returns the stored result without typing again. The same ID with different arguments is a conflict, and the original operation is left untouched.
  5. Scope and read-only status are checked again at execution time, before a stored result is replayed and before dispatch. A grant that loses its scope, or a session that turns read-only, between a call and its retry is denied on the retry instead of being handed the stored result.
  6. People outrank the agent. The arbitration order in the code is local host, then a browser viewer, then one MCP writer. If a human is typing, a new send gets busy, and a send whose human veto lands after the claim comes back delivery_uncertain.

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.

The check it cannot make

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.

Keeping control narrow in practice

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.py

Revocation 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.

Privacy and the limits we have tested

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.

Give the second agent the smallest grant that does the job

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.online
`; +--- + + + diff --git a/src/pages/blog/search-and-download-terminal-output-in-the-browser-what-shell-onlines-refstream.astro b/src/pages/blog/search-and-download-terminal-output-in-the-browser-what-shell-onlines-refstream.astro new file mode 100644 index 00000000..af5308d0 --- /dev/null +++ b/src/pages/blog/search-and-download-terminal-output-in-the-browser-what-shell-onlines-refstream.astro @@ -0,0 +1,80 @@ +--- +import BlogLayout from '../../layouts/BlogLayout.astro'; + +const bodyContent = `

On 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.

+ +
+

What Refstream is, and what switching to it changes

+

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 terminal output in the browser, and download it

+

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."

+ +
+

Command boundaries and exit codes need OSC 133 markers

+

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.

+
+ +
+

A read-only shared shell for reviewing a machine

+

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".

+
+ +
+

What the downloaded output is not

+

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.

+
+ +
+

Refstream's Connect agent is a separate channel

+

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.

+
+ +
+

Where this sits

+

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.

+
+ +

Share a terminal people can search

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.online
`; +--- + + +