OpenCode Tool System Architecture: How grep, glob, read, write, edit, and lsp Work Together
OpenCode's tool system uses a modular, permission-aware architecture built around Tool.define, ToolRegistry, and Tool.Context to safely execute filesystem and LSP operations.
The anomalyco/opencode repository implements a plug-in-style tool layer that enables AI agents to interact with the filesystem, search codebases, edit files, and query Language Server Protocol (LSP) services. Understanding this OpenCode tool system architecture is essential for extending the platform or integrating custom tooling.
Core Architecture Components
Tool Definition with Tool.define
At the heart of the system is Tool.define in packages/opencode/src/tool/tool.ts (lines 48-84). This function creates type-safe tool definitions using Zod for parameter validation:
export function define<Parameters extends z.ZodType, Result extends Metadata>(
id: string,
init: Info<Parameters, Result>["init"] | Awaited<ReturnType<Info<Parameters, Result>["init"]>>,
): Info<Parameters, Result> { … }
The define function automatically validates arguments using toolInfo.parameters.parse before execution and wraps results with output truncation via Truncate.output. Each tool returns a Tool.Info object containing the tool's schema, description, and execution logic.
Tool Registry and Discovery
The ToolRegistry in packages/opencode/src/tool/registry.ts (lines 31-119) handles discovery and registration of both built-in and plugin tools. The registry assembles the final tool list in a specific order:
[
InvalidTool,
...(client === "cli" ? [QuestionTool] : []),
BashTool,
ReadTool,
GlobTool,
GrepTool,
EditTool,
WriteTool,
TaskTool,
WebFetchTool,
TodoWriteTool,
WebSearchTool,
CodeSearchTool,
SkillTool,
ApplyPatchTool,
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
...(config.experimental?.batch_tool ? [BatchTool] : []),
...custom // plugins & user-added tools
]
The registry also scans for custom tools in tool/*.ts or tools/*.ts files within user-configured directories and loads plugin tools via Plugin.list().
Tool Context and Permissions
The Tool.Context type in packages/opencode/src/tool/tool.ts (lines 16-27) carries session data and permission handling:
export type Context<M extends Metadata = Metadata> = {
sessionID: string;
messageID: string;
agent: string;
abort: AbortSignal;
extra?: { [key: string]: any };
messages: MessageV2.WithParts[];
metadata(input: { title?: string; metadata?: M }): void;
ask(input: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">): Promise<void>;
};
The ctx.ask method triggers permission requests (e.g., "read", "glob", "edit", "lsp") before executing sensitive operations. The frontend or test harness can grant or deny these requests, protecting the repository from unwanted modifications.
Built-in Tools Deep Dive
glob - File System Pattern Matching
The glob tool in packages/opencode/src/tool/glob.ts returns up to 100 matching file paths ordered by modification time:
export const GlobTool = Tool.define("glob", {
description: DESCRIPTION,
parameters: z.object({
pattern: z.string().describe("The glob pattern to match files against"),
path: z.string().optional().describe("Directory to search; defaults to cwd."),
}),
async execute(params, ctx) { … }
});
Execution flow:
- Calls
ctx.askfor"glob"permission - Resolves the search directory via
Instance.directoryto absolute path - Uses
Ripgrep.files(fast Rust-based file enumerator) to stream matching paths - Collects up to 100 entries, sorts by
mtime, and returns the list
Key implementation details are found in lines 38-56 for Ripgrep.files iteration and lines 43-48 for truncation handling.
grep - Content Search with Ripgrep
The grep tool in packages/opencode/src/tool/grep.ts searches file contents using regular expressions:
export const GrepTool = Tool.define("grep", {
description: DESCRIPTION,
parameters: z.object({
pattern: z.string(),
path: z.string().optional(),
include: z.string().optional(),
}),
async execute(params, ctx) { … }
});
Core steps:
- Permission request (
"grep") - Resolve search directory
- Build
rgcommand with--regexp,--glob(if provided), and--field-match-separator=| - Spawn process with
Bun.spawn, capture stdout/stderr - Parse lines of format
file|line|text - Sort by modification time, truncate to 100 matches, format human-readable output
Key code appears in lines 46-50 for process spawning and lines 77-96 for match parsing.
read - Safe File and Directory Reading
The read tool in packages/opencode/src/tool/read.ts handles file and directory reading with safety limits:
export const ReadTool = Tool.define("read", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string(),
offset: z.coerce.number().optional(),
limit: z.coerce.number().optional(),
}),
async execute(params, ctx) { … }
});
Highlights:
- Resolves relative paths against
Instance.directory - Calls
ctx.askfor"read"permission - For directories: lists entries with pagination (
offset,limit) - For files: detects images/PDFs (returns data-URI) and binary files (throws error)
- Enforces line count (
DEFAULT_READ_LIMIT), line length (MAX_LINE_LENGTH), and byte budget (MAX_BYTES) - Updates LSP and file-time caches via
LSP.touchFileandFileTime.read
Binary detection logic appears in isBinaryFile function (lines 206-260), with result construction in lines 90-104.
write - File Writing with Diff Generation
The write tool in packages/opencode/src/tool/write.ts overwrites files with preview capabilities:
export const WriteTool = Tool.define("write", {
description: DESCRIPTION,
parameters: z.object({
content: z.string(),
filePath: z.string(),
}),
async execute(params, ctx) { … }
});
Execution flow:
- Resolve absolute path, verify external directory
- Compute unified diff via
diff.createTwoFilesPatch→trimDiff - Permission request
"edit"with diff metadata - Write file using
Bun.write, publishFile.Event.EditedandFileWatcher.Event.Updated - Trigger LSP via
LSP.touchFile, fetch diagnostics viaLSP.diagnostics - Format diagnostic snippets (max 20 per file, up to 5 files), embed in output
Key code includes diff generation (lines 34-35), permission request (lines 35-43), and diagnostics handling (lines 60-73).
edit - Batch File Modifications
The edit tool in packages/opencode/src/tool/edit.ts provides a higher-level batch edit capability, wrapping the write tool functionality to handle multi-file modifications used by the Apply Patch tool.
lsp - Language Server Protocol Integration
The lsp tool in packages/opencode/src/tool/lsp.ts executes LSP operations:
export const LspTool = Tool.define("lsp", {
description: DESCRIPTION,
parameters: z.object({
operation: z.enum(operations),
filePath: z.string(),
line: z.number().int().min(1),
character: z.number().int().min(1),
}),
async execute(args, ctx) { … }
});
Flow:
- Resolve file path, verify external directory
- Permission request
"lsp"(wildcard*pattern) - Convert to
file://URL and zero-based LSP position - Ensure LSP client attached via
LSP.hasClients - Call appropriate LSP method (
definition,references, etc.) - Return JSON-formatted results or "no results" message
Dispatch switch appears in lines 62-82, with result handling in lines 85-88.
Tool Registration and Discovery Flow
The ToolRegistry in packages/opencode/src/tool/registry.ts orchestrates how tools become available to the AI model. The registry assembles tools in a specific priority order:
- Built-in tools: Core utilities like
BashTool,ReadTool,GlobTool,GrepTool,EditTool,WriteTool, and conditionallyLspTool(whenFlag.OPENCODE_EXPERIMENTAL_LSP_TOOLis enabled) - CLI-specific tools:
QuestionTool(only whenclient === "cli") - Experimental tools:
BatchTool(whenconfig.experimental?.batch_toolis true) - Custom tools: Plugin tools from
Plugin.list()and user-defined tools discovered intool/*.tsortools/*.tsfiles
The ToolRegistry.state method (lines 34-48) handles dynamic discovery by scanning configured directories for TypeScript tool definitions and loading them at runtime.
Permission Model and Security
Every tool in OpenCode invokes ctx.ask before performing I/O operations, creating a security boundary between the AI agent and the filesystem. The permission request structure includes:
- permission: The operation type (
"read","glob","grep","edit","write","lsp") - patterns: File globs the tool wants to access (e.g.,
[filepath]or["*"]) - always: Patterns always allowed (typically
["*"]for read-only tools) - metadata: Optional diagnostic data for UI rendering (such as diffs for write operations)
The frontend or test harness evaluates these requests and grants or denies permissions, ensuring the AI cannot modify files without explicit user consent. This model applies consistently across all six core tools: glob, grep, read, write, edit, and lsp.
Practical Code Examples
Listing Available Tools
To retrieve the complete list of registered tools for a specific model and client:
import { ToolRegistry } from "@opencode-ai/opencode";
const modelInfo = { providerID: "opencode", modelID: "gpt-4" };
const tools = await ToolRegistry.tools(modelInfo, { name: "cli-agent" });
console.log(tools.map(t => `${t.id}: ${t.description}`));
This demonstrates how ToolRegistry.tools (lines 33-64) filters tools based on model capabilities and experimental flags.
Executing a Glob Search
Programmatically using the glob tool to find TypeScript files:
import { ToolRegistry } from "@opencode-ai/opencode";
async function runGlob(pattern: string) {
const tools = await ToolRegistry.all();
const GlobTool = tools.find(t => t.id === "glob");
const ctx = {
sessionID: "session-123",
messageID: "msg-456",
agent: "test-agent",
abort: new AbortController().signal,
messages: [],
metadata: () => {},
ask: async () => {} // Auto-approve for testing
};
const result = await GlobTool.init().execute({ pattern }, ctx);
console.log(result.output);
}
runGlob("**/*.ts");
This example shows the execute method receiving validated arguments and a context object, returning {title, output, metadata}.
Performing LSP Definition Lookups
Querying language server definitions programmatically:
import { ToolRegistry } from "@opencode-ai/opencode";
async function definitionAt(file: string, line: number, ch: number) {
const tools = await ToolRegistry.all();
const lsp = tools.find(t => t.id === "lsp");
const ctx = /* construct Tool.Context with ask() implementation */;
const result = await lsp.init().execute(
{ operation: "goToDefinition", filePath: file, line, character: ch },
ctx
);
console.log(JSON.parse(result.output));
}
This demonstrates the LSP tool's dispatch mechanism (lines 62-82 in lsp.ts) and JSON result formatting.
Writing Files with Diff Preview
Creating files with automatic diff generation and diagnostics:
import { ToolRegistry } from "@opencode-ai/opencode";
async function writeFile(path: string, newContent: string) {
const tools = await ToolRegistry.all();
const write = tools.find(t => t.id === "write");
const ctx = /* Tool.Context with ask() for "edit" permission */;
const res = await write.init().execute({ filePath: path, content: newContent }, ctx);
console.log(res.output); // includes unified diff and LSP diagnostics
}
This showcases the diff generation via diff.createTwoFilesPatch (lines 34-35) and diagnostic fetching via LSP.diagnostics (lines 60-73).
Summary
- OpenCode's tool system is built on three core concepts:
Tool.definefor schema validation,ToolRegistryfor discovery and registration, andTool.Contextfor permission handling. - Six built-in tools—
glob,grep,read,write,edit, andlsp—provide filesystem access and code intelligence through a unified interface. - Security is enforced via the
ctx.askpermission model, requiring explicit approval for read, write, and LSP operations before execution. - Extensibility is supported through dynamic discovery of custom tools in
tool/*.tsfiles and plugin integration viaPlugin.list(). - Output handling is standardized through automatic truncation (
Truncate.output) and rich metadata support for UI rendering.
Frequently Asked Questions
What is the OpenCode tool system architecture?
The OpenCode tool system architecture consists of a three-layer framework: the definition layer (Tool.define in packages/opencode/src/tool/tool.ts) handles schema validation and execution logic; the registry layer (ToolRegistry in packages/opencode/src/tool/registry.ts) manages tool discovery and model-specific filtering; and the context layer (Tool.Context) provides session management and permission controls. This architecture supports six core tools—glob, grep, read, write, edit, and lsp—through a unified, extensible interface.
How does OpenCode handle permissions for tool execution?
OpenCode implements a mandatory permission system where every tool invokes ctx.ask before performing I/O operations. The permission request includes the operation type (e.g., "read", "edit", "lsp"), file patterns being accessed, and optional metadata such as diffs for write operations. The frontend or test harness evaluates these requests and grants or denies access, ensuring the AI cannot modify files without explicit user consent. This model applies consistently across all tools in packages/opencode/src/tool/.
What is the difference between write and edit tools in OpenCode?
The write tool in packages/opencode/src/tool/write.ts overwrites entire files, generating unified diffs via diff.createTwoFilesPatch and triggering LSP diagnostics after writing. The edit tool in packages/opencode/src/tool/edit.ts provides a higher-level batch editing capability that wraps the write functionality, enabling multi-file modifications typically used by the Apply Patch tool. While write handles single-file atomic replacements, edit orchestrates complex, multi-file refactoring operations.
How does OpenCode integrate LSP functionality into its tool system?
OpenCode integrates LSP through the lsp tool in packages/opencode/src/tool/lsp.ts, which executes Language Server Protocol operations including goToDefinition, references, hover, and symbol queries. The tool converts file paths to file:// URLs and adjusts to zero-based LSP positions before calling methods on attached LSP clients via LSP.hasClients. Results are returned as JSON-formatted data. The LSP integration is gated behind the Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL flag in the registry and requires "lsp" permission via ctx.ask before execution.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →