Back to skill

Security audit

OpenCode ACP Control

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent OpenCode automation purpose, but it includes unsafe update/install guidance and a runnable demo that automatically approves tool permissions.

Install only if you are comfortable with an agent spawning OpenCode with filesystem-write and terminal authority. Do not run the demo in a sensitive workspace or with secrets available, and avoid the documented curl-to-bash installer path unless you independently verify the downloaded installer first. Treat permission prompts as security decisions and prefer manual per-request review.

Vulnerability Patterns
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T03 · Remote Payload Retrieval and Execution

Error
Location
SKILL.md:213
Finding
Unpinned Remote Installer Is Downloaded and Executed Directly## Vulnerability Details **File Location**: `SKILL.md`, lines 213-216 **Vulnerability Type**: Remote payload retrieval and execution **Risk Level**: Critical ### Vulnerable Code ```text install (review the installer script before piping `curl | bash`): ``` curl -fsSL https://opencode.ai/install | bash ``` ``` ### Technical Analysis The installation fallback pipes mutable content retrieved from an external URL directly into a shell. The downloaded script is not pinned to an immutable version, authenticated with a cryptographic signature, or checked against an expected digest before execution. Although the surrounding instruction recommends reviewing the installer, the command itself provides no review step: the response body is immediately interpreted by `bash`. Consequently, the effective code executed by the Skill can change after the Skill package has been reviewed. Executing a remote installer is not required for the Skill's core ACP session-control functionality. Including it as an update fallback therefore expands the privilege and supply-chain exposure beyond the minimum necessary behavior. `CHANGELOG.md:56` records that a safety note was added, but this documentation-only warning does not technically mitigate the unsafe execution path. ### Attack Path 1. The agent checks the installed OpenCode version and determines that an update is available. 2. The documented restart-based automatic update does not produce the expected version. 3. The agent follows the fallback command in `SKILL.md`. 4. `curl` retrieves the current response from `https://opencode.ai/install`. 5. A compromised website, hosting environment, DNS or delivery path, or maliciously modified upstream installer supplies attacker-controlled shell code. 6. The response is passed directly to `bash` without review or integrity verification. 7. The attacker-controlled code executes with the operating-system privileges and environment inherited ...[truncated 851 chars]
Remediation
## Remediation Suggestions 1. Remove the direct `curl | bash` pipeline from the Skill. 2. Prefer installation through a trusted package manager that supports pinned versions and package-signature verification. 3. If a standalone installer must be supported: - Pin an immutable release artifact rather than a mutable installer endpoint. - Download the artifact to a dedicated file without executing it. - Verify a vendor-published cryptographic signature or pinned SHA-256 digest. - Display the verified script or provide its path for review. - Require explicit user approval before execution. - Execute it with the least-privileged account and in a constrained environment. 4. Do not allow the agent to infer approval merely because an update is available or a restart-based update failed. 5. Document the expected release version, artifact URL, checksum source, and verification procedure. 6. Treat verification failure, redirects to unexpected hosts, and transport errors as hard failures rather than reasons to execute an unverified response.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
examples/acp_demo.py:143
Finding
ACP Demo Automatically Approves Arbitrary Tool Permission Requests## Vulnerability Details **File Location**: `examples/acp_demo.py`, lines 143-159 **Vulnerability Type**: Permission-boundary bypass through unconditional approval **Risk Level**: High ### Vulnerable Code ```python # Handle Server-to-Client Requests (e.g., requestPermission) if "method" in msg and "id" in msg: if msg["method"] == "requestPermission": params = msg.get("params", {}) tool_call = params.get("toolCall", {}) title = tool_call.get("title", "unknown") print(f"[demo] server request: requestPermission for '{title}' (id={msg['id']})") # Auto-approve permission request to prevent deadlocks reply = { "jsonrpc": "2.0", "id": msg["id"], "result": {"reply": "once"} } proc.stdin.write(frame(reply)) proc.stdin.flush() print(f"[demo] auto-approved permission request id={msg['id']}") continue ``` ### Technical Analysis The demo responds to every `requestPermission` server request with `{"reply":"once"}`. It extracts only the tool title for logging and does not validate the requested command, operation kind, arguments, target path, network destination, or session identity. It also does not obtain user confirmation. This defeats the purpose of the ACP permission boundary. A response of `once` is narrower than session-wide `always` approval, but it still authorizes the complete requested operation. An attacker or prompt-influenced model can submit successive requests and receive a fresh approval for each one. The risk is amplified because the ACP initialization declares filesystem read, filesystem write, and terminal capabilities. The spawned OpenCode process also inherits the invoking user's environment and filesystem access. The beha ...[truncated 2073 chars]
Remediation
## Remediation Suggestions 1. Remove unconditional permission approval and default unknown or unattended requests to `reject`. 2. Before requesting user consent, display the complete normalized request, including: - Tool kind and title. - Command and arguments. - Working directory. - Filesystem paths and requested operation. - Network destinations, where applicable. - Session and tool-call identifiers. 3. Require explicit, per-request user confirmation before sending `reply: once`. 4. Never provide `reply: always` by default. 5. Introduce restrictive allowlists for demo-safe operations and reject shell metacharacters, unexpected executables, paths outside the selected project, and other high-risk inputs. 6. Add a clearly named opt-in flag if unattended approval is necessary for isolated testing. The flag should emit a prominent warning and require a sandbox with no sensitive credentials. 7. Run OpenCode in a constrained environment with: - Project-scoped filesystem access. - A minimal environment without credentials. - Restricted network access. - A non-privileged operating-system account. 8. Add tests verifying that unknown requests, dangerous commands, malformed permission messages, and out-of-scope paths are rejected, and that approval requires explicit user input. 9. Update the README to disclose the permission model and safe execution requirements.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (20)

