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
| Tool | Description |
|---|---|
| Read | Read file contents with metadata. Supports line ranges for large files. |
| Write | Write content to a file. Creates parent directories. Overwrites existing files. |
| Edit | Replace an exact string in a file. Fails if the string is not unique. |
| Grep | Search for patterns using ripgrep. Filter by path, file type, glob. |
| List | Recursively list directory contents. Control depth and include hidden files. |
| Bash | Execute shell commands. Returns stdout, stderr, exit code. Use waitUntil: 0 for fire-and-forget. |
| Skill | Load a skill's full instructions by name from configured skills sources. |
| JavaScript | Run 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
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:
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
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
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
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
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
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
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
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:
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
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:
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:
- activeTools — Disables Skill and JavaScript
- needsApproval — Bash requires approval only for
rm; Write always requires approval - tool.before — Blocks writes to
/etc; forces Bash to run in/workspace - tool.after — Redacts password-like strings from Bash output
Next Steps
- Hooks — Full hook API and ToolContext in hooks
- Tools — Built-in tools and custom tools
- Custom Tools — Build tools that use sandbox and context
- Approvals — Resolution flow and frontend integration
- Build Your First Agent — End-to-end setup