Enforcement Behavior
Supported modes: Hosted Hybrid Local Available in: Free Solo Teams
Every Control Zero surface (SDKs, Gateway, coding-agent hooks, browser extension) evaluates your policies the same way, using the same three knobs and the same machine-readable decision codes. This page documents the contract so a policy you author once behaves identically everywhere.
The three knobs
A policy bundle carries three top-level settings that govern what happens outside the happy path of a rule match. All three ship inside the signed, encrypted bundle, so a tampered file cannot flip an organization from deny to allow.
| Knob | Values | Fires when | Default |
|---|---|---|---|
default_action | deny | allow | warn | Bundle loaded, zero rules matched the call. | deny |
default_on_missing | deny | allow | Client is enrolled but no bundle can be loaded (never synced, network error, bad key). | deny |
default_on_tamper | warn | deny | deny-all | quarantine | Bundle verification fails, or machine is in local quarantine. | warn |
When to reach for each
default_action is the knob that answers the question, "I attached a policy that blocks deletes. What happens to writes?"
- Keep
deny(the default) when you want the policy you authored to be the complete list of allowed operations -- a true allow-list. - Set
allowfor audit-only rollouts or for policies that only block a specific operation class (e.g. "block deletes; everything else is fine"). This is the fix for the classic "I attached db-read-only, why is my AI blocked from everything?" scenario. - Set
warnduring a soft rollout to see what the blocks would be before flipping todeny.
default_on_missing exists separately from default_action because "I have no rule that matches this call" is a different question from "I have no bundle at all."
- Keep
denyto fail closed if a machine cannot pull its bundle. - Set
allowto avoid bricking the agent during an outage. Coding-agent hooks default to this today.
default_on_tamper picks the severity of the response when the bundle signature does not verify or a local state file was altered.
warnjust logs.denydenies the one call that triggered the check.deny-allandquarantineare aliases: the machine enters a quarantine state and denies every tool call until recovery (re-enrollment or a fresh policy pull).
Override precedence
The three knobs can be set at three levels. Lower levels override higher levels:
- Organization -- the default for every project in the org, set from the dashboard.
- Project -- overrides the org default for a specific project.
- User YAML -- in local/unenrolled mode only, a
controlzero.yamlwith asettings:block overrides the project default.
In hosted mode the server resolves org -> project -> canonical default at bundle build time, so the SDK only sees the single resolved triple. In local mode the SDK reads settings: from the YAML directly.
Example user YAML:
version: '1'
settings:
default_action: allow
default_on_missing: deny
default_on_tamper: quarantine
rules:
- deny: 'delete_*'
reason: 'No deletes in staging.'
The eight decision codes
Every decision carries a reason_code alongside the free-text reason. Automation should branch on reason_code. The free-text reason can be re-worded or translated without breaking downstream consumers.
reason_code | Meaning | Typical decision |
|---|---|---|
RULE_MATCH | A user-authored rule fired and its effect is the decision. | Whatever the rule's effect is. |
NO_RULE_MATCH | Bundle loaded cleanly, no rule matched. Decision follows default_action. | Follows default_action. |
NO_ACTIVE_POLICIES | Bundle is structurally empty (zero attached policies). Synthetic rule fires. | Follows default_action. |
BUNDLE_MISSING | Client is enrolled but the bundle cannot be loaded. Decision follows default_on_missing. | Follows default_on_missing. |
BUNDLE_TAMPERED | Bundle verification failed. Decision follows default_on_tamper. | deny under deny / deny-all. |
MACHINE_QUARANTINED | Machine is in the local quarantine state. Every tool call is denied until recovery. | Always deny. |
NETWORK_ERROR | Backend is unreachable and no cached bundle is available. Follows default_on_missing. | Follows default_on_missing. |
DLP_BLOCKED | A DLP rule with action: block matched the tool arguments and overrode a would-be allow. | Always deny. |
Which surface emits which
Not every surface can emit every code. The table below lists which codes a given surface is expected to produce.
| Surface | RULE_MATCH | NO_RULE_MATCH | NO_ACTIVE_POLICIES | BUNDLE_MISSING | BUNDLE_TAMPERED | MACHINE_QUARANTINED | NETWORK_ERROR | DLP_BLOCKED |
|---|---|---|---|---|---|---|---|---|
Python SDK guard() | Y | Y | Y | Y | Y | Y | Y | Y |
Node SDK guard() | Y | Y | Y | Y | Y | Y | Y | Y |
Go SDK Guard() | Y | Y | Y | Y | Y | Y | Y | Y |
| Compiled policy engine | Y | Y | Y | - | - | - | - | - |
| Gateway request guard | Y | Y | Y | Y | Y | - | Y | - |
| Coding-agent hook-check | Y | Y | Y | Y | - | Y | - | - |
| Browser extension | Y | Y | - | Y | - | - | - | Y |
- Server-side surfaces (Gateway, compiled engine) have no per-machine state, so they do not emit
MACHINE_QUARANTINED. - The compiled engine receives only rules + settings; it has no bundle-loading concept, so
BUNDLE_MISSING/BUNDLE_TAMPERED/NETWORK_ERRORare not reachable there. - Browser extension DLP is a separate decision surface (regex against text content), so its matches emit
DLP_BLOCKEDbut not the other DLP-adjacent codes.
Checking the effective values
In hosted mode you see the resolved knobs on the dashboard's Project Settings page.
In local mode, or to double-check what a machine is actually enforcing, SDK callers can introspect the settings at runtime.
Python:
import controlzero
cz = controlzero.Client()
settings = cz.policy_settings
print(settings.default_action, settings.default_on_missing, settings.default_on_tamper)
Node:
import { Client } from '@controlzero/sdk';
const cz = new Client();
const s = cz.policySettings;
console.log(s.default_action, s.default_on_missing, s.default_on_tamper);
Go:
c, _ := controlzero.New()
s := c.PolicySettings()
fmt.Println(s.DefaultAction, s.DefaultOnMissing, s.DefaultOnTamper)
The controlzero status CLI (shipped with the Python SDK) also prints the effective values under the "Enforcement defaults" section. Run it from any project directory to see which triple your SDK will enforce on the next guard() call.
$ controlzero status
...
Enforcement defaults
default_action: allow (from project)
default_on_missing: deny (from org)
default_on_tamper: quarantine (from user YAML)
Compatibility
Older SDK versions that do not understand the three knobs fall back to deny / deny / warn, which matches the hard-coded pre-Phase-2 behaviour. Upgrading is non-breaking in both directions: a new bundle is safe for old SDKs, and a new SDK is safe against an old bundle.
Related
- Policies -- authoring rules.
- Feature Availability -- which surfaces and tiers support which knobs.