Home Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other All categories Projects About Tools

OpenCode Security Agent: Runtime Protection for AI Agents

Leer en espanol
OpenCode Security Agent: Runtime Protection for AI Agents

Table of contents

AI code agents like OpenCode have access to powerful tools: shell, file reading/writing, network access. This is necessary ===

The problem: AI agents with full access to your system

AI code agents like OpenCode have access to powerful tools: shell, file reading/writing, network access. This is necessary for them to be useful, but it opens up a real attack vector.

The Skills and MCP servers ecosystem is young. Recent studies show that ~36% of AI agent skills contain security flaws, more than 138 CVEs have been tracked, and thousands of malicious skills have been identified in public records. A single compromised skill can:

  • Exfiltrate SSH keys, API tokens or cloud files
  • Inject malicious code in your repository
  • Set reverse shells to external servers
  • Modify your settings shell without you noticing

The Postmark MCP case (September 2025)

The canonical example is the Postmark MCP incident. A skill that was clean and trusted for fifteen versions sent a silent update (v1.0.16) that BCCed every email the user sent to an external domain controlled by the attacker. Static analyzes of v1.0.15 would have found nothing — the attack came in a seemingly routine update.

This type of supply-chain attack is the most difficult to detect: It's not that you install something malicious, it's that something you already trusted becomes malicious.

What is OpenCode Security Agent

OpenCode Security Agent is a runtime security plugin for OpenCode that implements three layers of protection:

  1. Layer v1 (static analysis)- Scans installed skills and MCP servers against multiple vulnerability databases and analyzes the code for suspicious patterns.
  2. Layer v2 (runtime protection): an OpenCode plugin with hook tool.execute.before which inspects every tool call before it runs and blocks dangerous ones.
  3. Layer v3 (Semgrep SAST): static analysis with Semgrep using custom rules designed to detect backdoors in skills/MCPs, plus 40 community rules from the official repository semgrep/semgrep-rules. Zero LLM cost, deterministic, offline.

Architecture

CODE
+-----------------------------------------------------------+
|                    OpenCode Runtime                        |
|                                                           |
|  +-------------+     +-------------------+                |
|  | Agent (LLM) |---->| tool.execute.     |                |
|  +-------------+     |  before hook      |                |
|                       |  security-agent.ts|                |
|                       +--------+----------+                |
|                                |                           |
|                       +--------v----------+                |
|                       | sentinel_preflight|                |
|                       |      .py          |                |
|                       +--------+----------+                |
|                                |                           |
|                       +--------v----------+                |
|                       |   iocs.json       |                |
|                       |  (IOC library)    |                |
|                       +-------------------+                |
|                                |                           |
|                          ALLOW / DENY                      |
+-----------------------------------------------------------+

+-----------------------------------------------------------+
|                Pre-install / CI/CD scan                    |
|                                                           |
|  +-------------+     +-------------------+                |
|  | Skill/MCP   |---->| scan_semgrep.sh   |                |
|  | source code |     | (wrapper)         |                |
|  +-------------+     +--------+----------+                |
|                                |                           |
|                    +-----------+-----------+               |
|                    |                       |               |
|           +--------v-------+  +------------v---------+    |
|           | Custom rules   |  | Community rules      |    |
|           | (4 files)      |  | semgrep/semgrep-rules|    |
|           | MCP-specific   |  | (40 files)           |    |
|           +----------------+  +----------------------+    |
|                                                           |
|              text / json / SARIF output                    |
+-----------------------------------------------------------+

The flow is:

  1. The agent requests to run a tool (bash, read, write, etc.)
  2. The plugin security-agent.ts intercept the call via tool.execute.before
  3. Pass the arguments to sentinel_preflight.py
  4. Python evaluates against the library of IOCs and pattern rules
  5. If there is a match: immediate blocking with explanation of the reason
  6. If there is no match: the tool runs normally

Cost: zero LLM tokens. ~30-80ms per call.

Failure mode: fail-open. If the IOCs file does not exist or the script fails, the default decision is to allow execution.

What detects

The detection engine evaluates each tool call against multiple threat categories. This list reflects the status after the v1.3 security audit, where 14 bypasses found through red teaming were patched.

Sensitive routes (CRITICAL)

