Skip to main content

Recipe: Dev warns, prod denies

The problem

On a dev laptop the engineer is iterating, and a hard block on every warning kills flow. In production, the same action is a stop-the-world event. You do not want to maintain two sets of rules -- just one rule set with two different enforcement postures.

The policy

version: '1'
# Same policy rules for dev and prod. The enforcement behavior is
# driven entirely by the `settings` block at the org / project level.
#
# - dev project: settings.default_action = warn (log only, nothing
# is actually blocked)
# - prod project: settings.default_action = deny (hard block)
#
# The policy rules below identify "dangerous writes" the same way in
# both environments; only the settings differ.
rules:
- id: dangerous-db-deny
deny: 'database:DROP'
reason: 'Drops are not allowed in any environment.'
- id: dangerous-db-deny-truncate
deny: 'database:TRUNCATE'
reason: 'Truncates are not allowed in any environment.'
- id: dangerous-shell-deny
deny: 'Bash:rm'
reason: 'rm is not allowed in any environment.'

In the dashboard, attach this policy to both your dev project and your prod project. On the dev project, set the project-level default_action to warn. On the prod project, set it to deny (the org-wide default).

Why it works

The signed policy bundle carries the three enforcement knobs (default_action, default_on_missing, default_on_tamper) alongside the rule list. When the SDK or Gateway evaluates a call, an explicit deny rule always wins -- that is why DROP and rm are blocked everywhere. Anything NOT matched by a rule falls through to default_action, which is warn in dev and deny in prod. Same rule file, different posture.

What gets blocked

EnvironmentAgent callExtracted actionDecisionreason_code
prodDROP TABLE usersdatabase:DROPdenyRULE_MATCH
devDROP TABLE usersdatabase:DROPdenyRULE_MATCH
prodrm -rf buildBash:rmdenyRULE_MATCH
prodSELECT * FROM users (no rule matches)database:SELECTdenyNO_RULE_MATCH
prodcurl example.com (no rule matches)Bash:curldenyNO_RULE_MATCH

What gets allowed (with a warn log)

EnvironmentAgent callExtracted actionDecisionreason_code
devSELECT * FROM users (no rule matches)database:SELECTwarnNO_RULE_MATCH
devcurl example.com (no rule matches)Bash:curlwarnNO_RULE_MATCH

Note: in dev, a hand-written deny rule still fires -- warn only applies to the "no rule matched" path. This is exactly what you want: DROP is a bright-line never-do, regardless of environment.

Test it yourself

curl -O https://docs.controlzero.ai/recipes/dev-vs-prod-enforcement/policy.yaml
curl -O https://docs.controlzero.ai/recipes/dev-vs-prod-enforcement/scenarios.json

# The scenarios file uses `settings_override` on each case so one YAML
# can test both dev (warn) and prod (deny) postures in a single run.
controlzero test-policy policy.yaml --scenarios scenarios.json

Read Enforcement Behavior for the full override precedence (organization -> project -> user YAML) and for the complete list of reason_code values.

Caveats

  • warn lets the call through and logs. It does NOT add a prompt back to the agent asking for confirmation. For an approval workflow, wait for the approvals recipe (Phase 4) or wire your own approval service on top of the warn event.
  • The three-level override (org -> project -> user YAML) is resolved server-side when the bundle is built. If a user ships their own controlzero.yaml with default_action: allow, the bundle from the backend still wins when the SDK is in hosted mode. The user YAML only overrides in local-only mode.
  • A warn outcome still counts toward your monthly evaluations on metered plans. Audit-mode rollouts are cheaper on Teams than on Free.