How OpenCode Web Search and Web Fetch Tools Handle External Requests: A Deep Dive into the Permission-First Architecture

Both tools use a permission-first pattern where the model requests access via ctx.ask, waits for explicit user approval, then executes HTTP requests with strict safety limits including 5 MiB size caps, 2-minute timeouts, and automatic content normalization.

OpenCode (anomalyco/opencode) implements external web access through two specialized tools that prioritize security and user consent. This article examines how the WebSearch and WebFetch tools process external requests while maintaining strict runtime boundaries and normalizing content for LLM consumption.

Permission-First Architecture: The Foundation of External Requests

Every external request in OpenCode begins with an explicit permission check. When the model determines it needs web data, it does not connect directly to the internet. Instead, the tool calls ctx.ask with a specific permission identifier and the target URL or query pattern.

This design ensures that:

  • Users or agent policies must explicitly grant webfetch or websearch permissions
  • The exact URL or search query is visible before any network traffic occurs
  • All subsequent network operations run inside the sandboxed OpenCode runtime

WebFetch Tool: Fetching and Normalizing Web Content

The WebFetchTool in packages/opencode/src/tool/webfetch.ts handles single-URL retrieval with comprehensive safety checks and content transformation.

Parameter Validation and URL Security

Before any network request, the tool validates the input URL strictly:

// From webfetch.ts#L23-L27
if (!params.url.startsWith("http://") && !params.url.startsWith("https://")) {
  throw new Error("URL must start with http:// or https://");
}

This prevents file-system access or requests to internal schemes.

Request Execution and Abort Handling

The tool implements robust timeout and cancellation logic using the shared abortAfterAny utility from packages/opencode/src/util/abort.ts:

// From webfetch.ts#L39-L42
const timeout = Math.min((params.timeout ?? DEFAULT_TIMEOUT/1000)*1000, MAX_TIMEOUT);
const { signal, clearTimeout } = abortAfterAny(timeout, ctx.abort);

Default timeout is 30 seconds, capped at 2 minutes (120 seconds).

The tool sends realistic browser headers including a User-Agent string and Accept headers based on the requested format (markdown, text, or html):

// From webfetch.ts#L44-L63
const headers = {
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
  Accept: acceptHeader,
  "Accept-Language": "en-US,en;q=0.9"
};

Content Processing and Size Limits