Any read or write access to directories and files that contain cryptographic material, access tokens or cloud service configuration. This includes:

  • SSH key and Git credential directories
  • AWS, Azure, GCP and Kubernetes configuration
  • GPG keys, certificates (PEM, P12, PFX, KEY)
  • System hashes, service accounts files
  • Files with environment variables pattern
  • Docker and PyPI configuration

Furthermore, it detects dot-dirs enumeration — commands that attempt to recursively list all hidden directories in the user's home to discover what configurations exist.

Sensitive environment variables (HIGH)

Commands that reference environment variables that contain secrets. The plugin detects specific names of AI, cloud and payment service providers, in addition to generic patterns such as variables ending in _KEY, _SECRET, _TOKEN either _PASSWORD.

It also detects environment dumps- Commands that attempt to extract environment variables in bulk or look up shell history for keys.

Known malicious domains (CRITICAL)

Confirmed IOCs from real incidents, such as the domain used in the Postmark MCP backdoor. It also detects:

  • Pastebin type exfiltration services
  • URLs with raw IP without domain (IPv4 and IPv6)
  • Tunneling services (to bypass corporate firewalls)
  • Typosquatting- Automatically generates variants by homoglyphs, transpositions, character deletion, hyphen insertion and TLD changes of each known malicious domain. Only matches variants that appear in URL context to avoid false positives

Dangerous shell patterns (CRITICAL)

  • Download pipes to shell (remote download piped to shell)
  • Reverse shells (netcat, bash with TCP redirects)
  • Obfuscated exfiltration chains (base64 combined with network sending)
  • Use of eval/exec with external input
  • Overly open permissions (world-writable)
  • Modifying shell profile files (persistence)
  • DNS/ICMP Exfiltration: commands like nslookup, dig, ping either host combined with command substitution to send data through secondary channels
  • Alternative interpreters: code execution via scripting interpreters with imports or dangerous calls (sockets, threads, system calls)
  • Connections to raw IP: use of direct connection tools (netcat, socat, telnet) to IP addresses without a domain name

Data exfiltration (CRITICAL)

Detects command chains that combine archiving (tar, zip, 7z) with network upload, and commands to send sensitive files through HTTP upload.

Cryptomining (CRITICAL)

Detects known mining binaries, mining pool URLs, and patterns associated with embedded cryptominers.

Prompt injection (HIGH)

Manipulation phrases embedded in tool arguments, such as instructions for the agent to discard its previous context, act with elevated privileges, disable security mechanisms, or hide actions from the user.

Self-protection (CRITICAL)

The plugin protects itself: it blocks any attempt by the agent to write to the security configuration files (allowlist, IOCs, threats database). This protection operates at a double layer (TypeScript + Python) and cannot be disabled from within OpenCode.

Semgrep: pre-installation static analysis

The runtime layer (v2) protects during execution, but does not analyze the source code of the skills before installing them. To cover this gap, the project integrates Semgrep with two sets of rules:

Custom rules (4 files, ~31 rules)

Specifically designed to detect backdoors in skills and MCP servers. They do not exist in the Semgrep registry or in any other project — they are the only Semgrep rules in the ecosystem aimed at this type of threats:

FileDetect
credential-exfiltration.yamlReading of ~/.ssh/, ~/.aws/, harvesting env vars (_API_KEY, _SECRET), complete dumps of the environment
network-exfiltration.yamlRequests to pastebin/transfer.sh/webhook.site/ngrok, URLs with raw IP, hidden BCC fields (Postmark pattern), domain giftshop.club
dangerous-commands.yaml`curl\Bash, reverse shells, base64\sh, hijack de .bashrc, fork bombs, eval/exec, thread con shell=True`
supply-chain-patterns.yamlCrypto mining (xmrig, stratum+tcp, pool domains), prompt injection phrases, base64+send obfuscation, hex-encoded payloads

Community rules bundled (40 files, ~43 rules)

Curated selection from the official repository semgrep/semgrep-rules (1.1k stars on GitHub). Included offline without network dependency:

