Architecture
Technical reference for the Vectimus evaluation pipeline. This document covers component responsibilities, data flow and integration schemas.
TODO: Add architecture diagrams (evaluation pipeline, hook integration flow, server mode topology).
Evaluation flow
AI Agent (tool call) -> vectimus hook (stdin JSON) -> Normaliser -> Cedar PolicyEngine -> Decision
|
Audit Log (JSONL) + Signed Receipt
The default path is entirely local. The vectimus hook --source <tool> command reads JSON from stdin, evaluates via cedarpy and returns a decision via stdout JSON. No server, no network required.
Daemon mode
On the first hook call the daemon auto-starts as a background process, keeping the Cedar policy engine warm in memory. Subsequent evaluations skip the ~200ms Python startup cost. The daemon shuts down after 30 minutes of inactivity.
vectimus daemon status # check if running
vectimus daemon start # manual start (auto-starts on first hook call)
vectimus daemon stop # manual stop
vectimus daemon reload # flush cached engines (picks up config changes)
Platform differences. On Unix/macOS the daemon listens on a Unix domain socket at /tmp/vectimus-<uid>.sock with filesystem permissions handling authentication. On Windows it uses TCP localhost with an OS-assigned port and a random auth token stored in ~/.vectimus/daemon.json (user-only permissions). Unix daemonizes via double-fork. Windows spawns a detached child process.
The daemon caches policy engines per project path and refreshes them every 5 minutes. CLI commands that change config (rule disable, rule enable, rule enforce, pack enable, pack disable, mcp allow, mcp deny, policy update) automatically send a reload to the daemon so changes take effect immediately. If the daemon is unavailable, hooks fall back to inline evaluation transparently.
Temporary rule disables. The daemon holds an in-memory map of temporary rule disables created with vectimus rule disable <rule> --for <duration>. These are scoped per-project and auto-expire after the specified duration. No config files are written. If the daemon restarts all temp disables are lost (fail-closed). The engine cache is invalidated when temp disables are added, cleared or expire so policy changes take effect on the next evaluation.
When receipts are enabled the daemon runs receipt retention cleanup 30 seconds after the first request per project to avoid unbounded disk growth.
Cryptographic receipts
Every Cedar policy evaluation produces a signed governance receipt. Receipts provide tamper-evident proof of what was evaluated, what decision was made and which policies matched.
Each receipt contains:
- Receipt ID (included in deny messages for traceability)
- Event hash (SHA-256 of RFC 8785 canonical JSON of the input event)
- Decision, reason and matched policy IDs
- Timestamp and evaluation duration
- Ed25519 signature over the canonical receipt JSON
Receipts are stored per-project in .vectimus/receipts/YYYY-MM-DD/ as individual JSON files. Signing keys are generated automatically on first use and stored at ~/.vectimus/signing-key.pem.
vectimus verify <receipt-file> # verify a single receipt signature
vectimus receipts prune --days 30 # delete receipts older than 30 days
vectimus receipts prune --all # delete all receipts
Receipts are retained for 7 days by default. The daemon runs cleanup automatically after the first request per project. Configure retention via [receipts] retention_days in config, or manage manually with vectimus receipts prune --days 30 or vectimus receipts prune --all.
Receipts are enabled by default. Disable via [receipts] enabled = false in config or VECTIMUS_RECEIPTS_ENABLED=false.
Server mode
The optional HTTP server (vectimus server start) adds centralised policy evaluation for teams. See Running the server for setup and configuration.
Data models
All models use Pydantic v2 BaseModel. See src/vectimus/core/models.py for the full definitions.
VectimusEvent is the normalised event that Cedar policies evaluate. Key fields:
source(SourceInfo) — where the event came from (tool name, version, session)identity(IdentityInfo) — who triggered it (principal, persona, groups)action(ActionInfo) — what is being attempted (action_type, command, file_path, file_content, script_content, etc.)context(ContextInfo) — environmental context (repo, branch, cwd)
Decision is the governance result:
decision— “allow”, “deny” or “escalate”reason— human-readable explanation (mandatory for deny/escalate)suggested_alternative— what the agent should try instead (mandatory for deny)matched_policy_ids— which policies triggered
The verdict depends on the matched policy’s @enforcement annotation:
deny(default) — hard block, the agent cannot proceedescalate— blocks with a descriptive[escalate]message in local mode. In server mode the server can route to external approval workflows (PagerDuty, Slack) before returning allow/denyobserve— logs the match but returns allow, useful for testing new policies before enforcing them
Config-based overrides (via [rules.enforcement] in config.toml) take precedence over annotations. Global observe mode overrides everything.
AuditRecord pairs a VectimusEvent with its Decision for the audit log.
Action types
Normalised across all tools:
| Action type | Examples |
|---|---|
shell_command | Bash, terminal, shell execution |
file_write | Write, Edit, MultiEdit, 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, yarn |
git_operation | git push, commit, branch operations |
infrastructure | terraform, kubectl, docker, cloud CLI |
agent_spawn | Task, subagent creation |
Shell commands are further classified: commands starting with terraform/kubectl/docker map to infrastructure, npm/pip/cargo/yarn to package_operation, git to git_operation.
Shell commands that perform file I/O are reclassified to file_read or file_write with the target file path extracted for Cedar policy matching. This covers output redirects (echo x > file), tee, sed -i, cp/mv, dd of= and inline scripts (python3 -c "open('f','w').write('x')", node -e, ruby -e, perl -e). This prevents agents from bypassing file policies by wrapping file operations in shell commands.
Content inspection (double evaluation)
When an agent writes a file or executes a script, Vectimus runs a second Cedar evaluation pass treating the content as a shell command. Every existing shell_command policy automatically applies to file writes and script execution, closing the write-then-execute bypass where an agent writes a malicious script in one step and runs it in the next.
How it works:
- The normaliser extracts
file_contentfrom Write/Edit tool payloads and resolvesscript_contentby reading the target file when the command matches a script execution pattern (bash deploy.sh,python setup.py,./run.sh). - If the primary evaluation allows the action and content is present, the evaluator splits the content into lines and evaluates each non-empty, non-comment line against all
shell_commandpolicies. - If any line triggers a deny, the entire action is blocked. The deny reason includes which policy matched and whether it was caught via file content or script content inspection.
Content is limited to 5000 lines by default (configurable via VECTIMUS_CONTENT_MAX_LINES or [limits] content_inspection_max_lines in config). Line-based limits prevent the padding bypass where an attacker fills a file with harmless comments to push malicious commands past a byte boundary. Scripts are read line by line to avoid loading entire large files into memory. Unreadable files fail open (the primary evaluation result stands). No new policy syntax is needed. Your existing rules cover both direct commands and content automatically.
Normaliser input schemas
The normaliser accepts tool-specific JSON and produces VectimusEvent objects. New tools are added by registering a normaliser function.
Claude Code
{
"tool_name": "Bash",
"tool_input": { "command": "rm -rf /tmp/build" },
"tool_use_id": "uuid",
"session_id": "uuid",
"cwd": "/home/user/project",
"hook_event_name": "PreToolUse"
}
Tool name mapping:
| Tool name | Action type |
|---|---|
Bash | shell_command |
Write, Edit, MultiEdit | file_write |
Read, Grep, Glob | file_read |
WebFetch, WebSearch | web_request |
Task | agent_spawn |
mcp__* | mcp_tool |
Cursor
{
"conversation_id": "uuid",
"generation_id": "uuid",
"command": "rm -rf /tmp/build",
"cwd": "/home/user/project",
"hook_event_name": "beforeShellExecution",
"workspace_roots": ["/home/user/project"]
}
Event mapping: beforeShellExecution -> shell_command, beforeMCPExecution -> mcp_tool, beforeReadFile -> file_read, afterFileEdit -> file_write.
GitHub Copilot / VS Code
{
"timestamp": "2026-03-08T14:30:00.000Z",
"cwd": "/home/user/project",
"sessionId": "uuid",
"hookEventName": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf /tmp/build" }
}
Gemini CLI
{
"tool_name": "run_shell_command",
"tool_input": { "command": "rm -rf /tmp/build" },
"hook_event_name": "BeforeTool",
"session_id": "uuid",
"cwd": "/home/user/project"
}
Tool name mapping:
| Tool name | Action type |
|---|---|
run_shell_command | shell_command |
read_file | file_read |
write_file, edit_file | file_write |
list_directory | file_read |
mcp__* | mcp_tool |
Claude Agent SDK
The Claude Agent SDK shares the exact same hook mechanism and payload format as Claude Code. No separate normaliser is needed. Hooks defined in .claude/settings.json fire identically for both tools.
Google ADK
Google ADK uses a native Python integration rather than stdin/stdout hooks. See Google ADK integration for the plugin and callback APIs.
The VectimusADKPlugin maps ADK tool names to Vectimus action types:
| ADK tool name | Action type |
|---|---|
bash, shell, terminal | shell_command |
file_write | file_write |
file_read | file_read |
google_search, web_search | web_request |
server__tool (MCP pattern) | mcp_tool |
LangGraph / LangChain
LangGraph uses a Python middleware integration. See LangGraph integration for the middleware and MCP interceptor APIs.
Cedar schema
The Cedar schema defines entity types, actions and context shapes. See src/vectimus/core/schemas.py for the full definition.
Entity types: User, Agent, Tool.
Each action type (shell_command, file_write, etc.) applies to [User, Agent] principals and Tool resources with a context containing a parameters record.
Cedar policy conventions
Every policy rule must have:
@id("vectimus-<domain>-NNN")— unique identifier (e.g.vectimus-infra-001,vectimus-exfil-001)@description("...")— human-readable explanation@incident("...")— real-world incident reference (where applicable)@controls("...")— compliance controls it satisfies (where applicable)
See Writing policies for the full guide.
Server endpoints (opt-in)
Activated via vectimus server start. Not part of the default MVP flow.
| Method | Path | Purpose |
|---|---|---|
| POST | /evaluate | Evaluate a tool event against policies |
| GET | /policies | List loaded policies with metadata |
| GET | /health | Server status, policy count, uptime |
| GET | /events | SSE stream of real-time evaluation events |
The /evaluate endpoint accepts an X-Vectimus-Source header to identify the source tool. For Claude Code HTTP hooks, the response includes hookSpecificOutput with permissionDecision and permissionDecisionReason.
Configuration
Locations (in order of precedence):
- Environment variables (
VECTIMUS_PERSONA,VECTIMUS_CONTENT_MAX_LINES, etc.) .vectimus/config.toml(project-local, version-controllable)~/.vectimus/config.toml(user-level global)- Hardcoded defaults
[server]
url = "https://vectimus.internal.example.com" # omit for local-only
[identity]
persona = "default"
groups = ["engineering", "platform"]
identity_type = "human"
[limits]
content_inspection_max_lines = 5000
excessive_turns_threshold = 50
session_spawn_limit = 10
session_message_limit = 50
session_ttl_seconds = 3600
git_timeout_seconds = 5
[audit]
max_file_size_mb = 100
log_dir = "~/.vectimus"
[rules]
disabled = []
The .vectimus/ directory in the project root is protected by Cedar policy vectimus-fileint-005, preventing agents from modifying governance config.