Houston docs

Shell sandbox and network

Shell commands run under the host OS sandbox where one exists, confined to the project folder: writes outside the project are blocked, and network access is off by default.

PlatformSandbox
macOSSeatbelt (sandbox-exec)
Linuxbubblewrap (an unprivileged user namespace), when available
Windowsnone broadly available, so shell commands run unconfined. Houston reports them as not sandboxed and never silently auto-approves one.

The structured file tools stay confined to the project on every platform.

Working across repos. Additional folders (Settings) adds folders to the file tools' allowed roots and to the shell sandbox.

Network for shell commands

Network from run_shell always goes through the egress allowlist (see "Egress allowlist" below), never straight out. The sandbox can read your whole filesystem, so blanket outbound access plus full auto would be a one-command read-and-exfiltrate; the allowlist bounds where a command can send anything.

When shell commands have network:

  • Full auto: from the start, through the allowlist. Installing dependencies, cloning, and fetching toolchains just work.
  • Ask / auto-edit: after "Allow for run" on a shell command. Plain "Allow" and "Always allow" run that one command offline, and until the run has the grant, each shell approval prompt says so.
  • Egress set to "All domains": there is no allowlist to bound it, so even full auto starts offline and asks once, the first time a command fails for lack of network (the shell-network consent). Declining runs commands offline instead of blocking them.

A command that needs the network and doesn't have it (cloning, installing dependencies, gh, curl) isn't impossible; it just needs that grant.

Headless runs (-p) have no prompt to grant it from:

  • Under --full-auto, shell commands have network through the allowlist from the start.
  • --allow-shell-network lets shell commands reach the network for that run under any policy. It isn't remembered for the next run.
  • --on-approval allow alone never grants it under ask or auto-edit, since it approves each command the way plain "Allow" does.

Installs behave well offline and online:

  • Offline, npm, pip, and cargo skip their usual retries, so an install that needs the network fails in about a second instead of a minute or more.
  • Package-manager caches (npm, pip, yarn) are redirected to a writable temp folder, so installs need no cache workaround once network is on.
  • On macOS, Python's bytecode cache goes there too, since the system python3 keeps it under ~/Library/Caches, which the sandbox can't write.

Finding your toolchain

An app opened from the Dock doesn't read your shell startup files, so Houston adds the usual places runtimes live to the shell's PATH: Homebrew, nvm's default Node, fnm, Volta, mise, asdf, pyenv, rbenv, and the per-user bin folders of Rust, Go, Bun, and Deno.

A sandboxed command may stop processes it started (build tools do this with their workers), but nothing outside its own sandbox.

When a runtime is missing

If a command fails because Node.js, Python, Go, Rust, Deno, or Bun isn't installed:

  • the transcript says so and offers Install, which runs the install command (for example brew install node) in a new tab of the built-in terminal, outside the sandbox, so you can watch it and answer any prompt
  • there's also a link to the official download page
  • the agent is told not to attempt system installs itself, and to wait for you rather than write code it can't run

The terminal clients print the same install command.

Egress allowlist

Granted shell network isn't unrestricted. On macOS and Linux it's routed through a local proxy Houston controls, and the OS sandbox blocks any direct connection, so the proxy is the only road out.

The proxy allows a destination only if its domain is on the allowlist:

  • a built-in set of development infrastructure: package registries such as npm, PyPI, crates.io, RubyGems, Maven, and Go, and code hosts such as GitHub, GitLab, and Bitbucket
  • plus any domains you add in Settings under Sandbox egress

How it behaves:

  • Each entry also covers its subdomains.
  • A deny list wins over every allow.
  • Edits apply immediately, mid-run.
  • A refused destination fails with an EGRESS_BLOCKED message naming the domain. That's policy, not an outage.
  • In a chat, Houston then asks whether to allow that host for the chat: Allow for this chat or Keep blocked. It asks even when the command hid the error (piped its output away, or ran in the background), because the proxy reports the refusal itself. An allowed host is that exact name, not its subdomains, for this chat's shell commands only, and the answer holds until Houston quits. A deny-list entry or a private address is never offered. Headless runs keep the host blocked and tell the agent so.
  • Only HTTP(S) flows through the proxy, so other protocols (SSH, for example) are blocked under the allowlist. Use HTTPS remotes inside the sandbox, or switch the egress mode to "All domains" (Settings) to restore unrestricted granted network.
  • On Windows, where no OS sandbox exists, the allowlist can't be enforced.

