Skip to content
browse the docs

CLI Reference

vanzik v2.0updated view on github

Every vanzik (central server) subcommand, every flag, every env-var fallback. Source of truth: cmd/vanzik/main.go and cmd/vanzik/cli.go.

Scope: this page documents the vanzik binary — the central server that owns the policy engine, audit log, approval queue, and dashboard. For the wire-level proxy binaries see MCP_GATEWAY.md (vanzik-mcp-gateway) and LLM_API_PROXY.md (vanzik-llm-proxy).

vanzik <command> [flags]

Set up and run:
  setup       Set up, update or remove Vanzik on this computer (a menu)
  server      Start Vanzik: policy engine, approvals, audit log, dashboard

Policies:
  validate    Check that a policy file loads
  check       Evaluate one action against a policy file (no server needed)
  test        Run a policy against test cases in YAML files (for CI)
  tenant      Manage per-tenant policies in the store (put|list|rm)

Work with a running server:
  status      Show server health and pending approvals
  approve     Approve a pending action by ID
  deny        Deny a pending action by ID
  audit       Query the audit log

Other:
  hook        Check a coding agent's tool calls (its hook; setup connects it)
  migrate     Upgrade the audit log's format (the server does this at startup)
  report      Write a compliance evidence report from the audit log
  version     Print version information (also: --version)
  help        Show help for a command

Get started:
  vanzik setup
      Set Vanzik up to run at login, with a policy and an API key
  vanzik server --dashboard
      Or start the server yourself, then open http://localhost:8080/dashboard
  vanzik check --scope shell --command "rm -rf /"
      Try the policy on one action, no server needed

Also installed:
  vanzik-mcp-gateway   Guards the tools of an MCP client (Claude Desktop,
                           Cursor, …)
  vanzik-llm-proxy     Guards tool calls in OpenAI / Anthropic SDK code

Run 'vanzik help <command>' (or 'vanzik <command> -h') for its flags.

Every command's help (vanzik help <command>) has examples and says what its exit codes mean; the full list also shows the environment variables and a link to this page for the installed version.

Global conventions:

  • vanzik serve is the same command as vanzik server and keeps working for the whole 2.x line (COMPATIBILITY.md).
  • Flags can go before or after positional arguments: vanzik approve <id> --url <url> and vanzik approve --url <url> <id> are the same. A -- ends the flags; everything after it is an argument.
  • vanzik help, -h and --help print help on stdout and exit 0; so do vanzik help <command> and vanzik <command> -h. A mistyped command or flag gets a suggestion: vanzik: unknown command "--serve". Did you mean 'vanzik server'?
  • --api-key on server and on the client subcommands (approve, deny, status, audit) falls back to the VANZIK_API_KEY env var; --url on the client subcommands falls back to VANZIK_URL; --policy on server, validate, check and test is found automatically when not given (see Policy file).
  • Exit code 0 = success, 1 = failure, 2 = the command line was wrong (unknown command or flag, missing or extra argument). check and test have their own codes — see below; audit verify and report add 3 for a file (or server) they can't read.

vanzik setup#

Set Vanzik up on this computer, keep it current and remove it, from a menu: move with ↑/↓, choose with Enter, go back with Esc. It has no flags; every action is a menu item. It needs a terminal: scripts use the one-line installers, which install, update and uninstall too.

Set up (the first run) asks two questions — start Vanzik at login? protect it with an API key? — then does this, skipping what's already there:

Step Linux / macOS Windows
Starter policy (an existing one is kept) ~/.config/vanzik/default.yaml %APPDATA%\vanzik\default.yaml
Data folder: audit log, state database, server log ~/.local/share/vanzik %LOCALAPPDATA%\Vanzik\data
API key, readable only by you ~/.config/vanzik/api-key %APPDATA%\vanzik\api-key
Audit seal key, readable only by you (new setups) ~/.local/share/vanzik/audit-seal.key %LOCALAPPDATA%\Vanzik\data\audit-seal.key
Start at login, without admin rights systemd user service vanzik.service / LaunchAgent com.lictorate.vanzik Task Scheduler task Vanzik, at your logon

The service runs vanzik server --policy … --data-dir … --audit-log … --port 8080 --bind 127.0.0.1 --dashboard --api-key-file … --audit-seal --audit-seal-key-file …, on the next free port when 8080 is taken. The keys go in as files, so they never appear in the service definition or the process list. A new setup seals the audit log (see vanzik audit verify); an existing setup keeps its command line when you run setup again. XDG_CONFIG_HOME and XDG_DATA_HOME move the Linux and macOS folders.

Later runs show whether the server is running and offer: open the dashboard (the API key goes to the clipboard for its login), update, start or restart the server, connection details (the URL, where the key is, the lines for the SDKs, and a Claude Desktop block with no secret in it), open the policy file, change settings, and — last, under a divider — uninstall.

Update installs the newest release (or VANZIK_VERSION) for this OS and CPU, from GitHub or from VANZIK_DOWNLOAD_URL, like the installers. It checks the archive against the release's checksums.txt, swaps the three binaries in place — a running server keeps going until it restarts — and restarts the service. A go install copy gets the go install command instead; a source build and the container image don't update themselves.

Connect Claude Code, Connect Cursor, Connect Gemini CLI and Connect Codex CLI (each shown when that agent is installed) add Vanzik's hook to the agent's settings so its tool calls are checked. Each offers to add the agent's rules to a policy that has none, and asks what happens while Vanzik isn't running: let calls through with a warning (the default) or block them (--fail-closed). Disconnect takes the hook out again. See CODING_AGENTS.md.

Uninstall offers, in this order: stop the server and don't start it at login; uninstall and keep the policy, key and data; uninstall everything; cancel. It removes the login service, Vanzik's hooks from the coding agents' settings, and the three binaries — on Windows also the installer's PATH entry, and the running .exe right after setup exits — works offline, and prints a link for feedback. Nothing is sent.

