Skip to main content

Set up observation-only governance (watch, don't block)

Surfaces used: any surface (SDK, Gateway, coding hooks) Modes supported: Hosted Hybrid Local Tiers: Free Solo Teams

What you'll do

Stand up Control Zero so it watches and records every governed AI and tool call -- and blocks nothing. Every decision lands in the audit log so you can see exactly what your agents do in production, what a stricter policy would have blocked, and where your real risk is, before you ever turn enforcement on.

This is the safest way to introduce governance to a team that has never had it: nobody's workflow breaks on day one, and you collect the evidence you need to write a policy that fits your actual traffic.

Why this is the right path for you

  • You want visibility first. "Show me what's happening" beats "block things and find out what broke."
  • You are rolling out to other people's agents or laptops and cannot afford a false-positive block on day one.
  • You need an audit trail for a review, an incident, or a compliance conversation, but you are not ready to commit to a blocking posture.
  • You want to author a real allow-list, but you do not yet know which tools, models, and resources your agents actually touch.

This mirrors how mature security tooling onboards: detection mode first, enforcement second. You watch real traffic, tune against it, and only then flip the switch.

When NOT to use this approach

caution

Observation-only does not block anything, by design. If you already have a known-bad action you must stop today (for example, "agents must never run DROP TABLE in prod"), do not start in observation-only -- write a targeted deny rule for that one action and keep everything else permissive. See Dev warns, prod denies for the mixed posture.

How observation-only works

Control Zero evaluates every call against your policy and records the decision. What happens after a decision is controlled by one bundle-level setting, default_action, which decides the outcome when no rule explicitly matches a call:

default_actionPostureEffect on a call that no rule allows
allowObserveThe call proceeds. The decision is still logged. Nothing breaks.
warnSoftThe call proceeds, but the decision is flagged so you can see what a stricter policy would block.
denyEnforceThe call is blocked. This is the secure-by-default posture.

For a true watch-everything rollout you set default_action: allow. Every call is permitted and every decision is written to the audit trail. This is what the platform calls an audit-only rollout. When you are ready to tighten, you move the same policy to warn (a soft rollout that surfaces would-be blocks) and finally to deny.

5-minute setup

Hosted (dashboard-managed)

  1. Sign up at app.controlzero.ai and create a project (see the Quickstart).
  2. In the dashboard, open Project Settings and set the project's enforcement default to Allow (audit-only). This is the project-level equivalent of default_action: allow.
  3. Attach any policy you like -- even an empty starter -- and integrate a surface (Gateway, SDK, or coding hooks).
  4. Run your normal workload. Watch the Audit Log fill up.

Nothing is blocked while the default is allow. You are now collecting the data you need to write a real policy.

Local / Hybrid (policy file in your repo)

Drop this controlzero.yaml into your project. It logs everything and blocks nothing:

version: '1'
settings:
# Observation-only: anything no rule matches is allowed and logged.
default_action: allow
default_on_missing: allow
default_on_tamper: warn
rules:
# A single permissive rule keeps the bundle non-empty and makes the
# observe-everything intent explicit. Every call is still audited.
- id: observe-all
allow: '*'
reason: 'Observation-only: allow and audit every action.'

Wire it into the SDK with no other changes to your agent:

from controlzero import Client

# Local mode: policy on disk, audit to a local file. No blocking while
# default_action is "allow"; every guard() decision is still recorded.
cz = Client(policy_file="./controlzero.yaml")

# Your agent calls guard() before each tool call, exactly as it would
# under enforcement. In observation-only the decision is "allow" and the
# call proceeds -- but it lands in the audit log either way.
result = cz.guard("database", method="SELECT", args={"sql": "SELECT id FROM orders"})
print(result.decision) # "allow"

cz.close()

To govern AI coding assistants in observation-only, install the hook and use the same permissive policy. The hook is wired in one command:

controlzero install claude-code

The installer writes a starter policy you then relax to default_action: allow for the watch-only phase. The assistant keeps working exactly as before; every tool call it makes is recorded.

Verifying it's working

  1. Hosted: open your project's Audit Log. You should see a row per governed call with a decision of allow and the tool, method, and timestamp.

  2. Local: the SDK writes a rotating audit file (default ./controlzero.log). Follow it live:

    controlzero tail --log ./controlzero.log
  3. Confirm that calls you expect a stricter policy to block (a write, a delete, an unapproved model) are still allowed and logged. That is the whole point: you can now see them without having stopped anything.

Graduating to enforcement

Observation-only is a starting line, not a destination. The recommended path, the same one used by detection-mode security tooling, is three steps:

  1. Observe. Run with default_action: allow for one to two weeks. Let the audit log show you what your agents actually do.

  2. Soft rollout. Author deny rules for the actions you want to stop, but keep default_action: warn. Now the audit log flags every call your new rules would block, without blocking it. Review the warnings; fix false positives by widening your allow rules.

    version: '1'
    settings:
    # Soft rollout: would-be blocks are flagged, not enforced yet.
    default_action: warn
    default_on_missing: deny
    default_on_tamper: warn
    rules:
    - id: allow-reads
    allow: 'database:read'
    reason: 'Reads are fine.'
    - id: block-writes
    deny: 'database:write'
    reason: 'No writes from this agent (soft: surfaced as a warning for now).'
  3. Enforce. When the warnings look correct and the false-positive rate is acceptable, flip default_action to deny. Your allow rules become the complete list of permitted operations; everything else is blocked.

    version: '1'
    settings:
    # Enforce: only explicitly allowed actions proceed.
    default_action: deny
    default_on_missing: deny
    default_on_tamper: quarantine
    rules:
    - id: allow-reads
    allow: 'database:read'
    reason: 'Reads are the only permitted database operation.'
    - id: block-writes
    deny: 'database:write'
    reason: 'Writes are blocked.'

Because the policy file is the same across all three phases -- only default_action changes -- you can promote it through dev, staging, and prod without rewriting a single rule.

Common follow-ups

Reference