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 |
|---|---|
|
|
Secrets directory |
|
Age encryption keys |
|
Age decryption |
|
Password store |
|
SSH private keys |
|
GnuPG directory |
|
Destructive Operation Guards
| Control | Implementation |
|---|---|
Root filesystem |
|
Home directory |
|
Shell injection |
|
Force push |
|
Docker operations |
|
Service management |
|
Snapshot Safety Net
With "snapshot": true (default), OpenCode tracks all file changes:
-
Every
editandwriteoperation creates a snapshot -
/undoreverts to previous state -
/redore-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 |
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 |
|
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 |
|---|---|---|
|
Runs automatically, no confirmation |
Trusted operations (git status, read, grep) |
|
Prompts user for approval |
Moderate-risk operations (docker, ssh, service management) |
|
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 |
|
Expands |
|
Expands |
All other characters |
Match literally |
All Permission Keys
| Key | Matches Against | Default |
|---|---|---|
|
File path |
|
|
File path (covers edit, write, apply_patch, multiedit) |
|
|
Glob pattern |
|
|
Regex pattern |
|
|
Directory path |
|
|
Parsed command (tree-sitter parsed, not raw string) |
|
|
Subagent type name |
|
|
Skill name |
|
|
Non-granular (all-or-nothing) |
|
|
Non-granular |
|
|
URL |
|
|
Search query |
|
|
Search query |
|
|
Directory path (outside project root) |
|
|
Tool name (triggered on 3+ identical calls) |
|
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