Skip to main content

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 action
    • deny: "<action-pattern>" — deny this action
  • Explicit form — for rules that need resource or conditions:
    • effect: allow | deny | warn | audit
    • action: "<action-pattern>" (or actions: [...])
    • 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 the github tool
  • "*:delete" — the delete method on any tool
  • "delete_*" — any method on any tool whose name starts with delete_ (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

FeatureLocal-onlyHosted
Policy evaluationyesyes
Conditions on context / args / tagsyesyes
Fail-closed defaultyesyes
DLP scanningyesyes
Local rotated audit logyesno
Signed + encrypted policy bundlesnoyes
Dashboard and audit viewernoyes
Managed secrets vaultnoyes
Policy sync from servernoyes
Tamper-trigger quarantinenoyes

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.