Custom Policies
Create your own policies in .cupcake/policies/<harness>/ using OPA Rego for complete control over agent behavior.
Basic Structure
A custom policy file has three parts:
- Metadata — Declares when the policy should run
- Package — Unique namespace for the policy
- Rules — The actual policy logic
# METADATA
# scope: package
# custom:
# routing:
# required_events: ["PreToolUse"]
# required_tools: ["Bash"]
package cupcake.policies.my_policy
import rego.v1
deny contains decision if {
input.tool_name == "Bash"
contains(input.tool_input.command, "rm -rf")
decision := {
"rule_id": "SAFETY-001",
"reason": "Dangerous command blocked",
"severity": "HIGH"
}
}
Routing Metadata
The metadata tells Cupcake when to evaluate your policy:
# METADATA
# scope: package
# custom:
# routing:
# required_events: ["PreToolUse", "PostToolUse"]
# required_tools: ["Bash", "Write", "Edit"]
- required_events — Which hook events trigger this policy
- required_tools — Which tools this policy applies to (optional)
Available Events
| Event | Description |
|---|---|
PreToolUse |
Before a tool executes |
PostToolUse |
After a tool executes |
UserPromptSubmit |
Before sending prompt to LLM |
SessionStart |
When session starts or resumes |
SessionEnd |
When session ends |
Stop |
When agent stops |
SubagentStop |
When subagent (Task tool) completes |
PreCompact |
Before memory compaction |
Notification |
On agent notifications |
Decision Verbs
Policies emit decisions using these verbs (in priority order):
| Verb | Priority | Effect |
|---|---|---|
halt |
Highest | Block and stop the session immediately |
deny |
High | Block the action (policy violation) |
block |
High | Block the action (same priority as deny) |
ask |
Medium | Prompt user for confirmation |
allow_override |
Low | Explicitly allow (overrides default) |
add_context |
N/A | Inject context into the prompt |
Deny Example
deny contains decision if {
input.tool_name == "Bash"
contains(input.tool_input.command, "--no-verify")
decision := {
"rule_id": "GIT-001",
"reason": "Cannot bypass pre-commit hooks",
"severity": "HIGH"
}
}
Ask Example
ask contains decision if {
input.tool_name == "Bash"
contains(input.tool_input.command, "git push")
decision := {
"rule_id": "GIT-002",
"reason": "Pushing to remote repository",
"question": "Do you want to allow this push?",
"severity": "MEDIUM"
}
}
Context Injection Example
add_context contains context if {
input.hook_event_name == "UserPromptSubmit"
context := "Remember: Always run tests before committing."
}
Accessing Input Data
The input object contains event data:
# Tool name
input.tool_name # "Bash", "Write", "Edit", etc.
# Tool-specific input
input.tool_input.command # For Bash
input.tool_input.file_path # For Write/Edit/Read
input.tool_input.content # For Write
# Event metadata
input.hook_event_name # "PreToolUse", "PostToolUse", etc.
input.session_id
input.cwd
# For UserPromptSubmit
input.prompt
File Organization
Place policies in the harness-specific directory:
.cupcake/
├── policies/
│ ├── claude/ # Claude Code policies
│ │ ├── my_policy.rego
│ │ └── another.rego
│ ├── cursor/ # Cursor policies
│ │ └── cursor_rules.rego
│ ├── factory/ # Factory AI policies
│ │ └── factory_rules.rego
│ └── opencode/ # OpenCode policies
│ └── opencode_rules.rego
└── rulebook.yml
Testing Policies
Test your policy with a JSON event:
# Create test event
echo '{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {"command": "rm -rf /"},
"session_id": "test",
"cwd": "/tmp",
"transcript_path": "/tmp/transcript.md"
}' > test.json
# Evaluate
cupcake eval --harness claude < test.json