OpenCode Reference
OpenCode uses an in-process TypeScript plugin rather than external hooks. The plugin intercepts tool execution and spawns cupcake eval for policy evaluation.
Architecture
OpenCode Process
└── Cupcake Plugin (.opencode/plugin/cupcake.js)
└── spawns: cupcake eval --harness opencode
└── Returns: { decision: "allow" | "deny" }
└── Plugin throws Error to block
Unlike Claude Code and Cursor which use stdin/stdout hooks, OpenCode's plugin:
- Runs inside the OpenCode process
- Spawns
cupcake evalas a subprocess for each tool call - Throws an Error to block tool execution
Supported Events
OpenCode has a simpler event model focused on tool execution:
| Event | Description |
|---|---|
PreToolUse |
Before tool execution (tool.execute.before) |
PostToolUse |
After tool execution (tool.execute.after) |
Note: OpenCode does not support prompt events, session events, or compaction events.
Event Fields
Common Fields
All OpenCode events include:
{
"hook_event_name": "PreToolUse",
"session_id": "session-123",
"cwd": "/path/to/project",
"agent": "main",
"message_id": "msg-456"
}
PreToolUse
{
"hook_event_name": "PreToolUse",
"session_id": "session-123",
"cwd": "/path/to/project",
"agent": "main",
"message_id": "msg-456",
"tool": "bash",
"args": {
"command": "npm install express"
}
}
PostToolUse
{
"hook_event_name": "PostToolUse",
"session_id": "session-123",
"cwd": "/path/to/project",
"agent": "main",
"message_id": "msg-456",
"tool": "bash",
"args": {
"command": "npm install express"
},
"result": {
"success": true,
"output": "added 57 packages",
"exit_code": 0
}
}
Tool Name Mapping
OpenCode uses lowercase tool names. Cupcake normalizes them automatically:
| OpenCode | Cupcake Policy |
|---|---|
bash |
Bash |
edit |
Edit |
write |
Write |
read |
Read |
grep |
Grep |
glob |
Glob |
Response Format
OpenCode uses a simple, unified response format:
Allow:
{
"decision": "allow"
}
Deny:
{
"decision": "deny",
"reason": "Policy blocked: dangerous command"
}
Block (hard block):
{
"decision": "block",
"reason": "Critical security violation"
}
Ask (converted to deny):
{
"decision": "deny",
"reason": "This operation requires approval: <original ask reason>"
}
Note: OpenCode does not have native "ask" support. Ask decisions are converted to deny with the approval message included in the reason.
Plugin Configuration
Create .cupcake/opencode.json to customize plugin behavior:
{
"enabled": true,
"cupcakePath": "cupcake",
"harness": "opencode",
"logLevel": "info",
"timeoutMs": 5000,
"failMode": "closed",
"cacheDecisions": false
}
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable the plugin |
cupcakePath |
"cupcake" |
Path to cupcake binary |
logLevel |
"info" |
Log level: debug, info, warn, error |
timeoutMs |
5000 |
Max policy evaluation time (ms) |
failMode |
"closed" |
"open" (allow on error) or "closed" (deny on error) |
cacheDecisions |
false |
Cache decisions (experimental) |
Fail Mode
closed(default, recommended): If policy evaluation fails or times out, the action is denied. This is the secure option.open: If policy evaluation fails, the action is allowed. Use only in development.
Plugin Installation
The plugin is automatically installed by cupcake init --harness opencode:
Project-level:
.opencode/plugin/cupcake.js
Global:
~/.config/opencode/plugin/cupcake.js
Manual Installation
# Download from releases
mkdir -p .opencode/plugin
curl -fsSL https://github.com/eqtylab/cupcake/releases/latest/download/opencode-plugin.js \
-o .opencode/plugin/cupcake.js
# Or build from source
cd cupcake-plugins/opencode
npm install && npm run build
cp dist/cupcake.js /path/to/project/.opencode/plugin/
Writing Policies
Basic Policy Structure
# METADATA
# scope: package
# custom:
# routing:
# required_events: ["PreToolUse"]
# required_tools: ["Bash"]
package cupcake.policies.opencode.shell_policy
import rego.v1
deny contains decision if {
input.hook_event_name == "PreToolUse"
input.tool_name == "Bash"
contains(input.tool_input.command, "rm -rf")
decision := {
"rule_id": "OC-SAFETY-001",
"reason": "Destructive command blocked",
"severity": "CRITICAL"
}
}
Post-Execution Validation
# METADATA
# scope: package
# custom:
# routing:
# required_events: ["PostToolUse"]
# required_tools: ["Bash"]
package cupcake.policies.opencode.post_exec
import rego.v1
deny contains decision if {
input.hook_event_name == "PostToolUse"
input.tool_name == "Bash"
# Check if command failed
input.result.success == false
decision := {
"rule_id": "OC-EXEC-001",
"reason": concat("", ["Command failed: ", input.result.error]),
"severity": "MEDIUM"
}
}
Key Differences from Other Harnesses
| Feature | Claude Code / Cursor | OpenCode |
|---|---|---|
| Integration | External hooks (stdin/stdout) | In-process TypeScript plugin |
| Blocking mechanism | Return JSON response | Throw Error |
| Ask support | Native | Converted to deny with message |
| Context injection | additionalContext field |
Limited (future enhancement) |
| Prompt events | Yes | No |
| Session events | Yes | No |
Resources
- Setup Guide - Installation and configuration
- OpenCode Tutorial - Hands-on walkthrough
- Plugin Source Code - TypeScript plugin implementation