Troubleshooting: deny on every call
You attached a policy that says "allow" but every guard() call still returns deny. This page walks through the three most common causes and the fix for each.
If you are debugging your own integration more broadly (logs, decision codes, bundle on disk), see Debugging your integration.
Scenario A: dashboard shows v2 published, SDK still denies
You edited a policy in the dashboard, clicked Publish, and the dashboard shows version 2 active. Your SDK is still returning the version 1 (or default) behavior.
Check 1: SDK version
Hosted-mode SDKs older than 1.4.6 (Python) had a resource-matching bug where rules with resources: ["*"] were silently skipped on calls that did not pass context.resource. Result: every call returned deny with reason_code=NO_RULE_MATCH.
Upgrade to the latest SDK:
# Python
pip install -U controlzero
# Node
npm install @controlzero/sdk@latest
# Go
go get -u controlzero.ai/sdk/go
After upgrade, restart your agent process so the new client is loaded.
Check 2: bundle on disk
The SDK caches the signed policy bundle under ~/.controlzero/cache/ so it can run offline. The cache directory contains:
bootstrap-<prefix>.json(project + key metadata)bundle-<prefix>.bin(the signed bundle)bundle-<prefix>.meta(ETag for conditional fetches)
The bundle is encrypted and signed; cat-ing the .bin file shows nothing useful. To inspect what the SDK actually loaded, use the built-in debug command:
controlzero debug bundle
This decrypts the cached bundle in-memory using the project keys from the matching bootstrap-<prefix>.json and prints every policy and rule in human-readable form. Run it with --simulate to dry-run a guard() call against the same evaluator the SDK uses:
controlzero debug bundle --simulate "database read sql=SELECT id FROM orders"
The output shows DECISION, reason_code, the matched rule's reason, and the count of rules evaluated. The output is safe to paste into a support thread: it never includes the encryption key, signing public key, or your API key.
If the cache is out of date and the SDK cannot reach the backend, your client will keep enforcing the older bundle. Force a refresh:
# Python
cz.refresh() # returns True if the bundle changed
// Node
await cz.refreshPolicies();
// Go
err := cz.RefreshPolicies(ctx)
If refresh() returns False even after a known dashboard change, confirm the SDK can reach the backend (check outbound HTTPS to api.controlzero.ai).
Check 3: detach and re-attach the policy
If the policy version on disk is correct but the rules still are not firing, detach the policy from the project in the dashboard, save, then re-attach. This forces a fresh bundle build on the server.
Scenario B: policy uses legacy action names
You have a policy like:
rules:
- allow: 'database:query'
reason: 'Reads are permitted'
and the SDK returns deny on cz.guard("database", method="query", ...).
What to check
Both legacy (database:query, database:execute, database:delete) and canonical (database:read, database:write, database:admin) action names match the same calls. There is no need to migrate. If the rule is not firing, the action name itself is not the problem; check the reason code on the deny decision.
To confirm what action your call actually emits, look at the audit log row for the call. The Tool / Method column shows the per-call action. The SDK also evaluates a parallel canonical-class action so a single allow: database:read rule covers SELECT, EXPLAIN, SHOW, DESCRIBE, and CTE in any dialect.
Scenario C: audit log shows Decision=Deny, Policy column has a synthetic:* chip
The audit row says decision=deny and the Policy column renders a coloured chip whose label starts with synthetic:. This is a synthetic deny: no user-authored rule fired. The chip identifies which fail-closed class triggered the deny so you can jump straight to the right fix.
Each chip is a hyperlink to the matching anchor below.
Per-sentinel decision table
| Chip | What happened | Where to look | Most common fix |
|---|---|---|---|
synthetic:NO_RULE_MATCH | Bundle loaded cleanly, but no rule's actions matched the call. default_action took effect. | Audit row tool + method columns -- compare to the rule's actions patterns. | Add a rule that covers the call, or set default_action: allow for the project. |
synthetic:NO_ACTIVE_POLICIES | Bundle is structurally empty: zero policies attached to the project. | Dashboard Library tab -- confirm at least one policy is attached AND active. | Attach a policy from the Library page; verify the attachment is ACTIVE not DRAFT. |
synthetic:BUNDLE_MISSING | Client is enrolled but no bundle could be loaded (never synced, network error, bad cache). | Network reachability to api.controlzero.ai; bundle file under ~/.controlzero/cache/. | Re-run cz.refresh(); rotate the API key if it was revoked; clear the cache. |
synthetic:RESOURCE_GATE_SKIP | A rule's actions matched but its resources: list excluded the call. T83-class signature. | The rule's resources: list vs the call's context.resource. | Use resources: ["*"] for unscoped allows; pass context: { resource: "..." } to guard. |
synthetic:QUARANTINE | Machine is in local quarantine state due to tamper detection. Every call denied until recovery. | ~/.controlzero/quarantine.json; cz tamper-status CLI. | Re-enroll the machine: controlzero enroll; or controlzero policy-pull to recover. |
synthetic:ENGINE_UNAVAILABLE | Policy engine could not evaluate (compiled binary missing, evaluator panic, gateway misconfiguration). | Gateway logs; cz doctor CLI. | Restart the gateway; verify the compiled engine binary is present in the install. |
The same chip is rendered for both SDK-emitted and Gateway-emitted denies. Both surfaces stamp policy_id with the same synthetic:* value for the same condition, so a deny that fires on the SDK side and a deny that fires inside the gateway look identical in the dashboard.
no_rule_match
The bundle loaded cleanly, but no rule's actions and resources matched the call. The default_action knob took effect (default: deny).
Fix: add an explicit rule that covers the call, or change default_action to allow if you want everything-not-explicitly-denied to pass. See Enforcement Behavior for the trade-offs.
no_active_policies
The bundle is structurally empty: zero policies are attached to the project. A synthetic rule fires whose decision is the project's default_action.
Fix: attach at least one policy to the project from the dashboard's Library page.
bundle_missing
The client is enrolled but no bundle could be loaded (never synced, network error, bad cache). The default_on_missing knob took effect.
Fix: confirm the API key is valid (cz_live_* or cz_test_*), confirm outbound HTTPS to api.controlzero.ai is reachable, and re-run cz.refresh().
resource_gate_skip
A rule whose actions: matched the call was skipped purely because its resources: list excluded the call. This is the same family as the T83 incident: every dashboard policy emits resources: ["*"] for unscoped rules, and pre-1.4.6 SDKs silently required the caller to supply a context.resource even when the rule's resource pattern was the universal wildcard.
Fix: upgrade the SDK (pip install -U controlzero / npm install @controlzero/sdk@latest / go get -u controlzero.ai/sdk/go). If you are on the latest SDK and still see this chip, the rule's resources: is genuinely scoped (e.g. ["/safe/*"]) and your call did not pass a matching context.resource. Either pass the resource explicitly to cz.guard(...) or broaden the rule's resources.
quarantine
The machine is in local quarantine state. The SDK marks the machine as quarantined when bundle integrity verification fails or default_on_tamper is set to deny-all / quarantine. Every tool call is denied until recovery.
Fix: run controlzero enroll to re-enroll the machine, or controlzero policy-pull to fetch a fresh signed bundle. The quarantine lockfile lives at ~/.controlzero/quarantine.json.
engine_unavailable
The policy engine could not evaluate the call. Most commonly:
- The gateway is running without the compiled engine binary (e.g. a Docker image built without the build toolchain).
- The compiled engine raised an exception during evaluation (corrupt rules, FFI binding mismatch).
- An SDK evaluator panicked.
Fix: check the gateway / SDK process logs for the underlying exception. Restart the gateway. If the issue persists, verify the compiled engine binary is present in the install (pip show controlzero should reveal the _engine extension).
Still stuck?
Open a support ticket and include:
- The SDK language and version (e.g.,
python 1.4.6). - The policy YAML or a screenshot of the dashboard rule that should match.
- One audit row from the call that returned
deny, including thereason_codeandtool/methodcolumns. - A minimal repro snippet (the
guard()call shape, with arguments redacted).
See Debugging your integration for a complete diagnostic checklist.