Local-Only Mode
Supported modes: Local Available in: Free Solo Teams
This entire page describes Local mode: no API key, no network calls, no Control Zero account required.
Local-only mode lets you use the Control Zero Python SDK without an API key, an account, or any connection to the Control Zero backend. Policies are loaded from a YAML or JSON file on disk, evaluation happens entirely in-process, and audit logs are written to a local file (with rotation).
This is useful for:
- Evaluating the SDK before signing up
- Air-gapped or offline environments
- CI/CD pipelines where you want policy enforcement without network calls
- Local development and testing
Quick start
from controlzero import Client
cz = Client(policy_file="./controlzero.yaml")
# Evaluate a policy. Returns a decision with effect "allow" or "deny"
decision = cz.guard(
"llm",
method="generate",
context={"resource": "model/gpt-5.4"},
)
No api_key parameter, no CONTROLZERO_API_KEY environment variable, no
network calls. The SDK detects that policy_file is set and api_key is
absent and enters local-only mode automatically.
Policy file format
The policy file is a YAML or JSON document with a rules array. Each rule
has one of the following shapes:
- Shorthand allow/deny — the key name specifies the effect:
allow: "<action-pattern>"— allow this actiondeny: "<action-pattern>"— deny this action
- Explicit form — for rules that need
resourceorconditions:effect: allow | deny | warn | auditaction: "<action-pattern>"(oractions: [...])resource: "<resource-pattern>"(optional;resources: [...]also accepted)conditions: { key: glob_pattern, ... }(optional)reason: "..."(optional, shown in audit logs and deny errors)id: "..."/name: "..."(optional)
Actions follow the canonical tool:method format. A pattern without a colon
(e.g. "delete_*") is treated as "delete_*:*" — matches any method on
matching tools.
Example controlzero.yaml
version: '1'
rules:
- allow: 'llm:generate'
reason: 'LLM calls are permitted'
- deny: 'filesystem:write_file'
reason: 'No file writes in this agent'
- effect: allow
action: 'database:query'
resource: 'table/orders'
conditions:
agent_id: 'research-*'
reason: 'Research agents can query the orders table'
- effect: deny
action: '*'
reason: 'Default deny'
Example controlzero.json
{
"version": "1",
"rules": [
{ "allow": "llm:generate", "reason": "LLM calls are permitted" },
{ "deny": "filesystem:write_file", "reason": "No file writes in this agent" },
{
"effect": "allow",
"action": "database:query",
"resource": "table/orders",
"conditions": { "agent_id": "research-*" }
}
]
}
Action patterns
Actions follow the tool:method format. Wildcards are supported:
"github:list_issues"— exact match"github:*"— any method on thegithubtool"*:delete"— thedeletemethod on any tool"delete_*"— any method on any tool whose name starts withdelete_(equivalent to"delete_*:*")"*"— matches everything
Resource patterns
resource is optional. When present, the guard call's resource (passed via
context={"resource": "..."}) must match. Wildcards are supported the same
way as actions.
To pass a resource from your code:
decision = cz.guard(
"llm",
method="generate",
context={"resource": "model/gpt-5.4"},
)
Conditions
conditions is an optional map of key: glob_pattern. Each condition is
evaluated against a merged view of the guard call's context + args
(with context winning on collisions, and nested context["tags"]
flattened into the top level). All conditions must match (AND logic).
- effect: allow
action: 'llm:generate'
conditions:
agent_id: 'support-*'
environment: 'production'
cz.guard(
"llm",
method="generate",
context={
"agent_id": "support-agent-1", # matches "support-*"
"environment": "production", # matches "production"
},
)
See Policies → Conditions for the full semantics.
Audit logging
In local-only mode, audit log entries are written to a local file (default
./controlzero.log) in JSON lines format. To point it elsewhere or change
the format, pass log_path and log_format:
cz = Client(
policy_file="./controlzero.yaml",
log_path="/var/log/controlzero/audit.log",
log_format="json", # or "text"
)
Each line has the shape:
{
"timestamp": "2026-04-15T12:00:00Z",
"tool": "llm",
"method": "generate",
"decision": "allow",
"policy_id": "...",
"reason": "...",
"args_keys": ["model"],
"mode": "local"
}
Args values are not logged by default — only the keys. Log rotation is configurable via the SDK's rotation options.
Reloading policies
The SDK does not auto-reload policy files. To pick up edits at runtime,
construct a new Client:
cz = Client(policy_file="./controlzero.yaml")
What works and what does not
| Feature | Local-only | Hosted |
|---|---|---|
| Policy evaluation | yes | yes |
| Conditions on context / args / tags | yes | yes |
| Fail-closed default | yes | yes |
| DLP scanning | yes | yes |
| Local rotated audit log | yes | no |
| Signed + encrypted policy bundles | no | yes |
| Dashboard and audit viewer | no | yes |
| Managed secrets vault | no | yes |
| Policy sync from server | no | yes |
| Tamper-trigger quarantine | no | yes |
Migrating from local-only to hosted
When you are ready to connect to the Control Zero backend, replace the
policy_file argument with an api_key:
# Before (local-only)
cz = Client(policy_file="./controlzero.yaml")
# After (hosted)
cz = Client(api_key="cz_live_...")
Your policies are then managed in the Control Zero dashboard and pulled on startup as a signed bundle. The local policy file is no longer needed.