opencode-damage-control
v1.5.0
Published
OpenCode plugin that blocks dangerous commands and protects sensitive paths
Maintainers
Readme
opencode-damage-control
Defense-in-depth security plugin for OpenCode. Blocks dangerous commands and protects sensitive files before they execute.
Documentation · Issues · Contributing · Security
Why
AI coding agents have shell access, file read/write, and broad autonomy. A single bad command -- whether from a hallucination, prompt injection, or honest mistake -- can:
rm -rf /your filesystemDROP TABLEyour production database- Leak
~/.sshkeys or~/.awscredentials git push --forceover your team's workterraform destroyyour infrastructure
This plugin intercepts every tool call and either blocks or asks for confirmation before dangerous ones run.
Quick Start
Add to your opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-damage-control"]
}Restart OpenCode. Done -- all protections are active with zero configuration.
What It Protects
144 Command Patterns
56 hard-blocked, 88 require confirmation. Covers system destruction (rm -rf /, fork bombs, dd), SQL (DROP TABLE, DELETE FROM, TRUNCATE), git (--force push, filter-branch, stash clear), cloud infrastructure (AWS, GCP, Azure, Terraform, Pulumi), Docker/Kubernetes, databases (Redis, Postgres, MySQL, MongoDB), hosting platforms (Vercel, Netlify, Heroku, Fly.io, Cloudflare, Firebase, Serverless), process/system manipulation (crontab -r, systemctl, iptables, launchctl), and Windows-specific commands (del /s /q, rd /s /q, diskpart, reg delete, bcdedit, PowerShell Remove-Item, Stop-Service, Uninstall-Package).
Shell wrapper unwrapping: Commands wrapped in bash -c "...", sh -c "...", python -c "...", cmd /c "...", powershell -Command "...", pwsh -c "...", env bash -c "...", etc. are automatically unwrapped and inspected. Nested wrappers are handled recursively.
103 Protected Paths
Three-tier protection system for sensitive files:
| Level | Read | Write | Delete | Examples |
|-------|------|-------|--------|----------|
| zeroAccess | Block | Block | Block | ~/.ssh, ~/.aws, .env*, *.pem |
| readOnly | Allow | Block | Block | /etc/, lock files, node_modules/, dist/ |
| noDelete | Allow | Allow | Block | .git/, LICENSE, Dockerfile, CI configs |
Actions
| Action | Behavior | When |
|--------|----------|------|
| block | Hard block. Tool never executes. | Catastrophic commands (rm -rf /, DROP TABLE, terraform destroy) |
| ask | User sees confirmation dialog. | Risky-but-valid commands (git reset --hard, rm -rf, DELETE ... WHERE) |
How It Works
flowchart TD
CALL["OpenCode Tool Call"]
CALL --> EXEC["bash / shell / cmd"]
CALL --> READ["read / glob / grep"]
CALL --> WRITE["edit / write / create"]
EXEC --> UNWRAP["Unwrap Shell Wrappers"]
UNWRAP --> PP["Pattern + Path Check"]
READ --> PC1["Path Check"]
WRITE --> PC2["Path Check"]
PP --> |block| THROW["THROW (hard block)"]
PP --> |ask| STASH["STASH by callID"]
PP --> |no match| ALLOW1["ALLOW"]
PC1 --> |zeroAccess| BLOCK1["BLOCK"]
PC1 --> |otherwise| ALLOW2["ALLOW"]
PC2 --> |zeroAccess / readOnly| BLOCK2["BLOCK"]
PC2 --> |otherwise| ALLOW3["ALLOW"]
STASH --> PERM["permission.ask hook"]
PERM --> DIALOG["CONFIRM DIALOG"]
style THROW fill:#dc2626,color:#fff,stroke:#991b1b
style BLOCK1 fill:#dc2626,color:#fff,stroke:#991b1b
style BLOCK2 fill:#dc2626,color:#fff,stroke:#991b1b
style DIALOG fill:#f59e0b,color:#000,stroke:#d97706
style ALLOW1 fill:#16a34a,color:#fff,stroke:#15803d
style ALLOW2 fill:#16a34a,color:#fff,stroke:#15803d
style ALLOW3 fill:#16a34a,color:#fff,stroke:#15803dHook 1: tool.execute.before -- inspects every tool call. Matches with block throw immediately. Matches with ask are stashed by callID and proceed to the permission system. Protected paths are enforced based on their tier and the operation type.
Hook 2: permission.ask -- looks up stashed matches and forces output.status = 'ask', ensuring the user sees the confirmation dialog even if their permission config would normally auto-allow.
Configuration
Everything works out of the box. To customize, create a damage-control.json in either or both locations:
| Location | Scope |
|----------|-------|
| ~/.config/opencode/damage-control.json | Global (all projects) |
| .opencode/damage-control.json | Project (this repo only) |
Both optional. Project merges on top of global. Invalid config logs warnings and uses defaults.
Schema
{
"patterns": {
"add": [
{ "pattern": "my-dangerous-cmd", "reason": "Custom block", "action": "block" }
],
"remove": ["SQL DROP TABLE"],
"override": {
"Recursive delete from root": "ask"
}
},
"paths": {
"add": [
{ "path": "~/.my-secrets", "level": "zeroAccess" }
],
"remove": ["~/.npmrc"],
"override": {
"~/.docker": "none"
}
}
}Operations
| Operation | What it does |
|-----------|-------------|
| add | Append new patterns/paths after defaults |
| remove | Remove by exact reason (patterns) or path (paths) |
| override | Change action or level. Use "none" to unprotect a path. |
Processing order: defaults → remove → override → add.
When both global and project configs exist: add arrays concatenate, remove arrays union, override objects shallow-merge (project wins).
Examples
Relax a block to ask:
{ "patterns": { "override": { "Terraform destroy": "ask" } } }Add a custom pattern:
{
"patterns": {
"add": [{ "pattern": "prod-db-wipe", "reason": "Wipes production DB", "action": "block" }]
}
}Unprotect a path:
{ "paths": { "override": { "~/.npmrc": "none" } } }What Happens
When something is blocked
The AI agent sees an error and adjusts:
DAMAGE_CONTROL_BLOCKED: SQL DROP TABLE
Command: DROP TABLEWhen something triggers a confirmation
OpenCode shows the standard permission dialog:
damage-control flagged: git reset --hard
[once] [always] [reject]Limitations
- Substring matching for paths. A command that merely mentions a protected path (e.g., in a comment) will be blocked.
- Shell only, not subprocesses. Inspects command strings passed to
bash/shell/cmd. Cannot inspect commands spawned by scripts (but does unwrapbash -c,python -c, etc.). - Pattern ordering matters. First match wins. Specific patterns are ordered before generic ones.
- Ask requires permission system. The
permission.askhook forces the dialog even if the user's config auto-allows, but exact UX depends on OpenCode version.
Development
git clone https://github.com/whjvenyl/opencode-damage-control.git
cd opencode-damage-control
npm install
npm run build # output in dist/
npm test # 454 testsArchitecture
src/
patterns.ts 144 patterns, 103 paths, shell unwrapping, matching helpers
config.ts Config loading, validation, merging
index.ts Plugin entry point (2 hooks)
patterns.test.ts 428 pattern + unwrapping tests
config.test.ts 26 config tests| Module | Exports |
|--------|---------|
| patterns.ts | DEFAULT_PATTERNS, DEFAULT_PROTECTED_PATHS, matchPattern(), checkPathProtection(), checkShellPathViolation(), unwrapShellCommand() |
| config.ts | loadConfig(), applyConfig(), DamageControlConfig |
| index.ts | DamageControl plugin -- loads config at init, returns tool.execute.before + permission.ask hooks |
Contributing
See CONTRIBUTING.md for setup, architecture, and PR conventions.
For security vulnerabilities, see SECURITY.md.
Acknowledgments
Based on the concept from claude-code-damage-control by @disler. Reimplemented as a native OpenCode plugin with zero runtime dependencies.
License
MIT
