Skip to content

whjvenyl/opencode-damage-control

opencode-damage-control

Defense-in-depth security plugin for OpenCode. Blocks dangerous commands and protects sensitive files before they execute.

CI npm version npm downloads License: MIT Built with OpenCode Zero Dependencies

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 filesystem
  • DROP TABLE your production database
  • Leak ~/.ssh keys or ~/.aws credentials
  • git push --force over your team's work
  • terraform destroy your 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.

Full pattern list →

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

Full path list →

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:#15803d
Loading

Hook 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 TABLE

When 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 unwrap bash -c, python -c, etc.).
  • Pattern ordering matters. First match wins. Specific patterns are ordered before generic ones.
  • Ask requires permission system. The permission.ask hook 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 tests

Architecture

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

About

No description or website provided.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors