---
title: Built-in Tools
description: Sandboxed file and shell tools for AI agent tasks, with exact input schemas, security policies, and usage examples.
---

```ts
import { tools, read, write, edit, grep, bash, defineTool, getDefinedToolMetadata } from "smithers-orchestrator";
```

`tools` bundles all five tools keyed by name:

```ts
const { read, write, edit, grep, bash } = tools;
```

<Note>API reference: [Tools](/reference/tools) lists every built-in tool and helper, its options, and links to source and tests.</Note>

The `smithers-orchestrator/tools` subpath also exports lower-level helpers for advanced integrations:

| Export | Purpose |
|---|---|
| `readFileTool`, `writeFileTool`, `editFileTool`, `grepTool`, `bashTool` | Call the underlying implementation directly instead of the AI SDK tool wrapper. |
| `getDefinedToolMetadata(tool)` | Read Smithers metadata (`name`, `sideEffect`, `idempotent`) from a `defineTool()` result. |
| `getToolContext()`, `runWithToolContext(ctx, fn)` | Inspect or provide the task-local tool runtime context. |
| `getToolIdempotencyKey(ctx?)`, `nextToolSeq(ctx)` | Build stable idempotency keys and task-local tool-call sequence numbers. |
| `BASH_TOOL_MAX_*` constants | Upper bounds for bash command length, args, cwd, output bytes, and timeout. |

## Sandboxing

All tools are sandboxed to `rootDir` (defaults to the workflow directory). Paths are resolved relative to this root; escapes via symlinks are rejected.

