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

> Discover how OpenCode's web search and fetch tools manage external requests using a permission-first architecture. Learn about user approval, safety limits, and HTTP request handling.

- Repository: [Anomaly/opencode](https://github.com/anomalyco/opencode)
- Tags: deep-dive
- Published: 2026-02-19

---

**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`](https://github.com/anomalyco/opencode/blob/main/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:

```typescript
// 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`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/util/abort.ts):

```typescript
// 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`):

```typescript
// 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`](https://github.com/anomalyco/opencode/blob/main/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:

```typescript
// 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`:

```typescript
// 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:

```typescript
// 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`](https://github.com/anomalyco/opencode/blob/main/webfetch.txt), [`websearch.txt`](https://github.com/anomalyco/opencode/blob/main/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:

```typescript
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:

```bash

# 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`](https://github.com/anomalyco/opencode/blob/main/webfetch.ts) and [`websearch.ts`](https://github.com/anomalyco/opencode/blob/main/websearch.ts).

### Underlying HTTP Request Structure

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

```javascript
// 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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/webfetch.txt), [`websearch.txt`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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`](https://github.com/anomalyco/opencode/blob/main/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.