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:

  1. Calls ctx.ask for "glob" permission
  2. Resolves the search directory via Instance.directory to absolute path
  3. Uses Ripgrep.files (fast Rust-based file enumerator) to stream matching paths
  4. 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:

  1. Permission request ("grep")
  2. Resolve search directory
  3. Build rg command with --regexp, --glob (if provided), and --field-match-separator=|
  4. Spawn process with Bun.spawn, capture stdout/stderr
  5. Parse lines of format file|line|text
  6. 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.ask for "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.touchFile and FileTime.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:

  1. Resolve absolute path, verify external directory
  2. Compute unified diff via diff.createTwoFilesPatchtrimDiff
  3. Permission request "edit" with diff metadata
  4. Write file using Bun.write, publish File.Event.Edited and FileWatcher.Event.Updated
  5. Trigger LSP via LSP.touchFile, fetch diagnostics via LSP.diagnostics
  6. 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:

  1. Resolve file path, verify external directory
  2. Permission request "lsp" (wildcard * pattern)
  3. Convert to file:// URL and zero-based LSP position
  4. Ensure LSP client attached via LSP.hasClients
  5. Call appropriate LSP method (definition, references, etc.)
  6. 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:

  1. Built-in tools: Core utilities like BashTool, ReadTool, GlobTool, GrepTool, EditTool, WriteTool, and conditionally LspTool (when Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL is enabled)
  2. CLI-specific tools: QuestionTool (only when client === "cli")
  3. Experimental tools: BatchTool (when config.experimental?.batch_tool is true)
  4. Custom tools: Plugin tools from Plugin.list() and user-defined tools discovered in tool/*.ts or tools/*.ts files

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.

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.define for schema validation, ToolRegistry for discovery and registration, and Tool.Context for permission handling.
  • Six built-in toolsglob, grep, read, write, edit, and lsp—provide filesystem access and code intelligence through a unified interface.
  • Security is enforced via the ctx.ask permission model, requiring explicit approval for read, write, and LSP operations before execution.
  • Extensibility is supported through dynamic discovery of custom tools in tool/*.ts files and plugin integration via Plugin.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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →