All articles
Skills, Tools & MCP·September 28, 2025·8 min read

The Built-In Toolbox: web_search, bash, and a Sandbox per Agent

Every Matrix agent can opt into seven INTERNAL-transport built-in tools — web_search, fetch_url, bash, file_*, grep — each scoped to a per-(org, agent) sandbox. No keys, no quota.

By Matrix Team

Most agent tools are an HTTP callback away. You register an endpoint, you wire up auth, you pray the third-party rate limit holds, and you eat the latency of a round trip every time the model wants to grep a file or look something up. For the basics — searching the web, fetching a page, running a shell command, reading a file — that's a lot of ceremony for a function that could just run in-process.

Matrix ships seven of those basics as built-in tools: web_search, fetch_url, bash, file_read, file_write, file_list, and grep. They don't route through an HTTP callback. They route through BuiltinToolRegistry — they execute inside the backend JVM — and each one is scoped to a sandbox keyed per (org, agent). No API keys. No external quota. One skill attachment turns the whole set on.

This is the ai agent tools sandbox layer, and this post is the honest tour: how the tools work, how they get into every org, and exactly where the sandbox stops protecting you.

INTERNAL transport: tools that don't leave the process

In the generic entity model, a Tool is just an EntityNode. What distinguishes a built-in is its transport. A normal tool carries an HTTP transport and is dispatched by HttpToolCallback; an MCP-backed tool talks to an external server. A built-in carries an INTERNAL transport, and AgentToolSurface.composeForCaller resolves it through BuiltinToolRegistry — a lookup into in-JVM implementations under org.au.tools.builtin.* — instead of making a network call.

That composition is the same canonical path every other capability rides. For each turn, AgentToolSurface unions the agent's direct tools, its MCP servers, every attached skill's tools, the auto-attached search_knowledge tool, the memory built-ins, the ambient clock — and the BuiltinToolRegistry lookups for any INTERNAL-transport tools the agent has adopted. The model sees one flat tool surface. It has no idea some of those functions are an HTTP hop away and others are a method call.

The payoff of INTERNAL transport is exactly what you'd expect from removing the network: no key management, no per-call quota, no callback URL to keep alive, and the latency of a function call instead of a request.

The seven tools

web_search and fetch_url — read the open web, no key

web_search(query, max_results) scrapes DuckDuckGo's HTML results page and returns title / url / snippet for each hit. There is no API key and no quota — it's an HTML scrape, not a search API. That's a deliberate trade: you get web search on every agent for free, and you accept that you're at the mercy of a scraped page rather than a contracted SLA.

fetch_url(url, max_chars) is the natural follow-up: a JDK HttpClient GET, with the response run through Jsoup to convert HTML into readable text and truncated to max_chars. Search to find the page, fetch to read it.

web_search("CoALA cognitive architecture paper", max_results=5)
  → [{ title, url, snippet }, ...]

fetch_url("https://arxiv.org/abs/2309.02427", max_chars=8000)
  → "Cognitive Architectures for Language Agents ... (Jsoup-extracted text)"

bash — a real shell, fenced into the sandbox

bash(command, cwd?, timeout_seconds) runs an actual command via ProcessBuilder. Three things make it usable from an agent:

  • cwd is constrained to the sandbox. The working directory can't escape the agent's sandbox root, so a command can't casually cd / and wander the host filesystem.
  • stdout and stderr are captured and returned together, so the model sees the full result of what it ran — including the error output it needs to self-correct.
  • It's killed on timeout. A runaway command doesn't hang the turn; it gets terminated when timeout_seconds elapses.

This is what makes bundled skill files useful. SkillSandboxMaterializer writes every SkillFile row for the agent's attached skills into the sandbox before composition — so when the model runs bash, the skill's scripts/ and templates/ are sitting right there, as if the folder were checked out locally.

file_read, file_write, file_list, grep — sandbox-scoped I/O

The four filesystem tools do what they say, and all four are sandbox-scoped. The enforcement lives in one place: Sandbox.resolve rejects any path that escapes the sandbox root — including symlink-escape rejection, so you can't file_write a symlink that points outside and then read through it. Every path the agent hands a file tool gets normalized and checked before any I/O happens.

How they reach every org: BuiltinToolsSeeder

Built-ins aren't something you create per agent. BuiltinToolsSeeder runs on boot and creates one Tool row per implementation in every org — so the moment a tenant exists, the seven tools exist as attachable entities under it.

The interesting wrinkle is how it enumerates orgs. EntityManager.listEntities only ever sees the current tenant's rows — that's the whole point of tenancy being the floor under every query. A seeder that needs to touch every org can't go through it. So BuiltinToolsSeeder uses the raw Neo4j Driver to walk all orgs across tenants and upsert into each.

BuiltinToolsSeeder (on boot)
  └─ raw Neo4j Driver  ──> for each org:
       upsert Tool { transport: INTERNAL, name: "web_search", ... }
       upsert Tool { transport: INTERNAL, name: "bash",        ... }
       ... 7 rows per org

That cross-tenant-boot-via-raw-Driver pattern shows up wherever the platform needs to do something for all orgs at startup. If you ever add a boot operation that has to span tenants, reach for the raw Driver — the tenant-filtered EntityManager will silently only touch the org you happen to be running as.

One attachment: the toolkit-essentials skill

You don't attach seven tools to an agent one by one. The bundled toolkit-essentials skill groups all seven built-ins. Attach that one skill (it's seeded via scripts/create-skill-toolkit-essentials.sh) and the agent's tool surface picks up the entire toolbox in a single move.

This is the skill primitive doing what it's for: bundling a set of capabilities so a builder opts in once. (Skills can carry far more than tool refs — a prompt block, memory-field requirements, bundled files. For the full mechanics and how to import skills straight from a GitHub URL, see Import Any Anthropic Agent Skill From a GitHub URL.)

Be honest about the security posture

Here's the part you need before you turn bash loose, and we're not going to soft-pedal it.

The sandbox is path-restricted only. Sandbox.resolve keeps file and shell operations fenced inside the per-(org, agent) directory and rejects symlink escapes. That's the entirety of the isolation. There are no cgroups, no seccomp, no namespace isolation. The process runs with the backend's own privileges and the backend's own network access.

The practical consequence: bash can reach the network. A command can curl an arbitrary host. So a malicious skill — one that ships a script designed to exfiltrate — could, if you attach it and give the agent bash, phone home with whatever it can read in that sandbox.

The rule that follows is simple and load-bearing:

Don't attach bash to agents that adopt skills from untrusted sources.

Built-ins paired with skills you wrote or vetted: fine. Built-ins, specifically bash, on an agent running a community skill you imported off some GitHub URL and didn't read: that's a path-restricted shell with network access executing third-party code in your process. Treat the import like you'd treat any dependency you're about to npm install and run — because that's exactly what it is.

The sandbox is a cache, not durable storage

One more operational note that bites people in production. On Cloud Run, /tmp is per-container and ephemeral. The sandbox lives under /tmp/matrix-sandbox/{orgId}/{agentId}/, which means anything written there can vanish when the container recycles.

That's by design, and it's why SkillSandboxMaterializer re-materialises the skill files from Neo4j on every turn rather than assuming they persisted. The sandbox is a working scratch space and a cache of skill files — not a place to keep durable state. If your agent's workflow needs to remember something across turns, that's what the memory layer is for; if it needs to persist a result, write it to an entity. Don't expect a file you file_write this turn to still be there next turn, and absolutely don't build a feature that depends on it.

Takeaway

The built-in toolbox is the "batteries included" answer to the most common agent tools: search the web, fetch a page, run a command, touch the filesystem. They route through BuiltinToolRegistry over INTERNAL transport instead of an HTTP callback — so no keys, no quota, function-call latency — and BuiltinToolsSeeder plants all seven in every org on boot via the raw Driver. One toolkit-essentials attachment turns them on. The sandbox keeps file and shell I/O fenced to a per-(org, agent) directory with symlink-escape rejection — but it is path-restricted only: no cgroups, no seccomp, no network jail. So pair bash with skills you trust, and never lean on the sandbox as durable storage.

These built-ins are one of four primitives you compose agents from — and they sit right next to MCP, which lets your agents call external servers and turns Matrix itself into an MCP server under the same JWT. See MCP Both Directions: Your Platform as Client and Server.

Ready to build? Spin up a workspace, attach toolkit-essentials to an agent from /admin/skills, and watch it search, fetch, and grep with zero key management — the whole toolbox is one click in. Then read docs/ARCHITECTURE.md for the composition path that wires it all together.

#ai agent tools sandbox#bash#web search#security

Build your first agent on Matrix

Spin up a workspace, wire up tools and knowledge, give your agent a voice, and talk to it in real time — no agent code required.

Keep reading