External Script Fetching

High
Category
Supply Chain
Content
- `SKILL.md` release-check URL pointed at `anomalyco/opencode` (404). Corrected to `sst/opencode`.
- `SKILL.md` `updateOpenCode()` example also pointed at `anomalyco/opencode`; corrected to `sst/opencode`.
- `SKILL.md` install URL was `opencode.dev`. Corrected to `opencode.ai`.
- Added safety note alongside the `curl | bash` install command.
- `README.md` referenced a non-existent `hermes skill install` subcommand. Replaced with the two real copy-into-skills-dir patterns.
- Version drift: `_meta.json` said `0.1.0`, `SKILL.md` frontmatter said `1.0.2`. Aligned both to `0.2.0`.
Confidence
90% confidence
Finding
Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

External Script Fetching

High
Category
Supply Chain
Content
- `terminal("opencode acp", background:true)` to restart and trigger the
     auto-download.
4. Verify with `opencode --version` again. If still old, fall back to a manual
   install (review the installer script before piping `curl | bash`):

   ```
   curl -fsSL https://opencode.ai/install | bash
Confidence
98% confidence
Finding
The skill recommends falling back to a manual install by piping a remotely fetched script directly into `bash`, which executes unverified code from the network in the local environment. Even though it says to review the installer script first, the provided one-liner normalizes a dangerous pattern that bypasses integrity verification and can lead to full host compromise if the source, transport, or endpoint is tampered with.

External Script Fetching

High
Category
Supply Chain
Content
install (review the installer script before piping `curl | bash`):

   ```
   curl -fsSL https://opencode.ai/install | bash
   ```

## Implementation Notes
Confidence
99% confidence
Finding
The explicit command `curl -fsSL https://opencode.ai/install | bash` is a textbook remote code execution pattern. In a skill meant for autonomous agents with shell access, this is especially dangerous because an agent may execute it non-interactively, granting immediate execution of attacker-controlled or modified code with the agent's privileges.

Chaining Abuse

High
Category
Tool Misuse
Content
install (review the installer script before piping `curl | bash`):

   ```
   curl -fsSL https://opencode.ai/install | bash
   ```

## Implementation Notes
Confidence
98% confidence
Finding
The shell pipeline into `bash` is a chaining pattern that turns remote content retrieval directly into execution, eliminating inspection points and compounding the risk of command injection or supply-chain compromise. Given this skill's purpose—automating shell-driven agent workflows—the pattern is more dangerous than in ordinary documentation because it may be adopted verbatim by autonomous tooling.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The README explicitly advertises starting background CLI sessions, sending coding tasks, handling permission requests, and triggering auto-updates, but it does not warn users that these actions may execute commands, modify files, or affect local system state. In the context of an agent skill that programmatically drives a coding tool, omission of these safety caveats can lead operators to grant permissions or run the demo with insufficient understanding of the consequences.

Session Persistence

Medium
Category
Rogue Agent
Content
cp SKILL.md ~/.hermes/profiles/<profile>/skills/opencode-acp-control.md

# Or load the whole directory
mkdir -p ~/.hermes/profiles/<profile>/skills/opencode-acp-control
cp SKILL.md ~/.hermes/profiles/<profile>/skills/opencode-acp-control/SKILL.md
```
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Skill Enumeration

Medium
Category
Agent Snooping
Content
# Or load the whole directory
mkdir -p ~/.hermes/profiles/<profile>/skills/opencode-acp-control
cp SKILL.md ~/.hermes/profiles/<profile>/skills/opencode-acp-control/SKILL.md
```

