Tools
The built-in tool set — read, write, edit, bash, grep, glob, ls, fetch, websearch — parameters, output, and limits.
On this page
The built-in set
@arcturn/tools ships nine tools, built once per agent via createDefaultTools:
{ "read": "…", "write": "…", "edit": "…", "bash": "…", "grep": "…", "glob": "…", "ls": "…", "fetch": "…", "websearch": "…" }| Tool | Permission? | What it does |
|---|---|---|
read |
No | Read a file, optionally a line range, or (for large files) an auto-generated declaration outline. |
write |
Yes | Create or overwrite a file. |
edit |
Yes | Apply an exact find-and-replace edit to an existing file. |
bash |
Yes | Run a shell command, in the foreground or the background. |
grep |
No | Recursively search file contents by regular expression. |
glob |
No | Find files by glob pattern, sorted newest-first. |
ls |
No | List a directory. |
fetch |
Yes | HTTP GET a URL and return readable text. |
websearch |
No | Search the web and return a numbered list of results. |
Those nine are what @arcturn/tools provides. The CLI registers more beside them, from
other packages — memory, subagent and skill have pages of their own (Project
memory, Sub-agents, Model-invoked
skills). Three belong here, because they are part of the same
read-and-edit loop:
| Tool | Permission? | Ships from | What it does |
|---|---|---|---|
search_code |
Asked | @arcturn/index |
An offline BM25 + structural index of the repository, returning file:line addresses rather than file bodies. Registered unconditionally — see Code search. |
todo |
No | @arcturn/core |
The turn's task list. |
plan |
No | @arcturn/core |
Presents a plan for approval; see Sub-agents for plan mode and delegation. |
todo and plan ship from @arcturn/core rather than @arcturn/tools because they mutate
agent state instead of the outside world, and they are on the engine's always-allow list, so
they never prompt. search_code is the odd one: it never calls ctx.requestPermission
itself, but it is not on the read-only list either, so the call still reaches the engine and
is prompted in default mode — see its own
limits for why, and for the rule that settles it.
Every tool's permission decision, when it needs one, ultimately runs through the permission
engine — this page covers what each tool asks for and does once
allowed. sandbox (a filesystem jail for bash, see below), dryRun (route file
mutations to a shadow tree — see Dry-run mode), and taint/canary
(injection defenses — see Injection defense) all layer on top
of these tools without changing their parameters.
# equivalent to: import { createDefaultTools } from "@arcturn/tools";createDefaultTools({ cwd, sandbox }) returns { tools, read, write, edit, bash, grep, glob, ls, fetch, websearch, backgroundTasks } — call it once per agent, since it hands
back a fresh BackgroundTaskManager per call, so two agents never share background task
state.
Two features change what actually reaches the model on top of these definitions, without
changing any tool's parameters: an oversized result from any of these tools (bash and
MCP tools especially) can be written to a file and replaced with a stub — see
Context management — and, opt-in, most tool schemas can be
withheld from the request entirely until the model asks for them via tool_search — see
Deferred tools.
multiedit: reserved, and currently inert
Three lists in the codebase name a multiedit tool. No package registers one.
createDefaultTools returns exactly the nine tools above, and the CLI's
BUILT_IN_TOOL_NAMES — the list an extension may not shadow — does not carry the name in
either spelling.
| Where the name appears | What that list decides | Effect today |
|---|---|---|
DEFAULT_EDIT_TOOLS (@arcturn/core) |
Which tools acceptEdits auto-approves |
None. Nothing is named multiedit, so the entry matches no call. |
WRITE_TOOLS (/workflow dispatch) |
Which lane a role's step runs on | A role that declares multiedit is classed write lane — a worktree, a captured patch — while holding no tool that can write. |
DEFAULT_ALWAYS_ACTIVE_TOOLS (deferred tools) |
Which schemas are never withheld | None. The spelling there is multi_edit, and a name matching no tool is ignored. |
Two spellings, three lists, zero tools. The name is reserved for a batch-edit tool that has
not shipped. Until it does, "acceptEdits auto-approves multiedit" is a statement about
an empty set, and the only observable consequence of the name anywhere is that middle row.
Arcturn's security page says adversarial review found features that were present but unreachable. This is that class, found in Arcturn's own documentation rather than its code: pages across these docs described a tool the harness has never registered. They now describe what is there. The name stays in the table above so the reservation is visible instead of being rediscovered.
read
Reads a file, or, for large files, a structural outline instead of the body.
| Parameter | Type | Required | Notes |
|---|---|---|---|
path |
string | yes | Absolute or relative to the working directory. |
offset |
number | no | 1-indexed starting line. Default 1. Forces literal-line mode. |
limit |
number | no | Max lines returned. Default 2000. Forces literal-line mode. |
outline |
boolean | no | Force (true) or suppress (false) an outline. Ignored if offset/limit given. |
- Text output is formatted like
cat -n: right-padded line number, tab, text. Individual lines are truncated past 2000 characters. - Image files (
.png,.jpg,.jpeg,.gif,.webp) are returned as base64-encoded image content blocks instead of text. - Files at or above 16,384 bytes auto-outline: instead of the body,
readreturns a structural declaration list (kind, name, signature, line number) so a large file never floods context. Passingoffsetorlimitalways returns literal lines and skips the outline regardless of size; an outline attempted but unavailable (unrecognized language, minified content, no declarations found) silently falls back to a truncated body. - Output past the line limit is followed by
[Showing lines A-B of N. Use offset=B+1 to continue reading.]. - Requires no permission — reading is always allowed.
write
Creates or overwrites a file, including any missing parent directories.
| Parameter | Type | Required |
|---|---|---|
path |
string | yes |
content |
string | yes |
Always requests permission first (Create file <path> or Overwrite file <path>), with
a suggested "always allow" rule scoped to the parent directory (<dir>/**). On success,
returns Created <path> (N bytes). or Updated <path> (N bytes)..
edit
Exact substring replacement in an existing file.
| Parameter | Type | Required | Notes |
|---|---|---|---|
path |
string | yes | File must already exist — edit never creates one. |
oldText |
string | yes | Must match file contents verbatim, including whitespace. |
newText |
string | yes | Must differ from oldText. |
replaceAll |
boolean | no | Replace every occurrence instead of requiring a unique match. |
Fails with a clear error if the file doesn't exist, oldText isn't found, or (without
replaceAll) oldText matches more than once — the model is told the occurrence count
and asked to add more context or pass replaceAll: true. On success, requests permission
(subject: the file path, suggested rule allowing that exact path) and returns a unified
diff alongside the replacement count.
bash
Runs a shell command through the platform's shell: /bin/sh -c on macOS/Linux, or
%ComSpec% (cmd.exe by default) on Windows, which has no /bin/sh to fall back to.
This is a real behavior difference, not a shim — the tool's own description names the
shell it's running under so the model writes commands for the platform it's actually on;
POSIX idioms (single-quoted strings, $(...), heredocs) fail under cmd.exe the same as
they would if you typed them there yourself. See
Platform support. Stdout and stderr are merged.
| Parameter | Type | Required | Notes |
|---|---|---|---|
command |
string | yes | |
timeoutMs |
number | no | Foreground only. Default 120,000ms (120s), capped at 600,000ms (600s). |
background |
boolean | no | Start the command and return a taskId immediately instead of blocking the turn. |
Always requests permission first, with a suggested rule allowing the command's first word
followed by a wildcard (e.g. git *). Output is truncated to roughly the trailing 50KB
either way. Non-zero exit codes mark the result as an error, and the exit code is always
appended: [Exit code: N].
Background bash
{ "command": "npm run build:watch", "background": true }The BackgroundTaskManager spawns the command detached, as its own process group leader,
buffers merged stdout/stderr (same ~50KB trailing cap), and exposes poll/kill/list
operations. The runtime surfaces backgroundTaskStart / backgroundTaskOutput /
backgroundTaskEnd events as the command runs, so a UI — or the model, on its next turn —
can check in without blocking. Killing a task sends SIGTERM to its whole process group,
then SIGKILL after a 2 second grace period if it hasn't exited. Background commands
are never sandboxed (see below) — requesting background: true while sandbox is
"workspace-write" is refused outright rather than silently running unsandboxed.
grep
Pure-JS recursive regex content search — no dependency on system grep.
| Parameter | Type | Required | Notes |
|---|---|---|---|
pattern |
string | yes | JavaScript regex source, no surrounding slashes. |
path |
string | no | Directory to search. Defaults to cwd. |
glob |
string | no | Restrict which files are searched, e.g. **/*.ts. |
caseInsensitive |
boolean | no | |
contextLines |
number | no | Lines of context before/after each match. Default 0. |
Skips .git and node_modules, and skips files that sniff as binary (a NUL byte in the
first 8000 bytes). Capped at 200 matches total, after which results are truncated with
a note to narrow the pattern, path, or glob. Each match line is formatted
path:N:text (or path-N-text for context lines), joined by -- between match blocks.
Requires no permission.
glob
Finds files by glob pattern via tinyglobby.
| Parameter | Type | Required | Notes |
|---|---|---|---|
pattern |
string or string[] | yes | One or more glob patterns. |
path |
string | no | Base directory. Defaults to cwd. |
Skips .git and node_modules. Results are sorted most-recently-modified first and
capped at 500 results. Requires no permission.
ls
Lists a directory's contents.
| Parameter | Type | Required |
|---|---|---|
path |
string | no (defaults to cwd) |
Directories are suffixed /; files show a human-readable size (B/KB/MB/GB).
Sorted directories-first, then alphabetically. Capped at 500 entries. Requires no
permission.
fetch
HTTP GET a URL and return its content as readable text.
| Parameter | Type | Required | Notes |
|---|---|---|---|
url |
string | yes | Must be http: or https:. |
maxBytes |
number | no | Default 102,400 (100KB). |
Redirects are followed. HTML responses (content-type: text/html) have tags stripped and
entities decoded down to readable plain text. A 30 second timeout applies. Always requests
permission first, with the subject and suggested "always allow" rule scoped to the URL's
origin (not the full URL), so allowing one fetch to https://api.example.com/v1/x
covers future fetches to that origin.
websearch
Searches the web and returns numbered results.
| Parameter | Type | Required | Notes |
|---|---|---|---|
query |
string | yes | |
maxResults |
number | no | Default 5, capped at 10. |
Uses the Brave Search API when BRAVE_API_KEY is set:
export BRAVE_API_KEY=...Without a key, falls back to scraping DuckDuckGo's HTML results page — no key required,
no extra setup. Output is title — url per result with an indented snippet underneath. A
15 second timeout applies. websearch never calls ctx.requestPermission — it can't
mutate anything, so it runs in every permission mode without a prompt.
Background tasks and todos, together
bash's background mode and the todo tool (from @arcturn/core, described above) are
meant to be used together: the model starts a long build or test watcher in the
background, records it as an in-progress todo, and checks the task's output on a later
turn instead of blocking the whole session on it. Neither tool talks to the other
directly — the model is what threads a taskId through the todo text if it wants to.
Filesystem sandbox for bash
Set sandbox to "workspace-write" in .arcturn/config.json (default "off") to have
the OS itself deny writes outside a small set of roots for bash's foreground
commands — the working directory, the OS temp directory, and $HOME/.arcturn. Reads,
network access, and process spawning are untouched; only file writes are restricted.
{ "sandbox": "workspace-write" }The backend is platform-specific:
| Platform | Mechanism |
|---|---|
| macOS | sandbox-exec -p <profile>, denying file-write* outside the writable roots |
| Linux | bwrap, with / bound read-only and the writable roots bound read-write |
| Anything else, or a missing binary | Runs unsandboxed |
When sandboxing was requested but couldn't be applied — no bwrap on PATH, an
unsupported platform — the command still runs, and a
note: sandbox requested but unavailable on this platform line is prepended to its
output so the gap is visible rather than silent. Background commands are never sandboxed
(see above). Full permission-mode interaction is covered in
Permissions; dry-run and taint/canary defenses that also touch tool
output are covered in Dry-run mode and
Injection defense respectively.
The Tool contract
Every tool — built-in, MCP-bridged, or one you write — implements the same shape from
@arcturn/types:
{ "definition": { "name": "…", "description": "…", "parameters": "JsonSchema" }, "execute": "fn(input, ctx) -> ToolResult" }ToolExecutionContext is what the runtime hands every tool call: cwd, signal (aborts
on user interrupt), requestPermission (ask before a sensitive action), onUpdate
(stream incremental progress), sessionId, toolCallId.
A ToolResult carries rendered content plus an optional details payload for UIs that
want structured data instead of parsing text: content (array of text/image blocks),
isError (optional), details (optional, arbitrary structured data — every built-in tool
above populates one, e.g. EditToolDetails { path, replacements, diff }).
Tools should resolve with isError: true for expected failures (a missing file, a
non-zero exit code) and only reject the promise for genuine programming errors — the
runtime turns a rejection into a notice event, not a clean tool result the model can
react to.
Permission subjects
Tools don't talk to the permission engine directly for the decision — that's the
runtime's job — but they do report a subject: the specific thing being acted on
(a command, a path, a URL), which the permission engine matches rule specifiers against.
defaultSubject reads it from whichever of these keys the tool's input carries first:
command, file_path, filePath, path, url, pattern, query, target. That's
why a rule like { tool: "bash", specifier: "git *", action: "allow" } matches any bash
call whose command starts with git . See Permissions for the
full rule schema, scopes, and the four permission modes.
Writing your own tool
A custom tool is just an object matching the Tool interface — add it to the tools
array passed to createAgent:
{
"definition": {
"name": "deploy",
"description": "Deploy the current branch to the staging environment.",
"parameters": { "type": "object", "properties": {}, "additionalProperties": false }
}
}Its execute(input, ctx) calls ctx.requestPermission({ toolName, toolCallId, subject, description }), checks decision.behavior === "allow", does the work, and returns a
ToolResult. See Embedding with the SDK for a full runnable example.
For tools that need to read or mutate agent state (todos, plan, permission mode) rather
than the outside world, implement BindableTool instead — its bindAgent(controller)
hook receives an AgentStateController the first time the tool is attached to an agent.
createTodoTool and createPlanTool in @arcturn/core are the reference
implementations: see Sub-agents for how plan mode uses this to gate
execution on user approval.