CategoryRulesDetect
ai-mcp/11Command injection in MCP, tool poisoning, SSRF, credential leaks in responses, LLM-output-to-exec, DNS exfil in hooks
python-exec/11os.system, subprocess, spawn, exec, eval, paramiko remote exec, reverse shells in Python
python-deser/4pickle, jsonpickle, pyyaml unsafe load, marshal (arbitrary code execution)
python-secrets/2Hardcoded passwords, credential logging
javascript-exec/6child_process, eval, spawn with shell, dynamic method invocation
generic-secrets/5Private keys, API keys, AWS/GitHub tokens embedded in code
generic-shells/1Reverse shells in bash

The rules of ai-mcp/ are particularly valuable — they were created by Semgrep Inc. specifically to detect attacks in the MCP ecosystem.

Use

BASH
# Escanear un skill antes de instalarlo
bash scripts/scan_semgrep.sh /path/to/skill/

# Output JSON para procesamiento automatico
bash scripts/scan_semgrep.sh /path/to/skill/ --json

# Output SARIF para GitHub Security tab
bash scripts/scan_semgrep.sh /path/to/skill/ --sarif

# Solo reglas custom (sin comunitarias)
bash scripts/scan_semgrep.sh /path/to/skill/ --no-community

# Validar que las reglas son correctas
bash scripts/scan_semgrep.sh --self-test

Semgrep is optional — if not installed, the agent uses only the LLM analysis. But when available, it runs like Step 3a of the static analysis workflow, before the manual analysis of the LLM. Semgrep findings are high confidence and do not require model interpretation.

Semgrep Benchmark

32 regression tests (tests/test_semgrep_rules.py) verify that the rules work correctly:

Malicious samples (6 files reproducing real attacks):

SampleBased onFindings
postmark_bcc_backdoor.pyPostmark MCP Incident (Sept 2025)giftshop.club, hidden BCC
credential_harvester.jsToxicSkills study (Snyk 2025)SSH/AWS read, env dump, exfil webhook.site
reverse_shell_persistence.pyCommon payloads in malicious skillsReverse shell, bashrc hijack, crypto mining, env harvest
prompt_injection_tool.jsTool poisoning on MCP serversPrompt injection, eval(), transfer.sh
deserialization_dns_exfil.pyDeserialization CVEspickle, yaml.load, marshal, DNS exfil
child_process_abuse.jsBackdoors in Node.js skillschild_process, eval, spawn shell

Benign samples (4 files): normal file operations, legitimate HTTP client, MCP calculator server, standard TypeScript skill. Zero false positives in all.

Reusable GitHub Action

The project includes a GitHub Action (action.yml) that any repository can use to scan their skills, MCP servers or AI agent plugins:

YAML
# .github/workflows/security-scan.yml
name: Security Scan
on: [push, pull_request]

permissions:
  contents: read
  security-events: write

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: red-orbita/opencode-security-agent@v1
        with:
          target: "."
          upload-sarif: "true"
          fail-on-findings: "true"

The action installs Semgrep, runs all the rules (custom + community), and optionally uploads the results in SARIF format to the tab Security from the repository on GitHub. The findings appear alongside those from CodeQL and other SAST tools.

InputDefaultDescription
target.Route to scan
include-community-rulestrueInclude rules from semgrep/semgrep-rules
output-formattexttext, json either sarif
upload-sariffalseUpload SARIF to GitHub Security tab
fail-on-findingstrueFail the workflow if there are findings

This turns the Security Agent from a personal tool to ecosystem infrastructure — any skills author can integrate the scan into their CI/CD.

Prerequisites

  • OpenCode installed and functional
  • python 3 available in the system
  • Git to clone the repository
  • Semgrep (optional) — for pre-installation static analysis. Install with pip install semgrep either brew install semgrep. Without Semgrep, the agent uses only the LLM analysis

Step 1: Clone the repository

BASH
git clone https://github.com/red-orbita/opencode-security-agent.git
cd opencode-security-agent

Step 2: install the plugin

The repository includes an installation script that configures both the runtime plugin and the static analysis skill.

Global installation (recommended — protect all your projects):

BASH
bash scripts/install.sh --user

Installation by project (only protects the current project):

BASH
bash scripts/install.sh --project

What does the installer do?

