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
# scenarios.json is not hosted -- build it from the expected-decision table above.

# 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.
# Run each documented posture with the corresponding tool, method, and JSON arguments:
controlzero test <tool> --method <method> --policy policy.yaml --args '<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 human confirmation, see First approval flow and Approval for destructive actions.
  • 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.