WebFetch enforces a strict 5 MiB size limit on all responses. The tool checks both the Content-Length header and the actual downloaded body size, aborting if limits are exceeded (see webfetch.ts#L80-L89).

The tool handles content transformation based on MIME type:

  • Images: Converted to Base64 data-URLs and returned as attachments
  • HTML: Converted to Markdown using turndown or to plain text using HTMLRewriter
  • Text: Returned directly with metadata

This ensures the LLM receives normalized, consumable content regardless of the source format.

WebSearch Tool: Querying the EXA MCP Search Backend

The WebSearchTool in packages/opencode/src/tool/websearch.ts abstracts search engine interaction through the EXA MCP (Model Context Protocol) service.

Permission and Request Construction

Like WebFetch, WebSearch begins with a permission request:

// From websearch.ts#L66-L71
await ctx.ask({
  permission: "websearch",
  patterns: [params.query],
  description: `Search the web for "${params.query}"`
});

The tool constructs a JSON-RPC request to the EXA MCP endpoint at https://mcp.exa.ai/mcp:

// From websearch.ts#L79-L92
const searchRequest: McpSearchRequest = {
  jsonrpc: "2.0",
  id: 1,
  method: "tools/call",
  params: {
    name: "web_search_exa",
    arguments: {
      query: params.query,
      numResults: params.numResults ?? 5,
      type: params.crawl ? "keyword" : "neural",
      includeDomains: params.includeDomains,
      excludeDomains: params.excludeDomains,
      text: true
    }
  }
};

SSE Response Parsing and Error Handling

The EXA MCP service returns Server-Sent Events (SSE). The tool parses the stream to extract the first result:

// From websearch.ts#L119-L130
const lines = text.split("\n");
for (const line of lines) {
  if (line.startsWith("data: ")) {
    const data = JSON.parse(line.substring(6));
    if (data.result?.content?.length > 0) {
      return {
        output: data.result.content[0].text,
        title: `Web search: ${params.query}`
      };
    }
  }
}

WebSearch uses a fixed 25-second timeout combined with the session abort signal (websearch.ts#L95-L96). If the request times out or returns no results, the tool returns a friendly fallback message rather than throwing a raw error.

Shared Safety Mechanisms and Design Patterns

Both tools implement identical safety patterns from the OpenCode runtime:

Mechanism Implementation Purpose
Explicit consent ctx.ask with permission identifiers Prevents unauthorized external access
Abort awareness abortAfterAny utility Combines tool timeouts with session cancellation
Size limits 5 MiB maximum response size Prevents memory exhaustion from large payloads
Timeout caps 30s default (WebFetch), 25s fixed (WebSearch), max 2min Prevents indefinite hanging
Content normalization Base64 encoding for images, Turndown/HTMLRewriter for HTML Ensures LLM-compatible output
Modular descriptions External .txt files (webfetch.txt, websearch.txt) Allows human-readable help text without code changes

Implementation Examples

TypeScript SDK Usage

When building custom agents with the OpenCode SDK, both tools follow identical initialization patterns:

import { WebFetchTool } from "@/tool/webfetch"
import { WebSearchTool } from "@/tool/websearch"

async function demo() {
  // Initialize WebFetch
  const fetcher = await WebFetchTool.init()
  const page = await fetcher.execute(
    { url: "https://example.com", format: "markdown" },
    { 
      abort: new AbortController().signal, 
      sessionID: "demo", 
      messageID: "1", 
      ask: async (req) => { 
        // Permission granted programmatically
        return { approved: true } 
      } 
    }
  )
  
  console.log("Title:", page.title)
  console.log("Content preview:", page.output.slice(0, 200))

  // Initialize WebSearch
  const searcher = await WebSearchTool.init()
  const result = await searcher.execute(
    { query: "open source AI SDK", numResults: 3 },
    { 
      abort: new AbortController().signal, 
      sessionID: "demo", 
      messageID: "2", 
      ask: async () => ({ approved: true }) 
    }
  )
  
  console.log("Search summary:", result.output)
}

demo()

CLI Invocation

OpenCode exposes both tools through the command-line interface with automatic permission handling:


# Fetch a markdown version of a documentation page

opencode run webfetch url="https://github.com/anomalyco/opencode/blob/dev/README.md" format="markdown"

# Execute a web search with specific result count

opencode run websearch query="opencode documentation" numResults=5

The CLI surfaces the permission request in the terminal UI; upon approval, it executes the same internal flow defined in webfetch.ts and websearch.ts.

Underlying HTTP Request Structure

When WebFetch executes an approved request, it constructs the following HTTP signature:

// Simplified representation of the fetch call in webfetch.ts#L66-L72
await fetch("https://example.com", {
  method: "GET",
  headers: {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
    "Accept": "text/markdown;q=1.0, text/plain;q=0.8, */*;q=0.1",
    "Accept-Language": "en-US,en;q=0.9"
  },
  signal: abortSignal
})

If Cloudflare returns a 403 challenge, the tool automatically retries with a simplified User-Agent: opencode header to bypass the block.

Key Source Files and Architecture

Understanding the complete request lifecycle requires examining these specific files in the anomalyco/opencode repository:

File Purpose
packages/opencode/src/tool/webfetch.ts Core WebFetch implementation including URL validation, permission requests, timeout handling, Cloudflare bypass logic, and content normalization (Markdown conversion, image base64 encoding).
packages/opencode/src/tool/websearch.ts Core WebSearch implementation handling EXA MCP JSON-RPC requests, SSE stream parsing, and search result formatting.
packages/opencode/src/tool/webfetch.txt Human-readable tool description loaded at runtime to provide model context and UI help text.
packages/opencode/src/tool/websearch.txt Human-readable description for the search tool, enabling dynamic help generation without code modification.
packages/opencode/src/util/abort.ts Implements abortAfterAny, the shared utility that combines tool-level timeouts with session cancellation signals.
packages/opencode/src/cli/cmd/run.ts CLI integration layer that wires opencode run <tool> commands to the underlying tool implementations.
packages/web/src/content/docs/tools.mdx Public documentation covering usage patterns, permission defaults, and format options for both tools.

Summary

  • OpenCode web search and web fetch tools operate on a strict permission-first basis, requiring explicit user approval via ctx.ask before any external network traffic occurs.
  • WebFetch (packages/opencode/src/tool/webfetch.ts) validates URLs, enforces 5 MiB size limits and 2-minute maximum timeouts, handles Cloudflare challenges via User-Agent rotation, and normalizes content into Markdown, plain text, or base64-encoded images.
  • WebSearch (packages/opencode/src/tool/websearch.ts) communicates with the EXA MCP backend via JSON-RPC over HTTP, parses Server-Sent Events (SSE), and returns condensed text summaries with a fixed 25-second timeout.
  • Both tools utilize the abortAfterAny utility from packages/opencode/src/util/abort.ts to ensure requests respect both tool-level timeouts and session-level cancellation signals.
  • Content normalization ensures LLMs receive consumable formats regardless of source MIME types, while modular description files (webfetch.txt, websearch.txt) allow runtime configuration of help text.

Frequently Asked Questions

How does OpenCode prevent unauthorized external web requests?

OpenCode prevents unauthorized requests by requiring explicit permission through the ctx.ask mechanism before any HTTP call. When the model invokes either tool, it passes a permission identifier ("webfetch" or "websearch") and the exact URL or query pattern to the runtime. The runtime surfaces this request to the user or policy engine, and only upon approval does the tool execute the actual network request. This ensures the LLM never contacts the internet directly without consent.

What safety limits does WebFetch enforce on external content?

WebFetch enforces multiple safety limits defined in packages/opencode/src/tool/webfetch.ts. The tool caps response size at 5 MiB, checking both the Content-Length header and the actual arrayBuffer.byteLength to prevent memory exhaustion. It limits request duration to a default of 30 seconds, with a hard maximum of 2 minutes (120 seconds). Additionally, it validates that URLs begin with http:// or https:// to prevent file system access, and uses abortAfterAny to ensure requests respect session cancellation signals.

How does WebSearch differ from WebFetch in terms of implementation?

While both tools follow the permission-first pattern, WebSearch operates as a client to the EXA MCP (Model Context Protocol) service rather than making raw HTTP requests to arbitrary URLs. Located in packages/opencode/src/tool/websearch.ts, it constructs JSON-RPC payloads targeting the web_search_exa tool, sends them via POST to https://mcp.exa.ai/mcp, and parses Server-Sent Events (SSE) to extract results. It uses a fixed 25-second timeout (shorter than WebFetch's configurable timeout) and returns condensed text summaries rather than raw HTML or binary content.

Can OpenCode handle Cloudflare-protected websites when fetching content?

Yes, WebFetch includes specific logic to handle Cloudflare challenges. When a request returns a 403 status code with Cloudflare challenge headers, the tool automatically retries the fetch with a simplified User-Agent: opencode header instead of the standard browser User-Agent string. This bypass mechanism is implemented in packages/opencode/src/tool/webfetch.ts around lines 68-72, allowing the tool to retrieve content from sites protected by basic Cloudflare filters while maintaining standard browser headers for regular requests.

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 →