Writing Policies
This guide covers how to write effective policies for your rulebook.
Namespace Convention
All catalog files must use these namespace patterns:
| Directory | Pattern | Example |
|---|---|---|
policies/<harness>/ |
cupcake.catalog.<name>.policies.<policy> |
cupcake.catalog.security_hardened.policies.dangerous_commands |
helpers/ |
cupcake.catalog.<name>.helpers.<helper> |
cupcake.catalog.security_hardened.helpers.commands |
system/ |
cupcake.catalog.<name>.system |
cupcake.catalog.security_hardened.system |
Namespace Validation
cupcake catalog lint will fail if your policies don't follow these patterns.
Policy Structure
Basic Policy
# policies/claude/example.rego
package cupcake.catalog.my_rulebook.policies.example
import rego.v1
# METADATA
# scope: package
# title: Example Policy
# description: Demonstrates policy structure
# custom:
# severity: medium
# routing:
# required_events: ["PreToolUse"]
# required_tools: ["Bash"]
deny contains decision if {
input.hook_event_name == "PreToolUse"
input.tool_name == "Bash"
input.tool_input.command == "bad_command"
decision := {
"rule_id": "EXAMPLE-001",
"reason": "This command is not allowed",
"severity": "MEDIUM",
}
}
Key Components
- Package declaration - Must follow namespace convention
import rego.v1- Use modern Rego syntax- METADATA block - Routing and documentation
- Decision rule - Returns decision objects with rule_id, reason, severity
Routing Metadata
The routing section in METADATA controls when your policy is evaluated:
# custom:
# routing:
# events: [pre_tool_use, post_tool_use]
# tools: [bash, write]
Events
| Event | Description |
|---|---|
pre_tool_use |
Before a tool executes |
post_tool_use |
After a tool executes |
session_start |
When agent session begins |
session_end |
When agent session ends |
notification |
Agent notifications |
* |
All events |
Tools
Specify which tools trigger evaluation:
bash- Shell commandswrite- File writesread- File readsedit- File edits*- All tools
Decision Types
| Decision | Effect |
|---|---|
allow |
Explicitly permit the action |
deny |
Block the action |
ask |
Prompt user for confirmation |
halt |
Stop the agent entirely |
Severity Levels
| Severity | Use Case |
|---|---|
critical |
Immediate security threat |
high |
Significant risk |
medium |
Moderate concern |
low |
Minor issue |
info |
Informational only |
Aggregation Entrypoint
Each rulebook needs a single system/evaluate.rego at the root level (not per-harness):
# system/evaluate.rego
package cupcake.catalog.my_rulebook.system
import rego.v1
# METADATA
# scope: package
# custom:
# entrypoint: true
evaluate := {
"halts": collect_verbs("halt"),
"denials": collect_verbs("deny"),
"blocks": collect_verbs("block"),
"asks": collect_verbs("ask"),
"allow_overrides": collect_verbs("allow_override"),
"add_context": collect_verbs("add_context"),
}
collect_verbs(verb_name) := result if {
verb_sets := [value |
walk(data.cupcake.catalog.my_rulebook.policies, [path, value])
path[count(path) - 1] == verb_name
]
all_decisions := [decision |
some verb_set in verb_sets
some decision in verb_set
]
result := all_decisions
}
default collect_verbs(_) := []
This entrypoint uses walk() to automatically discover all decision verbs across all policies, so you don't need to manually import each policy.
Helper Functions
Place shared helpers in helpers/ at the rulebook root:
# helpers/commands.rego
package cupcake.catalog.my_rulebook.helpers.commands
import rego.v1
# Check if command contains a verb with word boundaries
has_verb(command, verb) if {
pattern := concat("", ["(^|\\s)", verb, "(\\s|$)"])
regex.match(pattern, command)
}
# Check if command has any of the specified flags
has_any_flag(command, flag_set) if {
some flag in flag_set
pattern := concat("", ["(^|\\s)", flag, "(\\s|$|=)"])
regex.match(pattern, command)
}
Import and use helpers in your policies:
# policies/claude/dangerous_flags.rego
package cupcake.catalog.my_rulebook.policies.dangerous_flags
import data.cupcake.catalog.my_rulebook.helpers.commands
import rego.v1
deny contains decision if {
input.hook_event_name == "PreToolUse"
input.tool_name == "Bash"
cmd := lower(input.tool_input.command)
commands.has_verb(cmd, "git")
commands.has_any_flag(cmd, {"--no-verify", "-n"})
decision := {
"rule_id": "GIT-001",
"reason": "Blocked --no-verify flag on git command",
"severity": "HIGH",
}
}
Testing Policies
Test your policies locally:
# Create a test event
cat > test-event.json << 'EOF'
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf /" },
"session_id": "test",
"cwd": "/tmp",
"transcript_path": "/tmp/transcript.md"
}
EOF
# Evaluate against the test input
cupcake eval --harness claude < test-event.json
Best Practices
- Be specific - Target exactly what you want to block
- Provide context - Use clear reason messages
- Test edge cases - Try variations of blocked inputs
- Document decisions - Explain why something is blocked
- Use appropriate severity - Don't cry wolf with everything as critical