| Policy | Behavior |
|---|---|
| Path resolution | Relative paths resolve against `rootDir`. Absolute paths must fall within root. |
| Symlinks | Rejected if target is outside sandbox. |
| Output size | Process output is truncated to `maxOutputBytes` (default 200KB); `read`, `write`, and `edit` reject files, content, or patches that exceed it. |
| Timeouts | `bash` and `grep` default to 60s; exceeded processes killed with `SIGKILL`. |
| Network | `bash` blocks network commands by default. See [bash](#bash). |

## Tool call state

Smithers creates the `_smithers_tool_calls` table and exposes adapter methods to insert and list rows. The current engine reads that table on retry to build warnings for previously recorded non-idempotent side-effect tool calls. The `defineTool()` wrapper itself does not insert a durable row for every call; it attaches metadata, provides `ctx.idempotencyKey`, and runs the side-effect snapshot hook when a task supplies one.

| Field | Description |
|---|---|
| `runId` | Workflow run ID |
| `nodeId` | Task node that invoked the tool |
| `iteration` | Loop iteration |
| `attempt` | Retry attempt number |
| `seq` | Sequential call counter within the task |
| `toolName` | `read`, `write`, `edit`, `grep`, or `bash` |
| `inputJson` | Serialized input arguments |
| `outputJson` | Serialized output (truncated if over limit) |
| `startedAtMs` | Start timestamp |
| `finishedAtMs` | End timestamp |
| `status` | `"success"` or `"error"` |
| `errorJson` | Error details (if `"error"`) |

## defineTool

`defineTool()` wraps custom [AI SDK](https://ai-sdk.dev) tools with Smithers runtime context, deterministic idempotency keys, side-effect metadata, and the side-effect snapshot hook.

```ts
import { defineTool } from "smithers-orchestrator";
import { z } from "zod";

const placeOrder = defineTool({
  name: "wholefoods.place_order",
  description: "Place a grocery order",
  schema: z.object({
    sku: z.string(),
  }),
  sideEffect: true,
  idempotent: false,
  async execute(args, ctx) {
    return await wholeFoods.placeOrder({
      sku: args.sku,
      idempotencyKey: ctx.idempotencyKey,
    });
  },
});
```

- `ctx.idempotencyKey` is stable across retries and resumes for the same task iteration.
- `sideEffect: true` opts the tool into Smithers side-effect tracking.
- `idempotent: false` marks the tool for retry warnings when a previous attempt has a recorded `_smithers_tool_calls` row.
- `defineTool()` does not persist `_smithers_tool_calls` rows directly; durable rows come from runtime paths that call the Smithers DB adapter.

### Side Effects and Idempotency

Every custom tool that modifies external state **must** declare `sideEffect: true`. This is how Smithers protects your [workflow](/workflows/overview) during retries and resumes. Without it, Smithers treats the tool as a pure read and replays it freely, potentially sending duplicate emails, double-charging payments, or creating duplicate records.

The two flags work together:

| `sideEffect` | `idempotent` | Smithers behavior |
|---|---|---|
| `false` (default) | `true` (default) | Pure read. Safe to replay on retry. No warnings. |
| `true` | `true` | Mutates external state, but calling twice with the same input produces the same result (e.g. an upsert, a PUT request). Safe to replay. No warnings. |
| `true` | `false` | Mutates external state and is **not** safe to replay (e.g. sending an email, placing an order, charging a payment). On retry, Smithers injects a warning listing the tool as already called so the agent can verify external state before calling again. |

With `sideEffect: true` and `idempotent: false`, Smithers does two things on retry:

1. **Warns the agent.** The retry prompt lists which non-idempotent tools were already called.
2. **Provides a stable idempotency key.** `ctx.idempotencyKey` is deterministic for a given task + iteration; pass it to external APIs that support idempotency (Stripe, AWS) to deduplicate.

If your `execute` function has `sideEffect: true, idempotent: false` but omits the `ctx` parameter, Smithers logs a startup warning. This is almost always a bug: you need `ctx.idempotencyKey` to handle retries safely.

```ts
// ✗ Bad: non-idempotent side effect without ctx
const sendEmail = defineTool({
  name: "email.send",
  schema: z.object({ to: z.string(), body: z.string() }),
  sideEffect: true,
  idempotent: false,
  async execute(args) {  // ← missing ctx parameter, Smithers warns
    await mailer.send(args);
  },
});

// ✓ Good: uses ctx.idempotencyKey to deduplicate
const sendEmail = defineTool({
  name: "email.send",
  schema: z.object({ to: z.string(), body: z.string() }),
  sideEffect: true,
  idempotent: false,
  async execute(args, ctx) {
    await mailer.send({ ...args, idempotencyKey: ctx.idempotencyKey });
  },
});
```

### What counts as a side effect

The rule is simple: **if you cannot undo it with `git reset`, mark it as a side effect.**

A side effect is any mutation the runtime should not blindly repeat on retry. If a custom tool talks to an external API, writes to a database, sends a message, or triggers a webhook, mark it.

The built-in `write` and `edit` tools are registered as `sideEffect: true` and `idempotent: false` because their file mutations are not safe to blindly replay on retry; like `bash`, they are treated conservatively. All three built-in mutating tools (`write`, `edit`, `bash`) are side-effecting.

| Tool | Side effect? | Why |
|---|---|---|
| Built-in `read`, `grep` | No | Pure reads |
| Built-in `write`, `edit` | **Yes** | Sandboxed file writes are tracked by git, but replaying on retry could overwrite content that diverged since the first call |
| Built-in `bash` | **Yes** | Arbitrary shell commands may not be safe to repeat |
| Custom tool calling an external API | **Yes** | Mutates state outside the sandbox |
| Custom tool writing to a database | **Yes** | External persistent state |
| Custom tool sending a Slack message | **Yes** | Irreversible external communication |
| Custom tool creating a GitHub PR | **Yes** | External state visible to others |

---

## read

Read a file from the sandbox.

```ts
{ path: string }  // relative to rootDir or absolute
```

Returns file contents as UTF-8. Throws `"File too large"` if size exceeds `maxOutputBytes`.

```ts
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { read, grep } from "smithers-orchestrator";

const codeAgent = new Agent({
  model: openai("gpt-5.6-sol"),
  tools: { read, grep },
});
```

```tsx
{/* outputs comes from createSmithers() */}
<Task id="review" output={outputs.review} agent={codeAgent}>
  Read the file src/auth.ts and identify any security vulnerabilities.
</Task>
```

---

## write

Write content to a file. Creates parent directories as needed.

```ts
{
  path: string      // relative to rootDir or absolute
  content: string
}
```

Returns `"ok"`. Throws `"Content too large"` if content exceeds `maxOutputBytes`. Logs content hash (SHA-256) and byte size; full content is not stored.

```ts
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { write, read } from "smithers-orchestrator";

const writerAgent = new Agent({
  model: openai("gpt-5.6-luna"),
  tools: { write, read },
});
```

---

## edit

Apply a unified diff patch to an existing file.

```ts
{
  path: string    // file to patch
  patch: string   // unified diff format
}
```

Returns `"ok"`. The file must exist. Reads current contents, applies the patch via `applyPatch`, writes back. Throws on size limits (`"Patch too large"`, `"File too large"`) or mismatched context (`"Failed to apply patch"`). Logs patch hash and byte size.

```
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -10,3 +10,4 @@
   const token = jwt.sign(payload, secret);
+  logger.info("Token issued", { userId: payload.sub });
   return token;
```

---

## grep

Search for a regex pattern using `ripgrep`.

```ts
{
  pattern: string    // regex
  path?: string      // directory or file (default: rootDir)
}
```

Returns matching lines with file paths and line numbers (`rg -n` format). Exit code 1 (no matches) returns empty string. Exit code 2 throws stderr as error. Requires `ripgrep` in PATH.

```
src/auth.ts:15:  if (token.expired()) {
src/auth.ts:42:  validateToken(token);
tests/auth.test.ts:8:  const token = createTestToken();
```

---

## bash

Run an executable directly with arguments.

```ts
{
  cmd: string                     // executable path/name; no shell parsing
  args?: string[]                 // arguments
  opts?: { cwd?: string }        // working directory (sandboxed)
}
```

Use `args` for arguments. If you need shell syntax such as pipes or redirects, invoke a shell explicitly, for example `cmd: "sh", args: ["-lc", "..."]`.

Returns combined stdout and stderr. Working directory defaults to `rootDir`. Timeout: 60s (killed with `SIGKILL` via process group). Non-zero exit codes throw.

### Network Blocking

Controlled by `allowNetwork` in `RunOptions`, `--allow-network` on CLI, or server config. Default: blocked.

When blocked, Smithers tokenizes `cmd` plus `args`. Executable basenames are
matched for known network tools, URL tokens are blocked by prefix, and `git`
plus a remote-operation token is blocked.

| Category | Match |
|---|---|
| HTTP clients | executable basename `curl` or `wget` |
| URL tokens | any token starting with `http://` or `https://` |
| Package managers | executable basename `npm`, `bun`, or `pip` |
| Git remote ops | `git` plus a `push`, `pull`, `fetch`, `clone`, or `remote` token |

Local git commands (`git status`, `git diff`, `git log`) are allowed.

<Warning>
**Enforcement vs. bypassable denylist.** True OS-level network isolation is
enforced **only on macOS**, where a blocked `bash` runs under `sandbox-exec`
with a network-deny profile (when `sandbox-exec` is available). On Linux and
every other platform, including the environments Smithers runs in for CI,
cloud, and production, `allowNetwork:false` cannot enforce a kernel sandbox and
degrades to the token/basename denylist in the table above. That denylist is
best-effort defense-in-depth, **not** a security boundary: it is trivially
bypassable by a shell, an interpreter (`python -c` assembling a URL from parts,
bash `/dev/tcp`), or a renamed binary. When isolation is unenforced, Smithers
logs a `TOOL_NETWORK_ISOLATION_UNENFORCED` observability warning. **Do not rely
on `allowNetwork:false` to sandbox untrusted code**. Run untrusted workloads
under a real [`<Sandbox>`](/components/sandbox) provider with egress controls
instead.
</Warning>

```ts
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { bash } from "smithers-orchestrator";

const devAgent = new Agent({
  model: openai("gpt-5.6-terra"),
  tools: { bash },
});
```

```tsx
{/* outputs comes from createSmithers() */}
<Task id="lint" output={outputs.lint} agent={devAgent}>
  Run the linter on src/ and report any issues.
</Task>
```

---

## Using Tools with Agents

Pass tools to an [AI SDK](https://ai-sdk.dev) agent and assign the agent to a [`<Task>`](/components/task):

```tsx
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { createSmithers, read, write, edit, grep, bash, Task } from "smithers-orchestrator";
import { z } from "zod";

const codeAgent = new Agent({
  model: openai("gpt-5.6-luna"),
  tools: { read, write, edit, grep, bash },
  instructions: "You are a senior engineer. Use the available tools to complete tasks.",
});

const { Workflow, smithers, outputs } = createSmithers({
  result: z.object({ summary: z.string() }),
});

export default smithers((ctx) => (
  <Workflow name="refactor">
    <Task id="refactor" output={outputs.result} agent={codeAgent}>
      {`Refactor the function in ${ctx.input.file} to improve readability.`}
    </Task>
  </Workflow>
));
```

The full bundle works too:

```ts
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { tools } from "smithers-orchestrator";

const fullAgent = new Agent({
  model: openai("gpt-5.6-terra"),
  tools,
});
```

## Configuration

| Option | Default | Description |
|---|---|---|
| `rootDir` | Workflow directory | Sandbox root |
| `allowNetwork` | `false` | Allow network commands in `bash` |
| `maxOutputBytes` | `200000` (200KB) | Max output size per tool |
| `toolTimeoutMs` | `60000` (60s) | Timeout for `bash` and `grep` |

```ts
import { Effect } from "effect";
import { runWorkflow } from "smithers-orchestrator";

const result = await Effect.runPromise(runWorkflow(workflow, {
  input: { file: "src/auth.ts" },
  rootDir: "/home/project",
  allowNetwork: false,
  maxOutputBytes: 500_000,
  toolTimeoutMs: 120_000,
}));
```

## See Also

- [Agents and Tools](/agents/overview)
- [Sandbox](/components/sandbox)
- [Common External Tools](/integrations/common-tools)
- [Tools Agent Example](/examples/tools-agent)