The script install.sh perform these actions:

  1. Copy security-agent.ts to the OpenCode plugins directory:
  • Global: ~/.config/opencode/plugins/security-agent.ts
  • Project: .opencode/plugins/security-agent.ts
  1. Copy sentinel_preflight.py next to the plugin so it can be invoked
  1. Copy iocs.json (the compromise indicator library) to the correct location
  1. Copy the skill SKILL.md to the skills directory:
  • Global: ~/.config/opencode/skills/security-agent/SKILL.md
  • Project: .opencode/skills/security-agent/SKILL.md

Step 3: verify the installation

Restart OpenCode and try asking the agent to read an SSH key file or run a command that references an API key. If the plugin is active, you will see a blocking message with the reason and an allowlist suggestion:

CODE
OpenCode Security Agent blocked a bash call.
Reason: [CRITICAL] sensitive path: ~/.ssh/
If this is a false positive, ask the human to manually add
an exception to .security/sentinel-allowlist.json
(this file cannot be edited by the agent).
Suggested exception (for the human to add manually):
  Add to "paths": ["~/.ssh/id_rsa"]
  Then re-run the operation.

How the plugin works (technical detail)

security-agent.ts

The OpenCode plugin is a TypeScript module that exports a plugin function. Use the hook tool.execute.before that OpenCode executes before each tool call:

TYPESCRIPT
export const SecurityAgent = async ({ project, client, $ }) => {
  return {
    "tool.execute.before": async (input, output) => {
      // Serializa los argumentos de la herramienta
      // Invoca sentinel_preflight.py con el JSON
      // Si el veredicto es "deny", lanza un Error
      // Si es "allow", no hace nada (la herramienta se ejecuta)
    },
  }
}

Key design points:

  • Does not modify arguments- only inspect and block or allow
  • stateless- each call is evaluated independently
  • No network: all evaluation is local
  • Without LLM: does not consume tokens

sentinel_preflight.py

The Python script is the detection engine. Receives a JSON with the tool and its arguments, and evaluates against:

  1. Sensitive route patterns — regular expressions vs paths
  2. Environment variables — detection of references to secrets in commands
  3. Domain IOCs — comparison against the library of malicious domains
  4. Shell patterns — detection of exfiltration chains and reverse shells
  5. Injection phrases — prompt injection detection in arguments

The verdict is returned as JSON to security-agent.ts:

JSON
{
  "decision": "deny",
  "reason": "[CRITICAL] sensitive path detected",
  "tool": "bash",
  "timestamp": "2026-04-22T10:30:00Z"
}

iocs.json

The compromise indicator library contains confirmed malicious domains from real incidents (never overridable), sensitive path patterns, environment variable patterns and regular expressions to detect dangerous commands.

Confirmed malicious domains they are never overrideable — not even with allowlist. This is by design.

Integration with OpenSpec

If your project uses OpenSpec (like Red Orbita), the Security Agent integrates naturally into the workflow.

The skill as an audit layer

The Security Agent includes a skill (SKILL.md) that OpenCode can load on demand. When loaded, the agent can:

  1. Scan skills and installed MCPs against vulnerability databases
  2. Analyze coherence — verify that the actions of each skill correspond to their stated purpose
  3. Generate reports security in .security/reports/
  4. Maintain a local database of threats in .security/mcp-sentinel-threats.json

OpenSpec + Security Agent combined flow

CODE
+----------------+    +----------------+    +--------------------+
|  Editar post   |--->|  Pre-commit    |--->|  Security Agent    |
|  o config      |    |  OpenSpec      |    |  (runtime plugin)  |
+----------------+    |  validates     |    |  blocks dangerous  |
                      |  frontmatter   |    |  tool calls        |
                      +-------+--------+    +---------+----------+
                              |                       |
                      +-------v--------+    +---------v----------+
                      |  Pre-deploy    |    |  Static scan       |
                      |  validate:     |    |  (skill invoked    |
                      |  deploy        |    |   on demand)       |
                      +----------------+    +--------------------+

OpenSpec validates the structure and content (schemas, catalogs, headers). The Security Agent protects the runtime (what commands are executed, what files are accessed, what connections are made).

They are complementary layers:

