A tool in OpenCode is a function the LLM can invoke, defined by a schema (Tool.Def), registration info (Tool.Info), and an execute handler; the tool registry filters and resolves them per model, agent, and permission. Tool definitions use Effect Schema for parameters and output, lazy initialization to defer setup, and a context object (Tool.Context) carrying session, message, permission-check functions, and metadata tracking for truncation.
The Tool.Def interface in tool.ts defines a tool's shape: id, description, parameters (Effect Schema decoder), optional jsonSchema override, execute function, and optional formatValidationError for custom schema-error prose.[1] The Tool.Info interface in tool.ts separates tool registration (id) from lazy initialization (init: () => Effect.Effect<DefWithoutID>), allowing tool definitions to be resolved on demand rather than eagerly.[1] Tool.init in tool.ts is a helper that resolves an Info to a full Def by calling info.init() and attaching the tool id.[1] The Tool.Context type in tool.ts carries sessionID, messageID, agent, abort signal, optional callID, optional extra map, messages array, a metadata updater effect, and an ask effect for requesting user permissions.[1] DynamicDescription in tool.ts is a function type (agent: Agent.Info) => Effect.Effect<string> used for tool descriptions that vary per-agent; it is marked as a temporary hack pending a cleaner abstraction.[1]
The Tool.define factory in packages/opencode/src/tool/tool.ts requires both Truncate.Service and Agent.Service as Effect context dependencies, resolving them once per tool definition.[1] In tool.ts, Schema.decodeUnknownEffect (the parameter parser) is compiled once per tool init call — not per LLM invocation — to avoid re-allocating the closure on every tool call.[1] InvalidArgumentsError in tool.ts is the typed error raised when the LLM calls a tool with arguments that fail the parameter schema; its message getter produces model-facing prose instructing the AI to rewrite the input, making it matchable upstream.[1] The wrap function in tool.ts calls agents.get(ctx.agent) to retrieve current agent configuration, then passes it to Truncate.Service to apply agent-specific output truncation limits; if result.metadata.truncated is already set, truncation is skipped.[1] When Truncate.Service truncates a tool's output, tool.ts adds truncated: true and, if applicable, outputPath to the result metadata so callers know the content was cut.[1] In packages/opencode/src/session/tools.ts, time.start is captured once at tool invocation start rather than reset on each log entry, ensuring elapsed-time fields correctly reflect actual tool duration for long-running tools. packages/opencode/test/session/tools.test.ts verifies that time.start is captured at tool invocation start and not overwritten on subsequent log entries.
packages/opencode/src/tool/registry.ts is the central tool registry; it initializes all built-in and custom plugin tools, and exposes Service (tagged @opencode/ToolRegistry) with ids, all, named, and tools methods.[2] The tools method on ToolRegistry.Service accepts providerID, modelID, agent, and optional permission (a PermissionV1.Ruleset) to return the filtered set of tool definitions for a given model invocation.[2] registry.ts uses InstanceState.make to manage per-instance tool registry state (custom and built-in tools), re-evaluating state per instance context.[2] The canonical built-in tool list order in registry.ts is: invalid, optionally question, shell, read, glob, grep, edit, write, task, fetch, todo, search, skill, patch, optionally execute (code mode), optionally lsp, optionally plan.[2] The question tool is included in the built-in list only when the client is "app", "cli", or "desktop", or when the enableQuestionTool flag is set; the lsp tool requires the experimentalLspTool flag; the plan (PlanExit) tool requires both experimentalPlanMode and client === "cli".[2] The execute (CodeMode) tool is conditionally loaded in registry.ts via a dynamic import of "./code-mode" only when the experimentalCodeMode runtime flag is set.[2] registry.ts determines whether web search is enabled based on provider ID (opencode or opencode-go) or the exa / parallel runtime flags, via the exported webSearchEnabled function.[2] The describeTask function in registry.ts filters available subagents by Permission.evaluate("task", item.name, agent.permission), excluding agents denied by the permission ruleset, then sorts them alphabetically.[2] A PermissionV1.Ruleset is a declarative set of allow/deny rules evaluated against a tool action and resource to determine whether a tool call is permitted before execution.
In registry.ts, custom tool files export tools at named exports; a default export uses the file's basename as the tool ID, while named exports use <basename>_<exportName> as the ID.[2] Plugin tools in registry.ts support both Zod-typed args and raw JSON Schema args: if all arg entries are Zod types, a Zod schema is used for validation; otherwise a legacy JSON Schema path is taken.[2] Plugin tools with missing args (pre-1.14.49 compatibility) are normalized to {} in registry.ts rather than passing undefined to Zod.[2] In registry.ts, the fromPlugin wrapper bridges the Effect-based ask permission function into a Promise-returning callback for plugin tools, using EffectBridge.make().[2]
All built-in tools are enabled by default and require no permission to run; tool behavior is controlled via the permission field in opencode.json — see Permissions.[3] The read tool reads files and supports specific line ranges for large files.[3] The glob tool searches for files using glob patterns and returns matching paths sorted by modification time.[3] The grep and glob tools use ripgrep internally; by default, ripgrep respects .gitignore patterns, excluding matched files and directories from searches.[3] To include .gitignore-excluded directories (e.g., node_modules/, dist/) in grep/glob searches, create a .ignore file in the project root with negation patterns like !node_modules/.[3] When handling tool.execute.before or tool.execute.after hooks for the patch tool, check input.tool === "apply_patch" (not "patch"). The tool uses output.args.patchText (not output.args.filePath); paths are embedded in marker lines within patchText and are relative to the project root.[3] The apply_patch tool in apply_patch.ts omits the move-path field entirely when it is absent or empty — rather than serializing an empty value — to produce structurally valid patch payloads; consumers must treat a missing move-path field as equivalent to an empty one.
Define typed tools with Effect Schema, stream a turn, dispatch tool calls locally with ToolRuntime.dispatch, and build follow-up history with LLM.updateRequest
const tools = {
get_weather: Tool.make({
description: "Get current weather for a city.",
parameters: Schema.Struct({ city: Schema.String }),
success: Schema.Struct({ forecast: Schema.String }),
execute: (input) => Effect.succeed({ forecast: `${input.city}: sunny, 72F` }),
}),
}
// Dispatch a tool-call event and build follow-up messages:
const dispatched = yield* ToolRuntime.dispatch(tools, event)
const followUp = LLM.updateRequest(request, {
messages: [
...request.messages,
Message.assistant([event]),
Message.tool({ ...event, result: dispatched.result }),
],
})
Define a named local tool with Tool.make, capturing services at construction time and using execute to sequence permission checks and domain logic.
const grep = Tool.make({
description: "Search file contents",
input: Input,
output: Output,
execute: (input, context) =>
Effect.gen(function* () {
const root = yield* filesystem.resolveRoot(input)
yield* permission.assert({
sessionID: context.sessionID,
agent: context.agent,
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
action: "grep",
resources: [input.pattern],
save: ["*"],
metadata: { root: root.resource },
})
return yield* filesystem.grep(input, root)
}).pipe(/* translate expected typed errors to ToolFailure */),
})
Register one or more tools by name using tools.register; the record key becomes the model-facing tool name.
yield * tools.register({
read,
write,
grep,
})
Sources