Troubleshooting
vanzik v2.0updated view on github
Symptom-first reference. If something is wrong, skim the headers below for the one that matches what you are seeing.
Agents get connection refused / connect: timed out#
Likely cause: --api-key is not set, so the server is bound to 127.0.0.1:<port> only. Agents running on any other host cannot reach it.
Confirm: the server log on startup will show:
INFO: binding to 127.0.0.1:8080 (localhost only) — set --api-key to listen on all interfaces
Fix: set a Bearer token.
export VANZIK_API_KEY="$(openssl rand -hex 32)"
vanzik server --api-key "$VANZIK_API_KEY" ...
Details: docs/DEPLOYMENT.md §1.
Dashboard login loops back to the login page#
Likely cause: you are running behind an HTTPS-terminating reverse proxy, but Vanzik is issuing session cookies without Secure. The browser sets them, then refuses to send them back on the next HTTPS request.
Confirm: in browser devtools (Application → Cookies), vanzik_session is present but Secure column is blank.
Fix: add --tls-terminated-upstream.
vanzik server --tls-terminated-upstream ...
Details: docs/DEPLOYMENT.md §2a.
Slack / webhook approval links point at localhost#
Likely cause: --base-url is not set, so approval_url falls back to http://localhost:<port>.
Fix:
vanzik server --base-url https://guard.example.com ...
Policy edits do not take effect#
Likely cause (a): the server loaded a different file than the one you edited. The startup log names it: Loaded policy: <name> from <path> …. Without --policy, the server picks the file itself (see CLI.md). Reloading is always on; --watch only adds a Policy reloaded: … log line per reload, which shows whether an edit was picked up:
vanzik server --policy ./my-policy.yaml --watch ...
Likely cause (b): the file's mtime did not change. The watcher reacts to fsnotify events on the file's directory, and falls back to polling os.Stat(path).ModTime() every 2s (pkg/policy/watcher.go DefaultPollInterval) only when fsnotify can't start. Either way a reload happens only when the mtime has advanced. If you edited through a tool that preserves mtime, touch the file:
touch configs/default.yaml
Likely cause (c): your edit is syntactically invalid. Parse errors are logged but do not halt the watcher — the old policy keeps serving. Check the server log for Policy reload failed: and run vanzik validate --policy configs/default.yaml to pinpoint the error.
Rate limit works locally but gets bypassed in production#
Likely cause: you are running more than one Vanzik replica on the default SQLite store. Rate-limit buckets are in-memory per instance (pkg/ratelimit/ratelimit.go); an agent can burst past the cap by hitting different pods / VMs round-robin.
Mitigations:
- Share state across replicas:
--store-dsn postgres://…plus a distinct--node-idper replica. Note the shared mode is bounded-overshoot — brief bursts can still exceed the cap by ≈reconcile-interval × peak rateper extra replica before the nodes converge. - Divide per-scope
max_requestsby the replica count when writing the policy. - Pin agents to a single Vanzik instance via session affinity.
- Keep
replicas: 1and scale vertically.
Details: docs/OPERATIONS.md.
Approvals disappeared after a restart#
Cause: the queue is persistent by default, so this should only happen if (a) you run --persist=false (in-memory only), (b) the process can't write its --data-dir (check the startup log for store errors), or (c) the entries were created inside the final ≥1 s store-sync window before a hard crash.
Mitigations:
- Run with
--persist(the default) and verifyvanzik.dbexists and grows in--data-dir. - Set SDK
wait_for_approval(timeout=...)to a realistic human SLA — approvals survive a restart, but nobody can resolve them while the process is down. - For mission-critical flows, have the calling agent checkpoint its state so it can re-issue the
checkand get a fresh approval ID.
/metrics or /api/stats are slow / inaccurate right after startup#
Cause: on boot, Vanzik replays the audit log through metrics.IncDecision to seed decision counters so /metrics survives restarts with accurate totals. The replay resumes from a saved byte-offset checkpoint, so normally only entries written since the last checkpoint are read; the whole log is replayed only on first boot, after a reset, or when the checkpoint can't be read (the log says replaying full log). For a multi-GB audit.jsonl, that full scan can take seconds to minutes.
Fix:
- Keep rotation enabled (it is on by default:
--audit-max-size-mb 100, 5 backups) so the active file stays bounded. - Ship historical audit data to an external aggregator and truncate the local file during scheduled maintenance.
Details: docs/OPERATIONS.md.
Webhook / Slack events are missing#
Cause: the notification dispatcher has a bounded queue (DefaultQueueSize = 256). If notifiers are slow or the queue is full, Dispatcher.Send drops the event and increments vanzik_notify_events_dropped_total{notifier, reason}.
Confirm:
curl -s http://127.0.0.1:8080/metrics | grep notify_events_dropped_total
Mitigations:
- Lower notifier timeouts in the policy file (
notifications.dispatch_timeout, or a target's owntimeout), so a slow or flaky endpoint holds a worker for less time. Longer timeouts keep workers busy and make drops more likely. - Reduce notifier fanout (remove unused targets).
- Shard to multiple webhook endpoints if a single Slack channel is a bottleneck.
- Watch
vanzik_notify_queue_depth(one shared queue, so it has nonotifierlabel) — steady growth means the workers cannot drain.
Python SDK always returns DENY in tests#
Cause: the Python SDK defaults to fail-closed on any network error (urllib.error.URLError, OSError, or a non-JSON response body → synthetic DENY with reason "Vanzik unreachable (deny): ..."). If your test environment cannot reach the proxy URL, every call fails closed.
Confirm: inspect result.reason — it will contain "Vanzik unreachable".
Fixes:
- Point the test
Guard(base_url=...)at a running test server, or stub the HTTP layer with the standard-library stub inSDK_PYTHON.md("Option B").responsescan't intercept the SDK's calls: it only patchesrequests, and the SDK usesurllib. - For tests that need fail-open behavior, pass
fail_mode="allow"explicitly. Do not flip this in production unless your threat model treats Vanzik as advisory — the"deny"default is what keeps agents safe when the proxy is down.
A chained or piped shell command is denied after upgrading to 1.2#
Symptom: a command that 1.1.x allowed, such as cat README.md | head -5 or ls | grep foo, is now DENY or REQUIRE_APPROVAL, and the reason names one part of it: No matching allow rule (default deny) (command 2 of 2: "head -5").
Cause: since v1.2.0, a shell command with shell syntax (;, &&, ||, |, &, a line break, $( ), backticks, a redirection) is split into the commands a shell would run, and every one of them must be allowed. In 1.1.x the whole line matched the first command's rule, which is how allow: "ls *" also allowed ls /tmp; rm -rf /. A bare command is its own segment, and ls * needs something after ls, so the ls in ls | grep foo is not allowed by ls *.
Fix: add allow rules for the commands your agents chain (head *, grep *, or ls on its own), or write an allow pattern that contains the operator, which matches command by command:
- scope: shell
allow:
- pattern: "head *"
- pattern: "ls"
- pattern: "cat * | head *"
A deny:shell:unparseable_command verdict means the command used syntax Vanzik doesn't split (a heredoc, $(( )), unbalanced quotes, or nesting deeper than 8 levels); rewrite the command without it. To see what 1.2.0 would decide for commands your agents already ran, replay them with vanzik check --batch — see MIGRATION.md.
*.foo.com does not match foo.com#
Not a bug. Vanzik uses standard glob semantics: *.foo.com requires at least one character before the dot. Note the opposite surprise for paths: a single * does cross / (api/*/users matches api/v1/internal/users, and /workspace/* matches every file under /workspace), so bound path allows with ** segments instead. See POLICY_REFERENCE § Single-star * crosses /.
Fix: add both patterns explicitly.
allow:
- domain: "foo.com"
- domain: "*.foo.com"
Details: docs/POLICY_REFERENCE.md.
Condition has time_window but it is being ignored#
Cause: time_window without require_prior errors out at policy load. LoadFromFile returns the validation error; the server refuses to boot until the policy is fixed. Either remove the orphan time_window or add a require_prior clause. See DEPRECATIONS.md.
Confirm: policy load fails with an error like:
rules[0]("deploy_*").conditions[0]: time_window without require_prior is rejected; pair time_window with require_prior or remove it
Fix: add a require_prior (the pattern/action that must have been allowed within the window), or remove the orphan time_window.
Details: docs/DEPRECATIONS.md and docs/POLICY_REFERENCE.md.
Dashboard shows a red LIVE badge#
Cause: the SSE connection on /api/stream dropped — either the server restarted, the network hiccupped, or your reverse proxy buffered the stream shut (nginx default is proxy_buffering on).
Fixes:
- Wait a few seconds: the dashboard reconnects by itself, and its badge shows
RECONNECTING...meanwhile. If it stays there, the server is down or something between you and it is blocking the stream. - If behind nginx, set
proxy_buffering offon/api/stream. Seedocs/DEPLOYMENT.md§2c.
I see 503 Service Unavailable on /v1/check or /auth/login#
Cause: the approval queue or the dashboard session store is full. (/v1/approve and /v1/deny never return 503.)
- Approval queue, on
/v1/check:MaxPendingApprovals = 10000(pkg/proxy/server.go). When a new approval arrives at capacity, the oldest resolved entry is evicted to make room; if every entry is still pending, the check that needed approval gets 503 withRetry-After: 30. - Session store, on
/auth/login:MaxSessions = 1024(hardcoded inpkg/proxy/auth.go). At capacity, expired sessions are swept first; live sessions are never evicted, so if all are live the login gets 503 withRetry-After: 5.
Mitigation: resolve outstanding approvals faster, or log out unused dashboard sessions (or wait for them to expire) to shrink the session table.
CORS preflight fails from a production frontend#
Cause: --allowed-origin is unset, so Vanzik is in permissive-localhost mode — any non-localhost Origin is rejected.
Fix: set the exact frontend origin.
vanzik server --allowed-origin https://app.example.com ...
Details: docs/DEPLOYMENT.md §3.
Docker: audit log is empty after container restart#
Cause: the audit log is written inside the container at /var/lib/vanzik/audit.jsonl. Without a volume mount, the filesystem is destroyed when the container is removed.
Fix:
docker run -d -p 8080:8080 \
-e VANZIK_API_KEY="$VANZIK_API_KEY" \
-v vanzik-audit:/var/lib/vanzik \
vanzik:latest
Keep -e VANZIK_API_KEY: without an API key the server binds 127.0.0.1 inside the container and the published port refuses connections.
MCP Gateway / LLM API Proxy issues (v0.5+)#
MCP client shows zero tools after pointing at vanzik-mcp-gateway#
The gateway started but no --upstream is wired, or the upstream subprocess crashed at boot. Run the gateway in the foreground and watch for info mcpgw: startup: upstream "<name>" failed to spawn: … (a missing launcher such as npx, uvx or docker shows up here). Verify the upstream command runs standalone. Details: MCP_GATEWAY.md.
LLM API Proxy: SDK hangs / unexpected end of stream#
The proxy was killed mid-stream and the buffered tool-call payload was never flushed. On SIGTERM the proxy drains in-flight requests for 10 s (fixed, pkg/llmproxy/server.go), then closes what is left. Give the host at least that long before it kills the process (TimeoutStopSec=15s / terminationGracePeriodSeconds: 15); a longer grace period doesn't extend the 10 s drain, so a stream that runs past it is still cut.
LLM API Proxy: agent sees 502 Bad Gateway#
The proxy couldn't reach the model provider: the response body reads upstream error: … (for example connection refused). Check --upstream-openai / --upstream-anthropic and that the proxy host can reach the provider (DNS, egress firewall, corporate proxy).
An unreachable Vanzik server (--guard-url) does not cause a 502. With --fail-mode=deny (the default) the agent gets a synthetic refusal with HTTP 200 until the server comes back; curl -sf <guard-url>/health confirms reachability, and VANZIK_API_KEY must match on both sides.
Proxy rejects every incoming connection#
It bound to 127.0.0.1. If your agent runs in a separate container or host, the loopback bind blocks it. Change to --listen 0.0.0.0:<port> and set --proxy-api-key: the proxy refuses to start on a non-loopback address without it (exit code 2), and then requires every client to send the key in the X-Vanzik-Proxy-Auth header. See LLM_API_PROXY.md.
Tool calls flow through the LLM API Proxy but never reach the Vanzik server#
The client's OPENAI_BASE_URL is missing the /v1 suffix, or the request is a plain chat completion (intentionally passthrough — no tool call means nothing to gate). Check the central server's audit log (vanzik audit --transport llm_api_proxy) — if nothing lands there while tool calls run, the traffic isn't reaching the proxy. Set OPENAI_BASE_URL=http://127.0.0.1:8081/v1 (include /v1); for Anthropic use ANTHROPIC_BASE_URL=http://127.0.0.1:8081 (no /v1). Details: QUICKSTART_LLM_PROXY.md.
Still stuck?#
- Run
vanzik status— shows/healthreachability and the pending queue. - Check
/metricson the central server — counters, gauges, histograms, dispatch drops. The LLM API Proxy and the MCP Gateway serve no/metrics; their decisions show up in the central server's metrics and audit log. - Response headers on
/v1/check:X-Vanzik-Policy-Ms,X-Vanzik-Audit-Ms,X-Vanzik-Total-Ms(timings in ms, 3 decimals). - Version skew: all three binaries report their version via
vanzik version/vanzik-mcp-gateway --version/vanzik-llm-proxy --version. Mismatch is the most common ops-time bug after a partial upgrade. - Open an issue with: versions of all binaries, sanitized policy snippet, and the relevant log lines from both the proxy and the central server. Security-sensitive reports go to
hello@lictorate.com— not the issue tracker.