OpenCode: Security Posture

Security Posture

OpenCode’s security model mirrors and extends the protections established in Claude Code. The same principles apply: defense in depth, deny-by-default for sensitive operations, never expose secrets.

Permission Layers

Request arrives
├── Global permissions (opencode.json)
│   ├── allow: Permitted without prompt
│   ├── deny: Blocked unconditionally
│   └── ask: User confirmation required
├── Agent permissions (agent .md frontmatter)
│   └── Per-agent allow/deny overrides
├── Mode permissions (active mode)
│   └── Tool whitelist/blacklist
└── Plugin hooks (onToolCall, onPermission)
    └── Programmatic allow/deny with reason

Four layers of permission checking, each more specific than the last.

Secrets Protection

Control Implementation

.env files

deny: ["read(.env*)"] — Blocks all environment file reads

Secrets directory

deny: ["read(~/.secrets/*)"] — Hard boundary

Age encryption keys

deny: ["read(~/.age/*)"] — Private keys off-limits

Age decryption

deny: ["bash(age -d *)"] — Cannot decrypt files

Password store

deny: ["bash(gopass show *)"] — Cannot read entries

SSH private keys

deny: ["read(~/.ssh/id_*)"] — Key files protected

GnuPG directory

deny: ["read(~/.gnupg/*)"] — Keyring protected

Destructive Operation Guards

Control Implementation

Root filesystem

deny: ["bash(rm -rf /*)"]

Home directory

deny: ["bash(rm -rf ~*)"]

Shell injection

deny: ["bash(bash -c *)"]

Force push

ask: ["bash(git push --force *)"] — Requires confirmation

Docker operations

ask: ["bash(docker *)"] — Requires confirmation

Service management

ask: ["bash(systemctl *)"] — Requires confirmation

Snapshot Safety Net

With "snapshot": true (default), OpenCode tracks all file changes:

  • Every edit and write operation creates a snapshot

  • /undo reverts to previous state

  • /redo re-applies reverted changes

  • Snapshots are session-scoped (persist until session ends)

This provides a safety net that Claude Code lacks — any file modification can be rolled back instantly.

Provider Security

Concern Mitigation

API key exposure

Use {env:VAR} references, never hardcode keys in config

Data exfiltration

Local models (Ollama) keep all data on-device

Provider logging

DeepSeek/OpenAI may log prompts — use Ollama for sensitive repos

Browser auth tokens

Stored securely by OpenCode, not exposed to plugins

MCP OAuth

Automatic token refresh, secure storage

Sensitive File Detection

Planned plugin to mirror Claude Code’s UserPromptSubmit hook:

// .opencode/plugins/sensitive-file-guard.ts
export default definePlugin({
  name: "sensitive-file-guard",
  onToolCall({ tool, args }) {
    const sensitivePatterns = [
      /\.env/, /\.key$/, /\.pem$/, /\.credentials/,
      /\.secret/, /\.password/, /id_rsa/, /id_ed25519/
    ];
    if (tool === "bash" && args.command.includes("git add")) {
      // Check staged files against sensitive patterns
      // Warn if match found
    }
  }
});

Security Comparison with Claude Code

Control Claude Code OpenCode

Permission model

Allow/Deny lists

Allow/Deny/Ask + per-agent + per-mode + plugin hooks

Secrets protection

Settings deny + CLAUDE.md + filesystem sandbox

Config deny + plugin enforcement

Destructive guards

Settings deny + autoMode soft_deny

Config deny + plugin blocking

File backup

PreToolUse hook (bash)

Snapshot system (built-in) + plugin option

Undo capability

None

/undo / /redo with full snapshot history

Sensitive staging

UserPromptSubmit hook

Plugin (planned)

Data locality

Cloud-only (Anthropic)

Ollama for full local operation

Permission System Deep-Dive

Permission States

State Behavior Use For

"allow"

Runs automatically, no confirmation

Trusted operations (git status, read, grep)

"ask"

Prompts user for approval

Moderate-risk operations (docker, ssh, service management)

"deny"

Blocks unconditionally

Dangerous operations (rm -rf, secrets access)

Evaluation: Last Matching Rule Wins

Rules are evaluated in order. The last rule that matches takes effect:

{
  "permission": {
    "bash": {
      "*": "ask",              // Catch-all: ask for everything
      "git *": "allow",       // Override: allow all git commands
      "git push --force *": "deny",  // Override: deny force push
      "grep *": "allow"       // Override: allow grep
    }
  }
}

Best practice: Place "*" catch-all first, then progressively specific overrides.

Pattern Matching

Pattern Matches

*

Zero or more characters (wildcard)

?

Exactly one character

~/projects/*

Expands ~ to home directory

$HOME/projects/*

Expands $HOME to home directory

All other characters

Match literally

All Permission Keys

Key Matches Against Default

read

File path

allow (except .env* which is deny)

edit

File path (covers edit, write, apply_patch, multiedit)

allow

glob

Glob pattern

allow

grep

Regex pattern

allow

list

Directory path

allow

bash

Parsed command (tree-sitter parsed, not raw string)

allow

task

Subagent type name

allow

skill

Skill name

allow

lsp

Non-granular (all-or-nothing)

allow

question

Non-granular

allow

webfetch

URL

allow

websearch

Search query

allow

codesearch

Search query

allow

external_directory

Directory path (outside project root)

ask

doom_loop

Tool name (triggered on 3+ identical calls)

ask

Default .env Protection

Out of the box, OpenCode denies reads on environment files:

{
  "permission": {
    "read": {
      "*": "allow",
      "*.env": "deny",
      "*.env.*": "deny",
      "*.env.example": "allow"
    }
  }
}

"Ask" Behavior

When a permission prompts, three options:

Option Effect

once

Approve this specific request only

always

Approve all future matching requests (current session only). OpenCode suggests a glob pattern.

reject

Deny this request

The "always" option is session-scoped. It does not persist between sessions.

Doom Loop Detection

If the LLM makes 3+ identical tool calls in a row, the doom_loop permission triggers. Default: "ask". This prevents infinite loops where the model repeats failed operations.

External Directory Access

When the LLM attempts to access files outside the project root:

{
  "permission": {
    "external_directory": {
      "~/atelier/_bibliotheca/*": "allow",
      "~/atelier/_projects/*": "allow",
      "/tmp/*": "allow"
    }
  }
}

Default: "ask" for all external directories.

Set All Permissions At Once

{
  "permission": "allow"
}
This allows everything including bash, file writes, and external access. Use only for trusted, isolated environments.

Agent-Level Permission Override

{
  "agent": {
    "build": {
      "permission": {
        "bash": {
          "*": "ask",
          "git *": "allow",
          "git commit *": "ask",
          "git push *": "deny"
        }
      }
    }
  }
}

Agent permissions narrow the global scope — they cannot grant permissions that global config denies.

Our Permission Strategy

Global permissions (opencode.json)
├── bash: ask by default, allow for git/make/ls/awk/sed/jq/grep/find
├── read: allow (deny .env*, .secrets, .age)
├── edit: allow for *.adoc, *.sh, *.py, *.ts
├── external_directory: allow for ~/atelier/*
├── doom_loop: ask
└── deny: rm -rf, bash -c, age -d, gopass show

Agent overrides
├── adoc-linter: deny all writes/bash (read-only)
├── build-fixer: allow writes + bash make
└── worklog-creator: allow writes