0062: Chat reads, and asks before it acts
On this page
Status: accepted
Context
A chat that can read Short Leash's sites, logs, requests, mail and dumps is useful, and it also reads text other people wrote: request bodies from a public tunnel, mail, log lines, a cloned repository's config. Text like that can steer a model. A small local model is easier to steer than a large one, and no published defense stops a determined attacker reliably. What does work is limiting what the chat can do, and asking the user before anything changes.
Decision
- The tools are shared. The MCP tools move from the CLI into
leash_core::tools. The MCP server's output stays exactly as it is, and a snapshot test checks that. leashd runs the tools for chat in-process against its own API router. - Every tool has a class, and a test checks that none is missing:
read: runs without asking. Listing and reading sites, logs, dumps, requests, mail, services, backups, webhooks and their deliveries, tasks, notes (notes_list,note_read), meeting notes, Pair's status, calls and the Monitor.act: asks every time. Starting and stopping sites, services and processes, adding a task to a site's list or the General list (its whole title and notes shown in the confirm, since agents may act on it later), marking a task done (recorded as "Short Leash chat"), writing a note (note_add, signed "Short Leash chat"; a coding agent can't sign as the user or as chat), stopping a share, ending a Pair session.never_in_chat: not offered at all. Sharing a site, letting someone into Pair, running code or Artisan, resending a webhook, changing a site's PHP or HTTPS, claiming tasks (other agents work on those), and sending calls, which can reach other hosts.
- Results are redacted for chat only. Removed: environment values, share and webhook URLs, Pair invites, webhook signatures, Cookie and Authorization headers, and call secrets (as 0050 masks them). Mail is passed as plain text.
- Untrusted by default. Every result counts as outside text except fields Short Leash writes itself (states, ports, versions). Invisible Unicode is removed, from a tool's error as from its answer, and an error counts as outside text when the tool's answer would, since Git's or
gh's message can quote the repository. What's invisible is one list,project::file::hidden_char: the characters a project file refuses (format characters, line and paragraph separators, variation selectors and the other default-ignorable ones; 0078). Once a chat has read outside text it shows "Has outside text" for the rest of the conversation, reopened or not. - Asking.
- The question names the action ("Stop shop.test?") and shows every argument, including the real command a process runs.
- Its buttons are Short Leash's own verb and Don't run, with focus on Don't run.
- It is never answered with Enter, never offers "always allow" in a chat with outside text, and asks at most three times a turn.
- Rendering. The app gets a content security policy with no remote images or connections. Chat's Markdown shows no images or raw HTML, allows only http and https links, and opens them only after a confirm.
- The
meeting_notesMCP tool is opt-in, because an MCP client may send other people's words to a cloud model.
Consequences
- Chat is useful for questions about Short Leash and can take small, visible actions; anything that exposes a site or runs code stays a deliberate step the user takes themselves.
- The new invariant: chat never runs code the model writes, never reaches a tool that exposes a site, lets someone in or sends data out, and asks before every action, showing its arguments.
- Every new MCP tool needs a class before it builds.