Layerthat protectsWhen
OpenSpec schemasPost frontmatter, structurePre-commit, pre-deploy
OpenSpec policiesHTTP Headers, CSP, SEOPre-deploy
Security Agent (runtime)AI agent tool callsEvery interaction
Security Agent (scan)Skills/MCPs installedon demand

Configuration in opencode.json

For projects that already use OpenSpec, you don't need to change anything in opencode.json. The plugin is automatically loaded from the plugins directory and the skill is discovered from the skills directory.

If you need to configure specific permissions for the skill:

JSON
{
  "$schema": "https://opencode.ai/config.json",
  "permission": {
    "skill": {
      "security-agent": "allow"
    }
  }
}

False positive management

The plugin can block legitimate actions. For example, during the writing of this same post, the Security Agent blocked the writing of the file because the content of the article mentioned sensitive routes and exfiltration patterns as educational examples. This is exactly the type of situation where you need the allowlist system — and where the agent must ask the human for authorization.

Fundamental rule: the agent cannot edit the allowlist

The allowlist is protected by the plugin's self-protection. No agent tool (write, edit, bash) can modify the file sentinel-allowlist.json. When the agent encounters a crash that it considers a false positive, it must:

  1. Inform the human of blockade and reason
  2. Suggest specific exception that the human must add
  3. Wait for the human to edit the file manually outside of OpenCode
  4. Retry the operation

This design guarantees that the human is always in the loop of security decisions.

How the allowlist works

There are two levels of allowlist, per project and global:

  • By project: .security/sentinel-allowlist.json at the root of the repository
  • Global: file sentinel-allowlist.json in the OpenCode configuration directory (~/.config/opencode/)

allowlist structure

The JSON file accepts three fields:

JSON
{
  "paths": [],
  "domains": [],
  "commands": []
}
FieldGuyDescription
pathsstring arrayAbsolute file paths that the agent can read/write without restriction
domainsstring arrayDomains that the agent can contact (e.g. internal APIs of your company)
commandsstring arraySpecific commands allowed that would otherwise be blocked

Example: allow writing of a content file

If the plugin blocks writing a post because its content mentions sensitive paths as an educational example, add the file path to the allowlist:

JSON
{
  "paths": [
    "/ruta/absoluta/al/proyecto/content/mi-post.md"
  ],
  "domains": [],
  "commands": []
}

Example: Allow access to an internal API

If your workflow needs to connect to an internal service that the plugin blocks:

JSON
{
  "paths": [],
  "domains": [
    "api.miempresa.internal",
    "registry.miempresa.internal"
  ],
  "commands": []
}

Example: combined allowlist

A typical allowlist for a development project:

JSON
{
  "paths": [
    "/home/dev/proyecto/content/post-sobre-seguridad.md",
    "/home/dev/proyecto/docs/guia-de-seguridad.md"
  ],
  "domains": [
    "api.miempresa.internal"
  ],
  "commands": [
    "make deploy-staging"
  ]
}

Critical rule on malicious domains

Domains that are on the list of confirmed IOCs from real incidents can never be overridden via allowlist. This is a deliberate design decision: if a domain has been verified as malicious in a real incident, there is no exception possible. This prevents a compromised skill from manipulating the agent into adding the domain to the allowlist.

Good practices for allowlists

  1. Be specific- Use full absolute paths, not wide patterns
  2. Document the reason: add a comment to the commit explaining why the exception was added
  3. Check periodically: delete entries that are no longer needed
  4. Don't commit to the repo (optional): you can add .security/sentinel-allowlist.json to .gitignore If you prefer that each developer manage their own allowlist
  5. Prefer project-level- Use the per-project allowlist before the global one to limit the scope
  6. Always edit outside of OpenCode: use your text editor, vim, nano or any external tool. The agent cannot and should not modify this file

Uninstallation

If you need to uninstall the plugin:

BASH
# Global
bash scripts/uninstall.sh --user

# Por proyecto
bash scripts/uninstall.sh --project

The uninstaller removes the plugin, Python script, IOCs and skill from the corresponding directories.

When to use each layer

