How OpenCode Handles External Directory Access and Security Boundaries

OpenCode isolates every operation to the project instance directory and requires explicit user authorization through a permission engine before accessing any external path.

The anomalyco/opencode repository implements strict filesystem isolation to prevent tools from accidentally or maliciously accessing sensitive host directories. Every file operation is validated against the project instance boundary, and any attempt to access paths outside this boundary triggers a structured permission flow. This article examines the technical implementation of external directory access and security boundaries in OpenCode.

How OpenCode Detects Out-of-Bounds Access

OpenCode defines the security boundary around the project instance, which consists of the opened directory and its associated git worktree. Before any tool executes a filesystem operation, the runtime validates the target path against this boundary.

The Instance Boundary Check

The Instance.containsPath() method in packages/opencode/src/project/instance.ts performs the containment validation. It checks whether the target filepath falls within the instance's directory or its worktree using the Filesystem.contains() utility.

// packages/opencode/src/project/instance.ts
containsPath(filepath: string) {
  if (Filesystem.contains(Instance.directory, filepath)) return true
  // worktree "/" means no git worktree – skip to avoid false-positives
  if (Instance.worktree === "/") return false
  return Filesystem.contains(Instance.worktree, filepath)
}

If containsPath() returns false, the runtime identifies the operation as an external directory access attempt and initiates the permission request flow.

The Permission Request Flow

When a tool attempts to access an external path, OpenCode constructs a permission request that must be evaluated against the project's security rules or presented to the user for explicit approval.

Building the Canonical Glob

The assertExternalDirectory() function in packages/opencode/src/tool/external-directory.ts handles the construction of the permission request. It creates a canonical glob pattern representing the external location to provide a consistent identifier for permission rules.

// packages/opencode/src/tool/external-directory.ts
const kind = options?.kind ?? "file"
const parentDir = kind === "directory" ? target : path.dirname(target)
const glob = path.join(parentDir, "*")
await ctx.ask({
  permission: "external_directory",
  patterns: [glob],
  always: [glob],
  metadata: { filepath: target, parentDir },
})

The always field contains the same glob pattern, enabling the permission engine to store this rule if the user selects "always allow" for future automatic approval.

Evaluating Against Security Rules

The permission request routes through PermissionNext.ask() in packages/opencode/src/permission/next.ts. This engine evaluates the request against the project's configured permission rules (Config.Permission) and determines one of three actions:

  • Auto-allow: If a rule matches with action: "allow", the operation proceeds without user intervention.
  • Auto-deny: If a rule matches with action: "deny", the runtime throws a DeniedError and blocks the operation.
  • User prompt: If no rule applies, the system presents the user with the permission dialog showing the external directory glob pattern.

When the user approves and selects "always allow," the glob pattern is stored in the PermissionTable, allowing future external directory access to the same location to bypass the prompt.

Bypass and Configuration Options

OpenCode provides controlled mechanisms for scenarios where the security check is unnecessary or already validated.

The Bypass Flag

Callers that have already vetted a path can invoke assertExternalDirectory() with the bypass: true option to skip the permission flow entirely. This is useful for internal utilities that pre-validate paths before calling shared filesystem tools.

await assertExternalDirectory(ctx, "/tmp/outside/file.txt", { bypass: true })

Directory vs. File Handling

The permission system distinguishes between accessing a directory and accessing a file. When kind: "directory" is specified in the options, the glob is constructed against the directory itself. For files (the default), the glob targets the parent directory, ensuring that permission grants cover the file's context rather than individual file instances.

Code Examples

Using assertExternalDirectory in a Custom Tool

The following example demonstrates how to implement external directory security in a custom tool:

import { assertExternalDirectory } from "@/tool/external-directory"

export async function myTool(ctx: Tool.Context, targetPath: string) {
  // Ensure the path is either inside the instance or explicitly allowed
  await assertExternalDirectory(ctx, targetPath)

  // Safe to read or write now
  const content = await Bun.file(targetPath).text()
  return { title: "File content", metadata: {}, output: content }
}

When targetPath is /tmp/outside/file.txt and the instance lives at /my/project, the first call triggers a permission prompt: "Allow OpenCode to read files in /tmp/outside/*?"

Bypassing the Check for Pre-Validated Paths

For scenarios where the path has already been security-checked:

await assertExternalDirectory(ctx, "/tmp/outside/file.txt", { bypass: true })

No prompt is issued; the function returns immediately, allowing the operation to proceed.

Granting Permanent Access via the Permission UI

When the user selects "always allow" in the permission dialog, the glob (/tmp/outside/*) is stored in the project's permission ruleset (PermissionTable). Future calls to assertExternalDirectory targeting paths matching this glob will auto-allow without prompting, streamlining workflows for trusted external directories while maintaining security boundaries for new locations.

Key Implementation Files

The external directory access and security boundary system spans several core files in the OpenCode repository:

These files collectively implement the security boundary that protects the host environment from unintended external directory access.

Summary

OpenCode enforces strict external directory access and security boundaries through a multi-layered validation system:

  • Instance containment – Every path is validated against the project directory and git worktree using Instance.containsPath() before any operation proceeds.
  • Permission gating – Out-of-bounds access attempts automatically trigger structured permission requests via assertExternalDirectory(), creating canonical glob patterns for consistent rule matching.
  • User control – The permission engine evaluates requests against configured rules and prompts the user when necessary, storing "always allow" decisions for future automatic approval.
  • Bypass safety – Pre-validated paths can skip the permission flow using the bypass flag, enabling internal utilities to operate efficiently while maintaining security for external tools.

This architecture ensures that no tool can access sensitive host directories without explicit authorization, preventing both accidental data exposure and malicious filesystem traversal.

Frequently Asked Questions

How does OpenCode determine if a path is outside the project boundary?

OpenCode uses the Instance.containsPath() method in packages/opencode/src/project/instance.ts to check if a target path falls within the project's directory or its git worktree. The method utilizes Filesystem.contains() to compare absolute paths. If the path is not contained within either location, the runtime classifies it as an external directory access attempt requiring permission validation.

What happens when a tool tries to access an external directory without permission?

When assertExternalDirectory() detects an out-of-bounds path, it constructs a canonical glob pattern representing the external location and routes a permission request through PermissionNext.ask(). The permission engine then evaluates this request against the project's rule set. It either auto-allows the operation based on existing rules, auto-denies it with a DeniedError, or prompts the user to approve or deny the specific external directory access.

Can I permanently allow access to specific external directories?

Yes. When the permission prompt appears for an external directory, selecting "always allow" stores the canonical glob pattern (such as /tmp/outside/*) in the project's PermissionTable. Future access attempts to paths matching this glob will automatically pass through assertExternalDirectory() without triggering additional prompts, while still maintaining security boundaries for other unapproved external locations.

Is there a way to skip the permission check for trusted internal operations?

Yes. The assertExternalDirectory() function accepts a bypass: true option that immediately returns without performing the permission check. This is intended for internal utilities that have already pre-validated paths or operate within trusted contexts. However, this bypass should be used cautiously, as it removes the security boundary protection for that specific call.

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 →