An AgentGuard 1.x install (this project's name before 2.0) is moved over before the menu opens, with vanzik migrate --from-agentguard; if a step fails, setup stops there. Without a terminal setup only says that one is there.

Opening the menu checks GitHub for a newer release unless VANZIK_NO_UPDATE_CHECK is set; the menu then offers "Check for updates". setup refuses to run as root (it sets Vanzik up for your own user; for a system-wide server see DEPLOYMENT.md) and inside the container image.


vanzik hook <agent>#

vanzik hook claude-code|cursor|gemini-cli|codex [--fail-closed]

The command a coding agent runs before each shell command, file read or edit, web fetch and MCP tool call, once vanzik setup has connected it. It reads the call as JSON on stdin, in the agent's own format, checks it on the running server as that agent, and answers in the agent's own format: go ahead when the policy allows it, a block with the reason when it denies it; a call that needs approval waits up to 5 minutes for one. It finds the server through VANZIK_URL, else the one setup runs, and the key through VANZIK_API_KEY, else the saved one. You don't run it yourself; see CODING_AGENTS.md.

Agent Checked as agent Audit transport
claude-code claude-code claude_code
cursor cursor cursor
gemini-cli gemini-cli gemini_cli
codex codex codex_cli
Flag Default Description
--fail-closed false Block calls Vanzik can't check (server down, API key refused, unreadable input), with rule deny:hook:fail_closed, instead of letting them through with a warning. Goes before or after the agent.

vanzik server#

Start the Vanzik server. This is the only subcommand that runs a long-lived process. vanzik serve is the same command.

vanzik server -h prints the flags below grouped by topic (network, security, policy, audit log, audit sealing, storage, multi-node, notifications, debugging).

Flag Default Description
--policy <path> found automatically Path to policy YAML. When not given, see Policy file. Rejected at startup if missing or invalid; the startup log names the file it loaded.
--port <int> 8080 TCP port. See bind behavior below.
--bind <host> (empty) Host or IP to listen on, e.g. 127.0.0.1 behind a same-host reverse proxy. Empty keeps the default bind behavior below. A non-loopback --bind without --api-key is refused at startup (exit 2).
--dashboard off Serve /dashboard HTML + /api/stream SSE. Required for human approval UI.
--watch off Log a line each time the policy file is reloaded. The reload itself always happens, with or without this flag (fsnotify events, with a 2 s mtime poll as fallback); no restart needed after policy edits.
--audit-log <path> audit.jsonl Append-only JSON Lines file. Mode 0600. Rotation is on by default; configurable via --audit-max-size-mb, --audit-max-backups, --audit-max-age-days, --audit-compress. Do NOT also configure logrotate against audit.jsonl: two rotators on one file corrupt the rotation index. See OPERATIONS.md.
--api-key <key> $VANZIK_API_KEY Bearer token for gated endpoints. If empty, the server binds to 127.0.0.1 only (localhost-only).
--api-key-file <path> (empty) Read the API key from the first line of this file instead of the command line, where other users of the machine could see it. --api-key wins over it; it wins over VANZIK_API_KEY. The server refuses to start when the file can't be read or is empty, rather than run unprotected. vanzik setup uses it.
--base-url <url> http://localhost:<port> External URL used when constructing approval_url in check responses. Set this behind a reverse proxy.
--allowed-origin <url> (empty) Exact CORS origin. Empty = permissive-localhost (accepts any http://localhost:* or http://127.0.0.1:*). Set to https://app.example for strict single-origin.
--tls-terminated-upstream off Issue session cookies with Secure even when r.TLS == nil. Set when behind a TLS-terminating proxy that does not forward X-Forwarded-Proto. See DEPLOYMENT.md.
--session-cost-ttl <dur> 0 (never expire) Evict idle session-cost accumulator entries. Example: 24h.
--session-cost-sweep-interval <dur> max(ttl/4, 1m) Sweeper cadence. Ignored when --session-cost-ttl 0.
--approval-validity <dur> 5m How long a resolved approval is honored by the /v1/check approval-id retry, measured from resolution. Past the window the retry re-enters the approval flow under a new id. 0 disables the bound. Default matches the SDKs' wait_for_approval poll window. Resolved ALLOWs are additionally one-shot regardless of this flag — see APPROVAL_WORKFLOW.md.
--audit-max-size-mb <int> 100 Rotate when the live audit file reaches this MiB. 0 disables rotation entirely (unbounded growth). See OPERATIONS.md.
--audit-max-backups <int> 5 Maximum number of rotated archives to retain. 0 keeps all archives indefinitely.
--audit-max-age-days <int> 30 Maximum age (days) of archived audit files. Older archives pruned at rotation time. 0 disables age-based pruning.
--audit-compress true gzip-compress rotated archives. Disable for plain JSONL siblings.
--audit-redact true Mask secrets (API keys, tokens, passwords, private keys, credential headers — see OPERATIONS § Audit redaction) in requests before they reach the audit log, GET /v1/audit, the SSE stream, the dashboard and the pending-approvals list. The policy's notifications.redaction.extra_patterns apply too. false stores requests verbatim and logs a startup warning.
--audit-buffered true Wrap the audit logger in a bounded async queue with disk-overflow durability so /v1/check does not wait on the audit mutex. false writes to the file synchronously.
--audit-queue-size <int> 1024 Bounded queue size for the buffered async logger. Ignored unless --audit-buffered.
--audit-workers <int> 4 Worker goroutines draining the buffered audit queue. Ignored unless --audit-buffered.
--audit-overflow-path <path> <audit-log>.overflow.jsonl Disk-overflow spill file used when the buffered queue saturates. Ignored unless --audit-buffered.
--audit-seal off Seal the audit log into a hash chain that vanzik audit verify checks for edited, removed or reordered entries. File backend only: with --audit-backend=store the server refuses to start (exit 2). Not encryption. See OPERATIONS § Sealing and anchoring.
--audit-seal-key-file <path> (empty) HMAC-SHA256 key for --audit-seal: the file's content, surrounding whitespace ignored. The server refuses to start when the file is missing, empty, or (on Unix) readable by every user. Without it the chain is plain SHA-256, which anyone who can write the file can recompute; the server warns at startup. Given without --audit-seal, it is a usage error. vanzik setup uses it.
--audit-anchor-interval <dur> 5m How often to log the head of the sealed chain (audit anchor segment=… seq=… hash=…) and set vanzik_audit_seal_seq, when something new was sealed. Anchors are also published at startup, at each rotation and at shutdown. 0 turns the periodic anchor off. Ignored unless --audit-seal.
--notify-spool <path> (empty) JSONL spool file for notification events that overflow the dispatch queue — spooled events are redelivered by a recovery loop (including leftovers from a previous process) instead of dropped. Empty disables (drop-on-full).
--debug-pprof off Expose Go pprof handlers on a separate localhost-only listener (--debug-pprof-port). Off by default; enable for performance investigations only. Tunnel via kubectl port-forward / ssh -L to access remotely — this listener never binds beyond 127.0.0.1.
--debug-pprof-port <int> 6060 Port for the localhost-only pprof listener. Ignored unless --debug-pprof.
--persist true Persist runtime state (approvals, rate-limit buckets, cost accumulators) to a durable store so it survives restarts. Zero-config: auto-creates vanzik.db (SQLite). Set false for pure in-memory state. The store is off the /v1/check hot path except for first-time tenant lookups — see Persistence & multi-tenancy.
--store-dsn <dsn> (empty) Durable store location. Empty ⇒ zero-config SQLite at <data-dir>/vanzik.db; a SQLite file path is also accepted. A postgres:// / postgresql:// DSN selects the PostgreSQL backend — required for multi-node deployments. Ignored when --persist=false.
--data-dir <path> . Directory for the zero-config SQLite database (vanzik.db + its -wal/-shm sidecars). Ignored when --store-dsn is set or --persist=false.
--node-id <id> (hostname) This replica's identity in the shared PostgreSQL store. Must be distinct per replica (two processes on one host need explicit values; in Kubernetes pass the pod name). No effect on the SQLite backend.
--reconcile-interval <dur> 2s Cadence of the background reconcile that merges other nodes' rate/cost consumption and approval state into this node's in-memory view (and publishes this node's). Postgres-only: forced off on the SQLite backend. Smaller ⇒ tighter rate-limit overshoot bound and fresher cross-node approvals, more store traffic. Never touches the /v1/check hot path.
--audit-backend <file|store> file Where the audit trail lives. file = JSONL (rotation + migration, the default). store = the durable store's indexed audit_entries table (one-file deployment, indexed /v1/audit queries). store requires --persist and always runs buffered (async) — a synchronous DB write per request would break the <3 ms budget, so buffering is forced.

Policy file#

When --policy is not given, server, validate, check and test use the first of:

  1. $VANZIK_POLICY.
  2. configs/default.yaml in the current folder (the flag's default, so running from a checkout works).
  3. The starter policy the installer writes: $XDG_CONFIG_HOME/vanzik/default.yaml (else ~/.config/vanzik/default.yaml, on macOS too), then /etc/vanzik/default.yaml (a root install, and the container image). On Windows, %APPDATA%\vanzik\default.yaml.

If none exists, the command stops and lists where it looked. validate, check and test print Using policy file <path> (…) on stderr when the file wasn't named with --policy; the server's startup log always names it (Loaded policy: <name> from <path> …).

Bind behavior#

  • --api-key set: binds on 0.0.0.0:<port> (all interfaces).
  • --api-key unset: binds on 127.0.0.1:<port> only. An INFO line is logged at startup. Remote agents cannot connect. This is the #1 source of "connection refused" for new users.
  • --bind <host>: binds on <host>:<port> instead. With --api-key any address is accepted; without one only a loopback address (127.0.0.1, ::1, localhost) is, and anything else exits 2 before listening. Use --bind 127.0.0.1 with --api-key when a reverse proxy on the same host is the only client.

Persistence & multi-tenancy (v0.6)#

By default server is stateful: runtime state survives a restart. On a clean run vanzik server creates vanzik.db in the working directory (override with --data-dir) and:

  • hydrates the in-memory approval queue, rate-limit buckets, and cost accumulators from the store on boot, then
  • write-behind syncs them back on a background ticker (≥ 1 s) and on graceful shutdown.

The store is a cold-path component — it is never read or written on the /v1/check request path, so the <3 ms p99 budget is unaffected. The one exception is tenant lookup on /v1/t/<tenant>/… routes: a tenant not yet in the in-memory cache is read from the store on its first lookup, and an unregistered tenant id costs at most one store read per --tenant-policy-refresh-interval (one per lookup when the interval is 0). --persist=false keeps all state in memory only.

Multi-node. With --store-dsn postgres://…, every replica shares the same durable tier and additionally reconciles its in-memory state with the other nodes every --reconcile-interval (default 2s), identified by --node-id. Memory stays authoritative per node — reconciliation is background-only, so distributed rate/cost limiting is bounded-overshoot rather than globally strict, and cross-node approval staleness is at most one interval. Semantics and sizing guidance: OPERATIONS.md.

Tenancy. The local tenant's policy comes from --policy. Additional tenants are registered in the store with vanzik tenant and addressed via the /v1/t/<tenant>/... route family; each tenant is evaluated against its own policy, with isolated approvals, rate limits, cost accumulators, and audit. A tenant that has no registered policy is rejected over HTTP with 404 {"error":"tenant not found"} (see API.md); the offline vanzik check --tenant-id path returns a synthetic DENY with matched_rule="deny:tenant:not_found".

Examples#

# Local dev — localhost-only, dashboard on, a log line per policy reload.
vanzik server --policy configs/default.yaml --dashboard --watch

# After the one-line installer: the starter policy is found automatically.
vanzik server --dashboard

# Production behind a reverse proxy.
vanzik server \
  --policy /etc/vanzik/policy.yaml \
  --audit-log /var/lib/vanzik/audit.jsonl \
  --api-key "$VANZIK_API_KEY" \
  --base-url https://guardrails.example \
  --allowed-origin https://app.example \
  --tls-terminated-upstream \
  --session-cost-ttl 24h \
  --dashboard

# Multi-node (v1.0) — every replica points at the same PostgreSQL and
# carries its own --node-id (here: the pod name).
vanzik server \
  --policy /etc/vanzik/policy.yaml \
  --store-dsn "postgres://vanzik:$PGPASS@pg.internal:5432/vanzik" \
  --node-id "$POD_NAME" \
  --reconcile-interval 2s \
  --api-key "$VANZIK_API_KEY"

Signals#

SIGINT/SIGTERM → graceful shutdown (drains in-flight requests up to the internal shutdown timeout, closes audit logger and notifier).


vanzik validate#

Load a policy file and report rule count / scope count. Give the file as an argument (vanzik validate policy.yaml) or with --policy; with neither, it validates the file the server would load (see Policy file). Exits 1 on parse error, load-time validation failure (e.g., .. in a filesystem path), or missing required fields (version, name).

Non-fatal warnings go to stderr as WARN: … lines and don't change the exit code unless you pass --strict: a scope that appears in more than one block (the blocks are merged), a scope name one or two edits away from a built-in one (shel → "did you mean shell?"), a path pattern whose single * crosses /, and a rule that has paths but no action (its paths never match, so it allows or denies nothing): WARN: policy: rules[0](filesystem).deny[0] has paths but no action, so its paths never match; add an action (read, write or delete) to the rule. The location varies (rules[i](<scope>).<list>[j] or agents.<id>.override[i](<scope>).<list>[j]), and the warning fires for a path rule in any scope. See POLICY_REFERENCE § Load-time validation.

Flag Default Description
--policy <path> found automatically Policy file to validate. Same as giving it as an argument; passing both is an error.
--strict false Exit 1 if the policy loads with any warning.
vanzik validate configs/examples/trading-bot.yaml
# VALID: trading-bot-policy — 13 rules across 4 scopes

vanzik validate /tmp/broken.yaml
# INVALID: parsing policy YAML: yaml: unmarshal errors:
#   line 6: cannot unmarshal !!seq into string

vanzik validate /tmp/typo.yaml --strict
# WARN: policy: rules[1] scope "shel" is not a built-in scope — did you mean "shell"? …
# INVALID (--strict): my-policy loads, but with 1 warning(s)

Use in CI:

for f in configs/default.yaml configs/examples/*.yaml; do
  vanzik validate --strict "$f" || exit 1
done

vanzik check#

Run a single policy check (or a batch from stdin) against a local policy file without going through the HTTP server. Useful in CI pipelines, pre-commit hooks, and one-shot scripts that want a deterministic verdict on a candidate action.

Synopsis#

vanzik check [flags]

Input modes (mutually exclusive)#

Exactly one of these selects how requests enter the subcommand. Specifying more than one returns exit code 3.

Mode How
Per-field flags (default) --scope, --command, --path, --domain, --url, --action, --agent-id, --session-id, --est-cost, --meta
--request '<json>' One JSON object inline on the command line
--stdin One JSON object read from stdin
--batch JSON Lines (one request per line) read from stdin
--watch <file> Follow a JSONL file (tail -f) and verdict each appended request — one policy load for the whole stream, policy hot-reloads on edit. Runs until SIGINT/SIGTERM, then exits with the aggregate code

Flags#

Flag Default Description
--policy <path> found automatically Policy YAML to evaluate against. When not given, see Policy file; nothing found → exit 3. Validated at startup; missing or malformed → exit 3.
--tenant-id <id> local Tenant identifier. The offline check command evaluates against the supplied policy file only — an unknown tenant resolves to a synthetic DENY with matched_rule="deny:tenant:not_found".
--request <json> (empty) Single check from a JSON string. Mutually exclusive with --stdin/--batch.
--stdin off Read a single JSON request object from stdin.
--batch off Read JSONL (one request per line) from stdin.
--watch <file> (empty) Follow a JSONL file and verdict each appended request. Mutually exclusive with --request/--stdin/--batch. Only newline-terminated lines are processed (a torn mid-append write is buffered until completed); a malformed line aborts with exit 3.
--output <fmt> text Output format: text (human-friendly) or json (one JSON object per request, matching the /v1/check response shape).
--scope <name> (empty) Required for the per-field flag mode. shell, filesystem, network, cost, data, etc.
--command <str> (empty) Shell command to evaluate (shell scope).
--action <name> (empty) Action name (read, write, delete, ...) — typically paired with --path.
--path <p> (empty) Filesystem path.
--domain <d> (empty) Network domain.
--url <u> (empty) Request URL.
--agent-id <id> (empty) Agent identifier (drives per-agent overrides in the policy).
--session-id <id> (empty) Session identifier (cost accumulator key).
--est-cost <f> 0 Estimated cost (cost scope).
--meta <pairs> (empty) Comma-separated k=v pairs (e.g. team=ml,prio=high). For metadata containing commas/quotes, use --request '{"meta":{}}' instead.

Exit codes#

The subcommand returns a structured exit code so shell pipelines can branch on the outcome.

Code Meaning
0 ALLOW — single mode; or every entry ALLOW in batch mode.
1 DENY — single mode; or any entry DENY in batch mode.
2 REQUIRE_APPROVAL — single mode; or any approval and no deny in batch mode.
3 Error — missing/invalid policy, malformed JSON, flag misuse (including an unexpected argument), mutually exclusive modes.

Severity precedence in batch mode is error > deny > approval > allow, regardless of numeric order: a deny (1) outranks an approval (2).

Examples#

# Per-field flag mode — the simplest form.
vanzik check --policy configs/default.yaml \
  --scope shell --command "rm -rf ./old_data" --agent-id my-bot

# Single check via inline JSON request.
vanzik check --policy configs/default.yaml \
  --request '{"scope":"shell","command":"ls","agent_id":"my-bot"}'

# Single check via stdin.
echo '{"scope":"shell","command":"ls","agent_id":"my-bot"}' | \
  vanzik check --policy configs/default.yaml --stdin

# Batch mode (JSONL via stdin).
cat <<EOF | vanzik check --policy configs/default.yaml --batch
{"scope":"shell","command":"ls","agent_id":"bot1"}
{"scope":"shell","command":"rm -rf /","agent_id":"bot1"}
{"scope":"network","domain":"api.openai.com","agent_id":"bot1"}
EOF

# Watch mode: follow a JSONL file and verdict requests as they are
# appended (one policy load for the whole stream; Ctrl-C to stop and
# get the aggregate exit code).
vanzik check --policy configs/default.yaml --watch actions.jsonl

# JSON output for downstream tooling.
vanzik check --policy configs/default.yaml \
  --request '{"scope":"shell","command":"ls"}' --output json
# {"schema_version":"v1","decision":"DENY","reason":"...","matched_rule":"..."}

CI gate example#

Fail the pipeline if any deploy command would be denied:

# deploy_actions.jsonl contains one ActionRequest per line.
if ! vanzik check --policy ci-policy.yaml --batch --output json \
       < deploy_actions.jsonl > /tmp/check_out.jsonl; then
  echo "Policy violation in deploy plan; see /tmp/check_out.jsonl"
  exit 1
fi

Behavior notes#

  • Without --watch, the subcommand is one-shot — no policy hot-reload. Each invocation reloads the policy, so pipelines that re-invoke check per action pay the load cost each time; --watch <jsonl-file> streams requests through a single policy load instead.
  • The decoder rejects unknown JSON fields. A typo like "actions":"read" (instead of "action":"read") returns exit 3, so silent default-deny on a malformed request is impossible.
  • Cost-scope evaluations DO reserve session cost into the in-memory accumulator for the lifetime of the process, but the accumulator is discarded on exit. Two consecutive vanzik check calls do not see each other's reservations — that's a server feature, not a CLI feature.

vanzik test#

Run a policy against test cases and report which pass. Each case is an action and the verdict the policy must give it. Use it in CI so a policy change that allows or blocks the wrong thing fails the build. No server needed.

Synopsis#

vanzik test [flags] <fixture.yaml | folder>...

Give fixture files, folders, or both. A folder is searched for *_test.yaml and *_test.yml files; its subfolders are not searched. A file holds one YAML document: a second one after --- is a load error, not something to skip.

Fixture files#

policy: ../default.yaml          # optional; relative to this file's folder
tenant: ""                       # optional; see below
tests:
  - name: plain ls is allowed
    request: {scope: shell, command: "ls -la", agent_id: dev}
    expect: ALLOW                    # ALLOW, DENY or REQUIRE_APPROVAL
    expect_rule: "allow:shell:ls *"  # optional: the exact rule
    expect_reason_contains: "shell"  # optional
  - name: deploy once the tests ran
    prior: [{scope: shell, command: "make test", agent_id: dev}]
    request: {scope: shell, command: "make deploy", agent_id: dev}
    expect: ALLOW
  - name: Claude Code sudo needs approval
    hook: claude-code                # instead of request:
    input: {tool_name: Bash, tool_input: {command: "sudo ls"}}
    expect: REQUIRE_APPROVAL
Key Meaning
policy Policy file for every case in this file, relative to the fixture's folder. Without it: --policy, else the file the server would load (see Policy file).
tenant Tenant to check as. Default local. The fixture's policy is served to this tenant, so its cases test that policy as the tenant would get it (history and cost are kept per tenant).
tests[].name Required, and unique within the file. --run matches against it.
tests[].request The action, with the /v1/check request fields: scope (required), action, command, path, domain, url, agent_id, session_id, est_cost, meta. Give this or hook and input.
tests[].hook Instead of request: the coding agent whose tool call input is, one of claude-code, cursor, gemini-cli, codex. Needs input.
tests[].input The tool call as that agent sends it to vanzik hook, written as YAML. It is read by the agent's own hook code, unchanged; Cursor's input needs its hook_event_name. When it becomes several checks (a Codex patch, one per file), the most severe verdict counts. See POLICY_REFERENCE § Testing a coding agent's tool calls.
tests[].expect Required. ALLOW, DENY or REQUIRE_APPROVAL.
tests[].expect_rule Optional. The exact matched_rule the verdict must have, such as allow:shell:ls *. It is compared as written: the * there is part of the rule's name, not a wildcard, so a case can't pass on a different, broader rule. A default deny has no rule; check its reason instead.
tests[].expect_reason_contains Optional. Text the verdict's reason must contain.
tests[].prior Optional. Actions recorded as allowed just now, before the check, for require_prior conditions. Same fields as request; each needs a scope and an action or a command.

Unknown keys are an error at every level, so a misspelled expect_rul: fails the load instead of passing a case that checks nothing.

Flags#

Flag Default Description
--policy <path> found automatically Policy for fixtures without a policy: line. A fixture's own policy: wins.
--strict false Treat policy load warnings as a load failure (exit 3), as validate --strict does. Without it the warnings are printed as WARN: lines on stderr.
--run <regexp> (all) Only run cases whose name matches this Go regular expression.
--output <fmt> text text, json (one document with passed, failed and every case), or junit (JUnit XML, one <testsuite> per fixture file, for CI test-report viewers). A hook case's result names its hook: "hook" in JSON, a hook property on the JUnit <testcase>.

Exit codes#

Code Meaning
0 Every case passed.
1 A case failed, or --run matched no case.
2 Command-line mistake: no fixture given, unknown flag, bad --output or --run.
3 A fixture or policy could not be loaded (missing file, unknown key, invalid policy, a warning with --strict). No case runs.

Examples#

vanzik test configs/tests
# configs/tests/default_test.yaml (policy configs/default.yaml)
#   PASS  plain ls is allowed
#   ...
# PASS: 162 case(s) passed

vanzik test --run 'claude-code' configs/tests

# A failed case shows what was expected and what the policy said.
#   FAIL  deploy once the tests ran
#         want ALLOW, got DENY
#         rule:   (none: no rule matched)
#         reason: "No matching allow rule (default deny)"
# FAIL: 1 of 2 case(s) failed

vanzik test --output junit configs/tests > policy-tests.xml

Behavior notes#

  • Every case runs on a fresh engine over the same loaded policy. Cost reserved for a session_id in one case does not count in the next, and a case sees no history except its own prior: entries.
  • Rate limits are not checked: the server applies them before the policy engine, and test only runs the engine. The same is true of check.
  • REQUIRE_APPROVAL has no approval_id here; there is no approval queue.
  • A hook case's input that the hook doesn't check (another tool or event) or can't read fails the case with the reason, since it would test nothing. Each check it becomes runs on a fresh engine, with the case's prior: entries.
  • The cases for the policies in this repo are in configs/tests/, and CI runs them. See POLICY_REFERENCE § Testing policies.

vanzik approve <id> / vanzik deny <id>#

POST to /v1/approve/{id} or /v1/deny/{id}. Used by humans or scripts to resolve REQUIRE_APPROVAL decisions.

Flag Default Description
--url <url> $VANZIK_URL, else http://localhost:8080 Server URL. Must include the scheme (http:// or https://).
--guard-url <url> Same as --url (the name the MCP gateway and LLM proxy use).
--api-key <key> $VANZIK_API_KEY Bearer token. Required if the server was started with --api-key.
vanzik approve ap_1a2b3c4d5e6f7890abcdef1234567890
# Approved ap_1a2b3c4d5e6f7890abcdef1234567890

VANZIK_API_KEY=$KEY vanzik deny ap_deadbeef… --url https://guardrails.example
# Denied ap_deadbeef…

Exit 1 when the server can't be reached or answers with an error, each with a message that says what to do: Cannot connect to Vanzik at … (connection refused). Is 'vanzik server' running there?, the server requires an API key (HTTP 401), no pending approval ap_… (…), ap_… was already approved. Exit 2 for a missing or extra argument. Approval IDs are ap_<32hex> as returned by /v1/check.


vanzik status#

Quick human-readable health + pending list. Hits /health (unauthenticated) then /api/pending (authenticated).

Flag Default Description
--url <url> $VANZIK_URL, else http://localhost:8080 Server URL.
--guard-url <url> Same as --url.
--api-key <key> $VANZIK_API_KEY Bearer token.
vanzik status
# Vanzik server: OK (http://localhost:8080)
# Pending approvals: 2
#   [ap_123…] scope=shell action="rm -rf ./old_data" agent=researcher-01
#   [ap_456…] scope=cost  action=""                   agent=trading-bot

If the server is running without --api-key, pending approvals appear unauthenticated. If you set --api-key on the server but not here, the pending list shows "unauthorized". /api/pending exists only when the server runs with --dashboard; without it the line reads Pending approvals: unavailable (the server was started without --dashboard). The exit code is 1 only when the server can't be reached.


vanzik audit#

Query /v1/audit for recent decisions. All filters are optional and AND-combined.

Flag Default Description
--url <url> $VANZIK_URL, else http://localhost:8080 Server URL.
--guard-url <url> Same as --url.
--agent <id> (none) Filter by exact agent_id.
--decision <D> (none) ALLOW, DENY, or REQUIRE_APPROVAL.
--scope <name> (none) shell, filesystem, network, browser, cost, data, mcp_tool.
--transport <name> (none) Filter by audit transport tag: sdk, mcp_gateway, llm_api_proxy, or a coding-agent hook's claude_code, cursor, gemini_cli, codex_cli. Entries with no transport tag count as sdk: transport=sdk matches them, any other value leaves them out.
--session <id> (none) Filter by exact session_id.
--parent-session <id> (none) Only the calls of sessions started by this session (entries whose request named it as parent_session_id).
--since <time> (none) Entries at or after this time. A duration counts back from now (2h, 30m, 90s); otherwise an RFC 3339 time (2026-10-14T09:00:00Z).
--until <time> (none) Entries before this time (exclusive). Same forms as --since. A value in neither form is a usage mistake (exit 2).
--limit <int> 50 Max entries. Server clamps silently above configured ceiling (default 1000).
--order <desc|asc> desc desc shows the newest entries first; asc, the oldest.
--api-key <key> $VANZIK_API_KEY Bearer token.

A server older than 1.4 ignores the parameters behind --since, --until and --parent-session and returns every entry. So when one of those flags is set, the CLI first asks the server whether it reads them (one /v1/audit request with a since that isn't a time: a server that reads it answers 400, an older one 200), and exits 1 if it doesn't. Asking, rather than comparing version numbers, also works with dev builds.

vanzik audit --session sess-42 --since 2h --order asc
vanzik audit --decision DENY --scope shell --limit 20
# Showing 20 audit entries:
#
#   2026-04-19T12:03:44Z  DENY                transport=sdk           scope=shell         agent=researcher-01    dd if=/dev/zero of=/dev/sda
#     reason: Raw disk operations are blocked
#   ...

The CLI uses /v1/audit?limit=N directly — pagination (?offset=) is supported on the HTTP API but not exposed as a CLI flag yet; use curl for paginated exports, or page by time with --since and --until. To see sessions grouped, with their parent and child sessions, use the dashboard's Sessions view or GET /v1/sessions.

vanzik audit verify#

Check a sealed audit log (server --audit-seal) for tampering. It reads files only, so the server does not need to run, and it works on a copy. It finds the live file, follows _meta.rotated_from back through the rotated archives (gzip or plain), and checks oldest to newest: each entry's hash, the seq and prev_hash links between entries, and the link between each file and the one before it.

vanzik audit verify [--audit-log <path>] [--key-file <file> ...] [--anchor <seq>:<hash> ...] [--output text|json]
Flag Default Description
--audit-log <path> audit.jsonl The live audit log (the server's --audit-log).
--key-file <file> (none) The HMAC key (the server's --audit-seal-key-file). Repeat it for a log sealed under more than one key. Needed for a log sealed with a key.
--anchor <seq>:<hash> (none) A published anchor to check (from an audit anchor log line or GET /v1/audit/head). Repeatable.
--output <text|json> text json prints the report as JSON.

The report lists the files, the sealed seq range, where the chain starts when older files were pruned, how many entries were written before sealing began (not protected), the first broken file:line and what broke (hash mismatch, seq gap, prev_hash mismatch, segment link broken, incomplete line), and each anchor: matches, does not match, missing (the log ends before it: entries were removed), or not checkable (its seq is in the pruned part).

vanzik audit verify --audit-log /var/lib/vanzik/audit.jsonl \
  --key-file /var/lib/vanzik/audit-seal.key --anchor 1200:3f9a…
# Audit log: /var/lib/vanzik/audit.jsonl
# Files:     3, oldest first
#   audit.jsonl.20261012T080000Z.gz
#   audit.jsonl.20261013T020000Z.gz
#   audit.jsonl
# Sealed:    1312 entries, seq 801 to 2112
# Seal:      hmac-sha256 key 3eb1bd439947eb76
# The chain starts at seq 801 (older files were pruned).
# Anchor 1200:3f9a…: matches
# OK: the chain is intact.

Exit status: 0 the chain is intact, 1 it is broken, no entry is sealed (NOT SEALED: the log has entries and none is under a seal, so nothing was verified; with --key-file this is a break), or an anchor does not match, 2 command-line mistake (also: the log is sealed with a key and no --key-file was given), 3 a file can't be read. What a passing verify does and does not prove: OPERATIONS § Sealing and anchoring.


vanzik report#

Write a compliance evidence report for a period: decisions by decision, scope, transport, tenant and agent; the top deny and require_approval rules; the approvals on record; the agents and sessions seen; whether the audit log is sealed and intact; posture checks; and a mapping to SOC 2 criteria. It is supporting evidence for an audit, not a certification. What the report holds, its JSON shape and its limits: COMPLIANCE.md.

vanzik report [--since <time>] [--until <time>]
                  [--audit-log <path> [--key-file <file> ...] | --url <url> [--api-key <key>] [--tenant-id <id>]]
                  [--policy <file>] [--output md|json|html] [--out-file <file>]
Flag Default Description
--since <time> 24h before --until Start of the period, inclusive.
--until <time> now End of the period, exclusive.
--audit-log <path> audit.jsonl The live audit log. Its rotated archives (gzip or plain) are read too.
--key-file <file> (none) HMAC seal key, passed to the audit verify check. Repeatable.
--url <url> (none) Read the audit log from this server instead of the files. --guard-url is the same. VANZIK_URL does not switch to server mode.
--api-key <key> $VANZIK_API_KEY, then the key setup saved Bearer token for --url.
--tenant-id <id> local Tenant to report on with --url (uses /v1/t/<id>/audit).
--policy <file> see below Policy file named in the report with its SHA-256.
--output <md|json|html> md Markdown, JSON (a stable shape) or one self-contained HTML page.
--out-file <file> stdout Write the report to this file (mode 0600) and print one line saying where.

Times are RFC 3339 (2026-10-01T09:00:00Z), a date (2026-10-01, midnight UTC), or a duration back in time (90m, 24h, 7d, 1d12h; d is 24 hours). A duration --until counts back from now; a duration --since counts back from --until.

Reading files (the default), the report runs the same check as audit verify and then reads every entry of the files it walked. With --url it pages GET /v1/audit oldest first by time (since moves forward page by page) and drops any entry outside the period itself, so a server that predates since and until still gives a correct report. It reads the seal head from GET /v1/audit/head before and after, but does not verify the chain. A server with the file backend answers from its live file only, so rotated archives are left out, and entries that rotate out while the report reads are lost; on a sealed server the report says so. Run the report on the files to include archives. --url must not hold a user name or password; give the key with --api-key.

Without --policy, a report on files names the policy server would load (Policy file); with --url, only --policy names one.

vanzik report --since 2026-09-01 --until 2026-10-01 \
  --audit-log /var/lib/vanzik/audit.jsonl \
  --key-file /var/lib/vanzik/audit-seal.key \
  --output html --out-file september.html
# Report written to september.html (audit entries in the period: 48211; audit log: intact).

Exit status: 0 the report was written, 1 it was written but the audit log failed verification, 2 command-line mistake, 3 the log, a key file, the policy file or the server can't be read. A log sealed with a key and no --key-file is not an error here: the report checks everything but the hashes (every entry after the seal is sealed, seq has no gaps, each prev_hash links up), and says the hashes were not checked.


vanzik tenant (v0.6)#

Manage per-tenant policies in the durable store. Operates directly on the store database. The server need not be running (SQLite WAL permits a concurrent writer). A running server sees a new tenant on its next lookup (within --tenant-policy-refresh-interval if that tenant ID was queried before it was registered); replacements and removals reach it within --tenant-policy-refresh-interval (default 30s). Requires persistence (the store); these commands open it directly.

vanzik tenant put <tenant-id> --policy <file.yaml> [--store-dsn <dsn>] [--data-dir <dir>]
vanzik tenant list                                 [--store-dsn <dsn>] [--data-dir <dir>]
vanzik tenant rm  <tenant-id>                      [--store-dsn <dsn>] [--data-dir <dir>]
Subcommand Description
put <id> --policy <f> Validate <f> (same checks as validate) and register it as tenant <id>'s policy. Re-running replaces it. A malformed policy is rejected and never stored. The tenant ID can come before or after the flags.
list List every registered tenant id (the local tenant is served from --policy, not the store, so it is not listed).
rm <id> Remove a tenant's policy. Reports whether a row existed.

--store-dsn / --data-dir resolve the database exactly like server (empty DSN ⇒ <data-dir>/vanzik.db).

# Register a tenant, then check it via its tenant-aware route.
vanzik tenant put acme --policy acme-policy.yaml
vanzik tenant list
#   Registered tenants (1):
#     acme
curl -s -X POST localhost:8080/v1/t/acme/check \
  -H 'Content-Type: application/json' \
  -d '{"scope":"shell","command":"deploy app"}'
# → evaluated against acme's policy, independently of the local tenant

A tenant added while the server is running is loaded lazily on its first /v1/t/<id>/... request (one store read, then cached). Tenants present at boot are eager-loaded.


vanzik migrate#

Run registered on-disk audit-schema migrations. Each migration has a Detect() step — it only runs if the on-disk format matches. See FILE_FORMATS.md for the schema history.

Flag Default Description
--audit-log <path> audit.jsonl Audit log to migrate in place.
--checkpoint <path> <audit-log>.replay-checkpoint The replay checkpoint vanzik server reads and writes at boot.
--backup-dir <path> <audit-dir> Where rollback copies are written.
--dry-run off Log intended actions without writing.
--list off Print registered migrations and exit.
--id <name> (none) Run only the named migration, even if Detect() returns false (operator override).
--reset-checkpoint off Delete the replay checkpoint first (forces a full replay on the next server start). Prints checkpoint removed (<path>) when a file was deleted and no checkpoint found at <path> otherwise — it never reports success for a file that was not there.
vanzik migrate --list
vanzik migrate --dry-run
vanzik migrate --audit-log /var/lib/vanzik/audit.jsonl

Startup migrations run automatically inside vanzik server before the audit logger opens — the migrate subcommand is for operator-driven out-of-band runs.

vanzik migrate --from-agentguard#

Moves this user's AgentGuard 1.x install to Vanzik. It takes no other flag. In order, it stops and removes AgentGuard's login service (and changes nothing if it can't), moves ~/.config/agentguard and the data folder to Vanzik's by renaming them (files unchanged), leaves a link in the old config folder's place, updates setup.json's paths, renames agentguard.db to vanzik.db, points the Claude Code, Cursor, Gemini CLI and Codex hook entries AgentGuard wrote at vanzik (keeping each old file as .vanzik-backup), and starts the server at login again. It ends with what is left to do. Run again, it finds nothing.

The installers run it after installing, and vanzik setup before its menu. Run as root it changes nothing: it lists a system-wide install to move by hand. Exit codes: 0 migrated or nothing to migrate; 1 a step failed (the summary says which and what to do); 2 usage. See MIGRATION.md.

vanzik migrate --from-agentguard

vanzik version#

vanzik --version (or -version) is the same command, matching the MCP gateway and LLM proxy flags.

vanzik version
# vanzik 2.0.0 (abc1234)

The version comes from the source; the part in parentheses identifies the build:

Built with Shows
make build (-ldflags "-X main.commit=…") the git short hash, e.g. abc1234
go install …/cmd/vanzik@vX.Y.Z or @latest module vX.Y.Z (from Go build info)
go build in a git checkout the short VCS revision, with -dirty if the tree had local changes
make docker (--build-arg COMMIT=…) the git short hash
any build with neither ldflags nor VCS information dev

Update notice on startup (v0.5.1+)#

Every subcommand except server, help and hook (that is setup, validate, check, test, tenant, status, approve, deny, audit, migrate, report and version) kicks off an async best-effort check against the GitHub Releases API at startup (800 ms wait budget, 1.5 s HTTP timeout). If a newer release exists, one line lands on stderr before subcommand output; otherwise silent.

Notice: Vanzik v<latest> is available (you have v<current>). Update: vanzik setup — what's new: https://github.com/lictorate/vanzik/releases/latest

The command it names depends on how this copy was installed:

Installed with Update command in the notice
The one-line installer or a release archive vanzik setup (choose Update); re-running the installer works too
go install …@vX.Y.Z / @latest go install lictorate.com/vanzik/v2/cmd/vanzik@latest
The container image (it sets VANZIK_DISTRIBUTION=container) docker pull ghcr.io/lictorate/vanzik:latest, then recreate the container

Both replace the binaries and keep your policy, and say whether they updated or downgraded (with a warning). See SETUP.md.

server never performs the check (nor does serve, the same command): the enforcement server opens no outbound connection the operator did not configure (see THREAT_MODEL.md). The check is also skipped for development builds — a version string containing dev, or no -ldflags commit (commit=dev) and no tagged release version in the Go build info, as with go build on an untagged or modified checkout. go install …@vX.Y.Z and @latest builds record the release tag, so they do check. It is also skipped when VANZIK_NO_UPDATE_CHECK is set to any value other than 0, or when the HTTP request fails. It is not run for vanzik help or a mistyped command either, nor for vanzik hook, which runs before every coding-agent tool call and must answer in milliseconds. Never touches stdout, never affects exit codes. Only the vanzik binary has the check; the MCP gateway and LLM proxy have none.


Environment variables#

Var Consumed by Default
VANZIK_API_KEY server, approve, deny, status, audit, report --url (when --api-key unset); the MCP gateway and LLM proxy; the SDKs empty
VANZIK_URL approve, deny, status, audit (when --url unset); the MCP gateway and LLM proxy (when --guard-url unset); the SDKs http://localhost:8080 (CLI, SDKs), http://127.0.0.1:8080 (gateway, proxy)
VANZIK_POLICY server, validate, check, test (when --policy unset) — see Policy file unset
VANZIK_NO_UPDATE_CHECK Every subcommand except server, help and hook (which never check) — disables the GitHub Releases startup check when set to any value other than 0 unset
VANZIK_TENANT_ID The SDKs only — a value other than local routes calls to /v1/t/<tenant>/… unset
VANZIK_DISTRIBUTION The update notice; the container image sets it to container unset

A flag always wins over its environment variable. The variables' AgentGuard 1.x names (AGENTGUARD_*) are never read: a command that would fall back to one that is set, with its VANZIK_* name unset, exits naming the new one (see MIGRATION.md). vanzik hook is the exception; it runs anyway and names the variable when it couldn't check a call. When no key is given at all, approve, deny, status, audit, report --url, the MCP gateway and the LLM proxy use the key vanzik setup saved (~/.config/vanzik/api-key, %APPDATA%\vanzik\api-key). The server never picks that file up by itself — a key changes which interfaces it listens on — only through --api-key-file.

The installers read VANZIK_VERSION, VANZIK_INSTALL_DIR, VANZIK_DOWNLOAD_URL, VANZIK_NO_MODIFY_PATH, VANZIK_UNINSTALL and VANZIK_PURGE (SETUP.md); vanzik setup's Update reads VANZIK_VERSION and VANZIK_DOWNLOAD_URL too.