NeedSolution
Protect your machine from malicious skills in real timeRuntime plugin (v2)
Scan skills code before installing them (deterministic)Semgrep SAST (v3) with custom + community rules
Audit skills before installing them (with LLM)Static analysis skill (v1)
Investigate a suspicious skillSkill research mode
Integrate scan in CI/CD of your skillsReusable GitHub Action
Validate that your HTTP headers meet the specOpenSpec validate:deploy
Detect secrets in the repositoryGitleaks (DevSecOps layer)
All of the above at onceThe three layers combined

Vulnerability discovered and resolved: the agent could bypass its own filters

During the writing of this same article we discovered a real vulnerability in the initial design of the plugin. The Security Agent blocked writing the post because the content mentioned sensitive paths, environment variables, and shell patterns as educational examples. In order to write the file, the IA agent (OpenCode) created a file .security/sentinel-allowlist.json with the path of the post as an exception and retry the writing — which this time was allowed.

The problem was obvious: The agent was able to create the allowlist and bypass the protection without any human intervention.

Attack scenario

A malicious skill could reproduce this sequence exactly:

  1. Attempt a blocked action (e.g. read SSH keys)
  2. Detect Security Agent crash
  3. Write .security/sentinel-allowlist.json with the necessary exception
  4. Retry the action — now passes without blocking
  5. Delete the allowlist to leave no trace

This completely defeated the runtime protection layer. The allowlist, intended as a false positive management mechanism, became a bypass vector.

Implemented solution: double layer of self-protection

The vulnerability was corrected by implementing self-protection in both layers from the plugin:

Layer 1 — TypeScript (security-agent.ts): Before passing the call to the Python engine, the plugin inspects whether the tool (write, edit, bash) tries to write to protected files. If it detects a match, it blocks immediately without invoking Python.

Layer 2 — Python (sentinel_preflight.py): As a second line of defense, the Python engine also checks writes to protected files before running any other checks.

The protected files are:

  • sentinel-allowlist.json — the exception allowlist
  • Any .json within .security/ — the threat database
  • iocs.json — the commitment indicators library

New flow when there is a block

When the agent needs an exception in the allowlist, it can no longer resolve it alone. The blocking message guides the human with the specific exception that must be added:

CODE
OpenCode Security Agent blocked a bash call.
Reason: [CRITICAL] sensitive path: ~/.ssh/
If this is a false positive, ask the human to manually add
an exception to .security/sentinel-allowlist.json
(this file cannot be edited by the agent).
Suggested exception (for the human to add manually):
  Add to "paths": ["~/.ssh/id_rsa"]
  Then re-run the operation.

The flow remains:

  1. The agent attempts an action → BLOCKED
  2. The agent tells the human: "I need you to add X to the allowlist"
  3. The human evaluates if it is legitimate and edits the file outside of OpenCode
  4. The agent retries → ALLOWED

The human is always in the security decision loop.

Verification in production

During the update of this same post we verified that the protection works. The agent couldn't write the allowlist when I needed it — had to ask the human to do it. Ironically, the best proof that the fix works is that this article needed human intervention to be written.

Current status

The vulnerability was reported as issue #1 and corrected in the same session. The tests cover 87 scenarios: 55 runtime hooks tests (including 7 self-protection) and 32 Semgrep rule regression tests.

Conclusion

Security in the AI ​​agent ecosystem is not optional. The Postmark incident demonstrated that even trusted skills can become attack vectors. OpenCode Security Agent adds a layer of protection that:

  • Does not require blind trust in the installed skills
  • Works without token costs LLM
  • Block before running, not after detecting
  • Detect backdoors in source code with Semgrep before installing
  • He protects himself — the agent cannot disable his own protections
  • Keeps the human in the loop — exceptions require explicit human intervention
  • Integrates into CI/CD — Reusable GitHub Action with SARIF output
  • Natively integrated with the OpenCode plugin system
  • Complements OpenSpec for complete protection of the development flow

The development of this plugin, including the discovery and resolution of the auto-bypass vulnerability, was carried out entirely in pair-programming sessions with OpenCode. The agent itself discovered the bug, reported it as an issue, implemented the fix with a double layer of protection, wrote 87 tests (55 runtime + 32 Semgrep), integrated static analysis with custom and community rules, created a reusable GitHub Action and updated this post — asking the human for permission every time I need an exception in the allowlist.

The code is open-source and is available at GitHub. Contributions, bug reports and new IOCs are welcome.

Comments