
Nexo attaches four sandbox-backed tools — `ReadFile`, `WriteFile`, `Shell`, and `Glob` — each gated by the [sandbox](../sandboxes/) and [permission](../permissions/) seams. Which tools attach depends on what the sandbox supports; what they may do depends on the permission mode.

---

## The four sandbox tools

| Tool | Capability | What it does | Attached when |
|---|---|---|---|
| `ReadFile` | `:read` | Read a file from the sandbox | Always |
| `WriteFile` | `:write` | Write a file into the sandbox | Always (gated by the permission mode) |
| `Glob` | `:glob` | Match files by pattern | Always |
| `Shell` | `:shell` | Run a shell command in the sandbox | Only when `sandbox.supports?(:shell)` |

`ReadFile`/`WriteFile`/`Glob` are always attached. `Shell` attaches only when the sandbox supports it — so a `:virtual` agent never advertises a `Shell` tool it can never run. `Local`/`Container` support all four capabilities; `Virtual` supports everything but `:shell` (it raises `NotImplementedError` on purpose — in-memory means no command execution).

> **Safe by default:** under `:read_only`, `:read`/`:glob` are auto-allowed and `:write`/`:shell` are denied — the agent can look but not touch. Grant individual capabilities with `Permissions.new(mode: :read_only, allow: %i[read glob fetch])` without changing the mode.

---

## Shell — output truncation

Unbounded command output (`npm install`, `git log`) is truncated before it reaches the model, so a single command can't blow a small context window. `Tools::Shell` wraps `stdout`/`stderr` through `Nexo::OutputTruncator.call(text, max_lines: 200, max_chars: 16_000)` — strips ANSI escapes, keeps the **last** `max_lines` lines, appends a `…[truncated N lines]` marker, then caps at `max_chars`. The integer `status` passes through untouched.

Pure line/char truncation — **no tokenizer**; configurable via the kwargs only (no global config, no per-agent macro).

---

## WriteFile — read-before-write + stale guard

Within a session, the agent is blocked from overwriting a file it never read, or one that changed underneath it. `Agent#chat` builds one `Nexo::ReadTracker` per chat and threads it into `ReadFile` (records `(path, mtime)` on a successful read) and `WriteFile` (enforces):

- Overwriting an existing, un-read file returns `{error: "read <path> before overwriting it"}`.
- A file whose mtime changed since the read returns `{error: "stale: <path> changed since you read it"}`.
- A new file writes freely.

The guard is **real-FS only** — skipped entirely on `Virtual` (nil `mtime`) and when no tracker is passed (direct tool construction). Best-effort: mtime-based, so a sub-second external edit may slip past the stale check (read-before-write is the primary guard). Clobber-safety within a session only — no versioning, locking, or VCS semantics.

---

## Failure model — errors, not exceptions

A denied capability returns `{ error: ... }` to the model (recoverable) and never raises into the loop — identical to a sandbox tool failure. A path that escapes the workspace raises `SecurityError` (sandbox misuse); everything else surfaces as recoverable context for the model to adapt to.

---

## Web tools — fetch and search

The `fetch` tool for reading the web and the `search` tool for discovering URLs live in the [Web](../web/) guide. They are gated by their own `:fetch` and `:search` capabilities (denied under `:read_only` exactly like `:shell`) plus a host allow-list / an injected backend, and they run in the **host process** — no sandbox constrains them, not even a `--network none` container.

These are sandbox refinements as much as tool behavior — see [Sandboxes](../sandboxes/) for the guard details behind each capability, and [MCP](../mcp/) for attaching external tool servers through the protocol.


---

## Next steps

<div class="not-prose mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2">
  <a href="../mcp/" class="group relative rounded-2xl border border-zinc-200 p-6 hover:border-[#50B1FD]/50 dark:border-zinc-800 dark:hover:border-[#50B1FD]/50 transition">
    <h3 class="font-sora font-semibold text-zinc-900 dark:text-white group-hover:text-[#50B1FD] transition">
      MCP
    </h3>
    <p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
      Attach external MCP servers behind a fail-closed allow-list.
    </p>
  </a>

  <a href="../web/" class="group relative rounded-2xl border border-zinc-200 p-6 hover:border-[#50B1FD]/50 dark:border-zinc-800 dark:hover:border-[#50B1FD]/50 transition">
    <h3 class="font-sora font-semibold text-zinc-900 dark:text-white group-hover:text-[#50B1FD] transition">
      Web
    </h3>
    <p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
      The fetch and search tools for reading and discovering the web.
    </p>
  </a>
</div>