Linux and dev servers. Each command runs in its own network namespace whenever shell network isn't granted, and also under the allowlist. So a dev server started inside the sandbox is reachable only from that same command: not from the Preview panel, view_localhost, or later commands. Granting shell network with "All domains" restores the shared network. The agent is told this on Linux, and checks web apps with requests from the command that starts the server.

Secret redaction

Tool results, the transcript, and logs are scrubbed before the model or the interface sees them:

  • well-known token formats, and this install's stored secrets, become [redacted:…]
  • a NAME=value line whose name marks it as a secret (SESSION_SECRET, ADMIN_PASSWORD, GITHUB_TOKEN, STRIPE_API_KEY, and the like), as cat .env, env, or grep prints it, keeps its name but shows the value as [redacted:env-secret]

So the agent refers to such a value by variable and file rather than repeating it. A file write that would put a [redacted:…] placeholder where a value goes is refused, so writing back a file it read can't replace the real secret with the placeholder.

Credential masking on egress

Houston's own network tools refuse to send a credential:

  • Before a web_fetch or web_search leaves the machine, its URL or query is scanned for a well-known token format or one of this install's stored secret values. The call is refused if one is present.
  • The GitHub tools scan everything they send the same way (titles, bodies, comments, reviews).

This is the outbound counterpart to secret redaction (which scrubs secrets on the way back in), so a prompt-injected agent can't leak a key by pasting it into a request. It covers Houston's own tools; a raw curl inside run_shell is bounded by the egress allowlist instead.

Untrusted content

A fetched page is written by whoever runs the site, an issue comment by whoever posted it, and an MCP result by whoever wrote what the server relays. So Houston treats the text these tools return as data, not instructions, without relying on the agent choosing to see it that way.

Covered tools:

  • web_fetch and web_search
  • GitHub tools that read issue and pull request text: gh_issue_view, gh_issue_list, gh_pr_view, gh_pr_list
  • GitHub tools that read CI: gh_run_view, gh_run_list, gh_pr_checks. A pull request from a fork runs the fork's own workflow files, so its author writes the job and step names and everything its steps print to the log.
  • MCP tool results and resources

Fencing

That text is wrapped in an <untrusted-content-NONCE> fence with a fresh random nonce per call, and the system prompt tells the agent that anything inside is attacker-controlled. The nonce is what makes the fence hold: with a fixed tag, the text could close the fence itself and carry on as if it were trusted.

  • Outside the fence: fields GitHub guarantees, such as an issue's number, state, author, and URL, or a CI run's id, outcome, and URL, so the agent can rely on them.
  • Inside the fence: titles, bodies, labels, branch names, diffs, workflow, job, step, and check names, and CI logs.
  • One fence per comment, with its author shown outside it, so one comment can't pass itself off as a maintainer's reply.
  • GitHub writes (opening, commenting, reviewing, merging, checking out, closing, reopening) report the outcome in a line Houston writes, plus the new item's URL, and fence everything gh printed, since its messages quote titles and branch names.

Isolation

Content is scored for injection signals: text telling the reader to ignore prior instructions, a new persona, an instruction to send data to a URL, references to key material, text telling the reader to call Houston's own tools, and characters hidden from a human reader.

A web page or MCP result that looks like an attempt isn't inlined at all. A separate model call, with no tools and no conversation history, reads it and reports what it says, and only that report reaches the agent. The isolation is structural: there's nothing for the content to act through, and no earlier instructions to talk it out of. If that call fails, the content is withheld rather than passed through.

Warning

GitHub text and web search results that look like an attempt stay inline and fenced, with a warning naming the signals:

  • the agent needs an issue's exact repro steps or a pull request's exact diff to do its job, and a summary wouldn't do
  • search results are short snippets, and any page the agent then opens goes through web_fetch and its isolation
  • diffs and CI logs are fenced but not scored, because code and build output routinely contain the phrases the scoring looks for

Cost and limits

  • Ordinary content is unaffected and arrives verbatim, so documentation and code samples stay byte-exact. Only flagged content pays for the extra call.
  • The score is deliberately loose, because a false positive costs one model call and some fidelity rather than a failed call.
  • Bidirectional text controls, which can make text display in a different order than it reads, are stripped from all fenced content.
  • This is defense in depth behind the approval gate (you still approve network and MCP calls), not a replacement for it. The classifier is a heuristic, and anyone who knows it's there can word around it.