Guides

Extending Built-in Tools

Customize built-in tools without replacing them. Use tool.before, tool.after, needsApproval, and activeTools to add validation, transformation, and restrictions.

Use tool.before and tool.after hooks to intercept execution, needsApproval to gate specific tools, and activeTools to control which tools are available per-agent or per-session.

The 8 Built-in Tools

ToolDescription
ReadRead file contents with metadata. Supports line ranges for large files.
WriteWrite content to a file. Creates parent directories. Overwrites existing files.
EditReplace an exact string in a file. Fails if the string is not unique.
GrepSearch for patterns using ripgrep. Filter by path, file type, glob.
ListRecursively list directory contents. Control depth and include hidden files.
BashExecute shell commands. Returns stdout, stderr, exit code. Use waitUntil: 0 for fire-and-forget.
SkillLoad a skill's full instructions by name from configured skills sources.
JavaScriptRun JavaScript in the sandbox. Orchestrate multiple tool calls, transform results, parallelize with Promise.all.

All built-in tools execute in the sandbox. See Tools for full details.

Using tool.before to Intercept

The tool.before hook runs before any tool executes—built-in or custom. You can block execution, modify input, or add logging.

Block Dangerous Commands

src/agent.ts
import { agent } from "experimental-agent";
import { vercelSandbox } from "experimental-agent/sandbox";

const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  hooks: {
    "tool.before": async ({ name, input }) => {
      if (name === "Bash" && (input as { command: string }).command.includes("rm -rf")) {
        throw new Error("Destructive commands are not allowed");
      }
      if (name === "Write" && (input as { path: string }).path.startsWith("/etc")) {
        throw new Error("Cannot write to system directories");
      }
      return undefined;
    },
  },
});

Throw to block. The tool never runs and the model receives an error.

Modify File Paths or Commands

Restrict operations to certain directories by transforming input:

src/agent.ts
const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  hooks: {
    "tool.before": async ({ name, input }) => {
      if (name === "Bash") {
        const cmd = (input as { command: string }).command;
        return {
          input: { ...input, command: `cd /workspace && ${cmd}` },
        };
      }
    },
  },
});

Return { input } to replace or modify the input before the tool runs. Return undefined to pass through unchanged.

Add Logging and Auditing

src/agent.ts
const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  hooks: {
    "tool.before": async ({ name, input, context }) => {
      console.log(`[audit] Tool ${name} called with`, input);
    },
  },
});

Use context (ToolContext) to access session, storage, or per-request context for richer audit data.

Using tool.after to Transform Results

The tool.after hook runs after any tool executes. Use it to redact secrets, transform output, or log results.

Redact Secrets from Output

src/agent.ts
function redactSecrets(text: string): string {
  return text.replace(/\b[A-Za-z0-9_-]{20,}\b/g, "[REDACTED]");
}

const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  hooks: {
    "tool.after": async ({ name, result }) => {
      if (name === "Bash") {
        const r = result as { stdout?: string; stderr?: string };
        return {
          result: {
            ...r,
            stdout: r.stdout ? redactSecrets(r.stdout) : r.stdout,
            stderr: r.stderr ? redactSecrets(r.stderr) : r.stderr,
          },
        };
      }
    },
  },
});

Return { result } to replace or modify the result before it returns to the model.

Post-Execution Logging

src/agent.ts
const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  hooks: {
    "tool.after": async ({ name, input, result }) => {
      if (name === "Write") {
        console.log(`[audit] Wrote file: ${(input as { path: string }).path}`);
      }
    },
  },
});

Using needsApproval for Built-in Tools

Map built-in tool names to approval rules. The workflow suspends until session.send({ type: "approval", approval: { approvalId, approved } }) is called.

Always Require Approval

src/agent.ts
const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  needsApproval: {
    Bash: true,
    Write: true,
  },
});

Every Bash and Write call will pause for approval.

Conditional Approval

src/agent.ts
const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  needsApproval: {
    Bash: (input) => (input as { command: string }).command.includes("rm"),
    Write: (input) => !(input as { path: string }).path.startsWith("/tmp/"),
  },
});
  • Bash — Only ask when the command contains rm
  • Write — Ask for any write outside /tmp/

Disable Approval

src/agent.ts
const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  needsApproval: {
    Bash: false,
  },
});

Explicitly set to false if you want to override a default or inherited rule.

See Approvals for the resolution flow and frontend integration.

Using activeTools to Restrict Availability

Limit which tools the agent can use. Useful for read-only sessions, reduced token usage, or security boundaries.

Agent-Level Restriction

src/agent.ts
const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  activeTools: ["Read", "Grep", "List"],
});

Only Read, Grep, and List are available. Write, Edit, Bash, Skill, and JavaScript are disabled.

Per-Session Restriction

Override at session creation for different access levels:

app/api/chat/[chatId]/route.ts
const session = myAgent.session(chatId, {
  activeTools: ["Read", "Grep", "List"],
});

Use case: read-only sessions for guest users, full tools for authenticated users.

Read-Only Sessions

src/agent.ts
const readOnlyAgent = agent("read-only-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  activeTools: ["Read", "Grep", "List"],
});

The agent can explore and search but cannot modify files or run arbitrary commands.

Combining Hooks, Approvals, and activeTools

Use all three together for layered control:

src/agent.ts
import { agent } from "experimental-agent";
import { vercelSandbox } from "experimental-agent/sandbox";

const myAgent = agent("my-agent", {
  model: "anthropic/claude-opus-4.6",
  sandbox: vercelSandbox(),
  activeTools: ["Read", "Write", "Edit", "Grep", "List", "Bash"],
  needsApproval: {
    Bash: (input) => (input as { command: string }).command.includes("rm"),
    Write: true,
  },
  hooks: {
    "tool.before": async ({ name, input }) => {
      if (name === "Write" && (input as { path: string }).path.startsWith("/etc")) {
        throw new Error("Cannot write to system directories");
      }
      if (name === "Bash") {
        return {
          input: {
            ...(input as Record<string, unknown>),
            command: `cd /workspace && ${(input as { command: string }).command}`,
          },
        };
      }
      return undefined;
    },
    "tool.after": async ({ name, result }) => {
      if (name === "Bash") {
        const r = result as { stdout?: string; stderr?: string };
        return {
          result: {
            ...r,
            stdout: (r.stdout ?? "").replace(/password=\S+/g, "password=[REDACTED]"),
            stderr: (r.stderr ?? "").replace(/password=\S+/g, "password=[REDACTED]"),
          },
        };
      }
      return undefined;
    },
  },
});

This setup:

  1. activeTools — Disables Skill and JavaScript
  2. needsApproval — Bash requires approval only for rm; Write always requires approval
  3. tool.before — Blocks writes to /etc; forces Bash to run in /workspace
  4. tool.after — Redacts password-like strings from Bash output

Next Steps