Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/data/blogPosts.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
import BlogLayout from '../../layouts/BlogLayout.astro';

const bodyContent = `<p>On 9 October a group from Penn State, Université Montpellier and Dartmouth posted <a href="https://arxiv.org/abs/2610.13651">ToFu: Exploiting and Fuzzing the LLM-Orchestrator-Tool Trust Boundary</a> 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.</p><p>This post walks through the one tool in <a href="https://shell.online"><span class="brand-shell">shell.online</span>, a browser link to any terminal process</a>, 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.</p><section><h2>What ToFu found, and why terminals are the hard case</h2><p><a href="https://gbhackers.com/researchers-discover-253-security-vulnerabilities/">GBHackers' write-up of the paper</a> 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.</p><p>The <a href="https://modelcontextprotocol.io/specification/2025-11-25/server/tools">MCP tools specification</a> 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.</p></section><section><h2>How AI agent terminal access works in <span class="brand-shell">shell.online</span></h2><p>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.</p><pre><code>shell --name "nightly-migration" claude
shell list
shell mcp grant &lt;ID&gt; "reviewer" observe 3600</code></pre><p>An observe grant gets four tools: shell_status, shell_screen, shell_output and shell_wait. The fifth, shell_send, needs a control grant:</p><pre><code>shell mcp grant &lt;ID&gt; "fixer" control 600</code></pre><p>Lifetimes are set in <a href="https://github.com/TeoSlayer/shell.online/blob/main/shared/mcp-grants.ts">shared/mcp-grants.ts</a> in the <a href="https://github.com/TeoSlayer/shell.online">MIT-licensed <span class="brand-shell">shell.online</span> source</a>. 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 <a href="https://shell.online/agents/">agent access guide</a> lists the full command set.</p></section><section><h2>What the relay checks before a byte reaches the PTY</h2><p>Most of this lives in two places you can read: the shell_send registration in worker/index.ts and the validator in <a href="https://github.com/TeoSlayer/shell.online/blob/main/shared/mcp-write.ts">shared/mcp-write.ts</a>. In order:</p><ol><li>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.</li><li>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.</li><li>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.</li><li>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.</li><li>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.</li><li>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.</li></ol><p>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.</p></section><section><h2>The check it cannot make</h2><p>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.</p><p>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 <span class="brand-shell">shell.online</span> 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 <a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/">OWASP entry on prompt injection</a> gives the same two mitigations that apply here: least privilege, and human approval for privileged actions.</p><div class="callout"><p>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 <strong>observe</strong> when reading is enough, and <strong>--read-only</strong> when nobody should type.</p></div></section><section><h2>Keeping control narrow in practice</h2><p>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:</p><pre><code>shell mcp grant &lt;ID&gt; "answer-one-prompt" control 300
shell mcp list &lt;ID&gt;
shell mcp revoke &lt;ID&gt; &lt;grant-id&gt;</code></pre><p>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:</p><pre><code>shell --read-only python train.py</code></pre><p>Revocation comes in two sizes. shell mcp revoke-all &lt;ID&gt; removes every MCP grant and leaves browser access alone. shell password rotate &lt;ID&gt; 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.</p></section><section><h2>Privacy and the limits we have tested</h2><p>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 <a href="https://github.com/TeoSlayer/shell.online/blob/main/docs/MCP.md">MCP design notes</a> state that the service stores no plaintext terminal content, key or bearer, only grant metadata, bearer hashes and operation fingerprints. The <a href="https://shell.online/security/">security model page</a> covers the browser side.</p><p>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 <a href="https://pilotprotocol.network/blog/shell-online-live-terminal-browser-link">introductory <span class="brand-shell">shell.online</span> post</a> covers the link and password model.</p></section><div class="cta"><h3>Give the second agent the smallest grant that does the job</h3><p>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.</p><a href="https://shell.online/">Try shell.online</a></div>`;
---
<BlogLayout
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="October 18, 2026"
tags={["shell-online", "mcp", "ai-agents", "security"]}
canonicalPath="/blog/ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she"
author={{ name: 'Artemii Amelin', type: 'Person' }}
bannerImage="/blog/banners/ai-agent-terminal-access-over-mcp-without-trusting-the-agents-arguments-what-she.svg"
>
<Fragment set:html={bodyContent} />
</BlogLayout>
Loading
Loading