Tools
The agent acts through tools. anyclaude-sdk ships the full Claude Code tool suite — files, shell, search, web, todos — and lets you add your own with defineTool or restrict the set with allow/deny lists.
Built-in tools
All built-ins are exported as ALL_CLAUDE_CODE_TOOLS and are the default when you don't pass tools. Each is a Tool = an OpenAI-shape function definition + a run(input, ctx) implementation.
| Tool | What it does | Key params |
|---|---|---|
bash | Run a shell command in the workspace; returns combined stdout+stderr | command, timeout_ms?, description? |
read_file | Read a file — text, images, PDF, notebooks, binary (see below) | path, offset?, limit?, pages? |
write_file | Write a file, creating parent directories as needed | path, content |
edit_file | Exact string replacement in an existing file | path, old_string, new_string, replace_all? |
multi_edit | Several exact replacements applied atomically to one file | path, edits[] |
delete_file | Delete a file or directory recursively | path |
list_files | List directory contents (defaults to workspace root) | path? |
glob | Find files matching a glob pattern (e.g. src/**/*.ts) | pattern, path? |
grep | Search file contents with a regular expression | pattern, path?, glob?, output_mode?, -i, -n |
notebook_edit | Edit cells in a Jupyter .ipynb notebook | path, cell_id?, new_source, cell_type?, edit_mode? |
todo_write | Create / replace the session's structured todo list | todos[] (content, status, activeForm?) |
web_fetch | Fetch a URL and return clean, readable text | url, prompt?, headers? |
web_search | Search the web and return the top results | query, num_results? |
tool_search | Discover available tools by keyword (handy with large tool sets) | query, limit? |
config | Read or write session configuration key/value pairs | action, key?, value? |
The agent loop runs each tool behind the permission gate; image/PDF bytes returned by a tool are forwarded to the model as a follow-up multimodal user turn automatically.
Multimodal file reading
read_file is content-aware — it returns the right block type for what it finds:
Text
UTF-8 text with cat -n line numbers; slice with offset / limit.
Images
PNG/JPG/… decoded to a base64 image block (downsampled to fit maxImageBytes) and sent to the model.
Rendered per page; request a range with pages (e.g. "1-5"), capped at maxPdfPages.
Notebooks
.ipynb parsed into cells + outputs; edit with notebook_edit.
Binary
Detected and reported (size/type) instead of dumping bytes into the context.
Tuning limits
Pass limits to query() to adjust the read caps (FileReadLimits):
import { query } from 'anyclaude-sdk'
for await (const msg of query({
prompt: 'Summarize report.pdf',
workspace, llm, model,
limits: {
maxSizeBytes: 256 * 1024, // reject text reads over 256 KB (default)
maxTokens: 25_000, // reject text whose estimate exceeds this
maxImageBytes: 3.75 * 1024 * 1024, // max image bytes after downsampling
maxPdfPages: 20, // max PDF pages returned per read
},
})) { /* … */ }Custom tools
Define your own tool with defineTool — a name, description, JSON-Schema parameters, and a run method. run receives the parsed input and a ToolContext (fs, exec, cwd, signal, …) and returns a ToolResult ({ content, isError? }). It can do anything async — hit an API, query a DB, call the sandbox.
import { defineTool, query } from 'anyclaude-sdk'
const weather = defineTool({
name: 'get_weather',
description: 'Current weather for a city.',
parameters: {
properties: { city: { type: 'string', description: 'City name' } },
required: ['city'],
},
run: async ({ city }, ctx) => {
const r = await fetch(`https://api.example.com/wx?q=${encodeURIComponent(String(city))}`, { signal: ctx.signal })
return { content: await r.text() }
},
})
// ctx also exposes the workspace — a tool that reads a file directly:
const countLines = defineTool({
name: 'count_lines',
description: 'Count lines in a workspace file.',
parameters: { properties: { path: { type: 'string' } }, required: ['path'] },
run: async ({ path }, ctx) => {
const text = await ctx.fs.readFile(String(path))
return { content: `${text.split('\n').length} lines` }
},
})extraTools vs tools
extraTools adds your tools to the built-ins; tools replaces the whole set.
query({
prompt: 'What\'s the weather in Paris?',
workspace, llm, model,
extraTools: [weather, countLines], // builtins (read/write/bash/grep…) STILL available
})import { readFile, writeFile } from 'anyclaude-sdk'
query({
prompt: '…',
workspace, llm, model,
tools: [readFile, writeFile, weather], // ONLY these three exist
}) A Tool is just { def, run, maxResultChars? }. defineTool builds the def for you; you can also hand-write the OpenAI function schema if you prefer.
Selecting & disabling tools
Narrow what the agent can use with allowedTools (whitelist — only these) and disallowedTools (denylist — remove these). They apply to built-ins, extraTools, and MCP tools alike. Set them per call or in .claude/settings.json.
// Whitelist: read-only research agent
query({ prompt, workspace, llm, model, allowedTools: ['read_file', 'glob', 'grep', 'web_search'] })
// Denylist: everything except shell + deletion
query({ prompt, workspace, llm, model, disallowedTools: ['bash', 'delete_file'] }){
"allowedTools": ["read_file", "write_file", "edit_file", "glob", "grep"],
"disallowedTools": ["bash"]
}Resolution order: start from tools (or built-ins) + extraTools → apply allowedTools → apply disallowedTools.
Interactive questions — ask_user_question
Ported from Claude Code's AskUserQuestion. The agent can pause and ask the human a multiple-choice question instead of guessing. The tool is registered only when you pass an onAskUser handler to query() — your handler renders the prompt (CLI, web, the <AskUser> React component) and resolves with the chosen label(s).
import { query } from 'anyclaude-sdk'
for await (const msg of query({
prompt: 'Set up linting for this repo.',
workspace, llm, model,
onAskUser: async (q) => {
// q = { question, header?, options: [{ label, description? }], multiSelect? }
// return the chosen label, or string[] when q.multiSelect
return await promptTheUser(q)
},
})) { /* … */ }If no onAskUser is provided the tool isn't offered, so the agent proceeds on its own. In the browser, wire onAskUser to the <AskUser> component.
Client-side tools — clientTools
When the agent runs on a server but a tool must execute on the client (e.g. a browser running bash in a WebContainer, or touching device-local state), list those tool names in clientTools. The server stays the brain; the client is the hands.
When the model calls a client tool, the run emits { type: 'system', subtype: 'client_tool_request' } with the tool name + input and pauses. The client executes it, then you resume the same session passing clientToolResults — the results are injected as the tool's output and the loop continues.
for await (const msg of query({
prompt, workspace, llm, model, sessionId,
clientTools: ['bash'], // these run on the client
})) {
if (msg.type === 'system' && msg.subtype === 'client_tool_request') {
// msg.requests = [{ id, name, input }] — send to the client, then resume
}
}query({
workspace, llm, model, sessionId,
resume: true, continueRun: true,
clientTools: ['bash'],
clientToolResults: [{ id, content: stdoutFromBrowser }],
})See the vercel-clienttools example — a serverless agent whose bash runs in the user's browser via WebContainer.
Large tool outputs
Tools can set maxResultChars. When a result exceeds it, the agent loop spills the full output to a file in the workspace and replaces it in-context with a short preview + the path — so a giant grep or log dump never blows the context window. Self-bounded tools (like read_file) opt out with maxResultChars: Infinity.
const bigReport = defineTool({
name: 'gen_report',
description: 'Generate a large report.',
parameters: { properties: {}, required: [] },
run: async () => ({ content: hugeString }),
maxResultChars: 4000, // beyond this → spilled to a file + preview
})