The agent will pick up the file on its next skills refresh.
Confidence
80% confidence
Finding
Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill clearly instructs use of shell execution (`terminal(...)`) and network access (`web_fetch`, installer download), but it does not declare any explicit tool scope or allowed-tools boundary. In an agent environment, missing capability scoping increases the chance that a caller grants broader execution and network powers than intended, making misuse or prompt-induced overreach more likely.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The update procedure tells the agent to enumerate and kill every running `opencode acp` process without an explicit confirmation step or warning about disrupting active sessions. Because this skill is specifically for programmatic process control, the instruction is contextually relevant, but it still creates a real availability risk: an agent could terminate unrelated or in-progress sessions and cause loss of work.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
title = tool_call.get("title", "unknown")
                print(f"[demo] server request: requestPermission for '{title}' (id={msg['id']})")
                
                # Auto-approve permission request to prevent deadlocks
                reply = {
                    "jsonrpc": "2.0",
                    "id": msg["id"],
Confidence
96% confidence
Finding
The explicit 'Auto-approve permission request' behavior is an unsafe autonomous decision affecting security-sensitive actions. In this ACP-control context, the agent is not merely suggesting an action; it is granting execution authority to a subprocessed agent session that may access the filesystem or terminal.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
The demo client automatically approves every `requestPermission` request from the OpenCode subprocess with `{"reply": "once"}`, removing any human review or policy enforcement. In this skill context, the ACP client advertises filesystem write and terminal capabilities, so auto-approval can let the controlled session perform sensitive local actions immediately if prompted or compromised.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The code accepts all permission requests from the subprocess without confirmation, effectively disabling the permission boundary the protocol expects. Because this skill is specifically designed to drive an agentic CLI session over JSON-RPC, that behavior is more dangerous than in a passive demo: it can authorize file modifications or terminal execution originating from model output or untrusted prompts.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
}
                proc.stdin.write(frame(reply))
                proc.stdin.flush()
                print(f"[demo] auto-approved permission request id={msg['id']}")
            continue

        if msg.get("method") == "session/update":
Confidence
95% confidence
Finding
The log message confirms that the system has already auto-approved a permission request, evidencing autonomous authorization without oversight. That weakens a core safety control and can enable harmful actions if the session is influenced by malicious prompts, compromised tools, or unexpected server behavior.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return 1

    print(f"[demo] spawning: {opencode_bin} acp (cwd={cwd})")
    proc = subprocess.Popen(
        [opencode_bin, "acp"],
        cwd=str(cwd),
        stdin=subprocess.PIPE,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
def test_read_frame_parses_single_line_bytes():
    stream = io.BytesIO(b'{"jsonrpc":"2.0","id":0,"result":{"ok":true}}\n')
    frame = acp_demo.read_frame(stream, timeout=0.0)
    assert frame == {"jsonrpc": "2.0", "id": 0, "result": {"ok": True}}
Confidence
75% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
def test_read_frame_parses_single_line_bytes():
    stream = io.BytesIO(b'{"jsonrpc":"2.0","id":0,"result":{"ok":true}}\n')
    frame = acp_demo.read_frame(stream, timeout=0.0)
    assert frame == {"jsonrpc": "2.0", "id": 0, "result": {"ok": True}}
Confidence
75% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
def test_read_frame_parses_single_line_bytes():
    stream = io.BytesIO(b'{"jsonrpc":"2.0","id":0,"result":{"ok":true}}\n')
    frame = acp_demo.read_frame(stream, timeout=0.0)
    assert frame == {"jsonrpc": "2.0", "id": 0, "result": {"ok": True}}
Confidence
75% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
def test_read_frame_parses_single_line_bytes():
    stream = io.BytesIO(b'{"jsonrpc":"2.0","id":0,"result":{"ok":true}}\n')
    frame = acp_demo.read_frame(stream, timeout=0.0)
    assert frame == {"jsonrpc": "2.0", "id": 0, "result": {"ok": True}}
Confidence
75% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _run_cli(*args: str) -> subprocess.CompletedProcess:
    return subprocess.run(
        [sys.executable, str(DEMO_PATH), *args],
        capture_output=True,
        check=False,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Intent-Code Divergence

Low
Confidence
88% confidence
Finding
The module docstring says the script 'Gracefully shuts the subprocess down,' but the actual cleanup logic sends OS-level terminate/kill signals in the finally block. That is a direct mismatch between the documented intent and the implemented behavior.

Static analysis

No suspicious patterns detected.