Why is my tool denied?
Supported modes: Hosted Hybrid Local Available in: Free Solo Teams
Your agent ran fine with a local policy, you attached a hosted policy that "only blocks deletes," and now Control Zero is blocking Bash, Read, everything. Nothing is broken — this is the allow-list default doing its job. This page explains why, and gives you the one-line fix.
The fail-closed allow-list behaviour (hosted policy with rules → deny on no-match) is live on current SDKs. Observe mode for an empty hosted policy and the default_on_empty knob ship with the policy-posture refinement (#1247 / #1252); see Enforcement Behavior → Compatibility.
The short version
A hosted policy that has rules denies anything those rules do not allow. It is an allow-list: only what you explicitly allow gets through, and everything else hits default_action, which is deny by default. A "block deletes" policy with a single deny rule allows nothing else — because nothing else is on the allow-list.
This is different from the local starter templates, which ship a catch-all allow: '*' and are therefore allow-by-default. Moving from local to hosted flips the default, which is exactly when this surprises people.
Local default vs. hosted default
This is the single most important distinction to internalise.
| Posture | Unmatched tool call | Why |
|---|---|---|
Local claude-code starter template | Allowed | The template ends with a catch-all allow: '*' rule — allow-by-default, observe-only. |
| Hosted policy with rules, no catch-all | Denied | default_action: deny — a fail-closed allow-list. |
| Hosted policy with no rules at all | Allowed + logged | Empty project runs in observe mode (default_on_empty: observe). |
They differ on purpose. A local template should let a developer start in seconds and watch what their agent does (observe-only). A hosted allow-list should fail closed so that adding "allow database reads" does not silently permit rm -rf. The trap is assuming a hosted policy behaves like the local template you started with — it does not.
On a hosted no-match deny, the SDK returns a self-explaining reason that names the blocked tool and spells out your options. For a blocked Bash:rm, it reads roughly:
No matching policy rule for
Bash:rm— this tool is neither allowed nor denied by any rule in your hosted policy, so the fail-closeddefault_action(deny) blocks it. Add a rule that allows it (a catch-allallow: '*', orallow: 'Bash:rm'), or set the project/org default action toallow, in the Control Zero dashboard.
If you see reason_code = NO_RULE_MATCH, you are in exactly this situation — the policy loaded fine and has rules, but none of them match this tool.
Fix 1 — add a catch-all allow: '*' (keep observing)
If you want the policy to only block the specific things you named and allow everything else (the behaviour the local template gives you), add a catch-all allow rule as the last rule. First-match-wins means your deny rules above it still fire; anything they do not catch falls through to the catch-all instead of to the default deny.
version: '1'
rules:
- deny: 'delete_*'
reason: 'No deletes.'
# Catch-all goes LAST: deny rules above still win.
- allow: '*'
reason: 'Allow everything else (observe-style).'
In the dashboard, add a final rule with action * and effect Allow.
Fix 2 — set default_action: allow (no catch-all rule needed)
Equivalent to Fix 1 but expressed as a setting instead of a rule. Flip the policy's default_action from deny to allow, and any call that no rule matches is allowed rather than blocked:
version: '1'
settings:
default_action: allow
rules:
- deny: 'delete_*'
reason: 'No deletes.'
In the dashboard this is the "Default action for unmatched calls" setting on Project (or Org) Settings. Use warn instead of allow for a soft rollout — unmatched calls are allowed but flagged — before you commit to a strict allow-list.
Fix 3 — author the allow-list properly (recommended for production)
If you actually want a fail-closed allow-list (the secure default), the fix is not to loosen the default — it is to add the tools your agent legitimately needs as explicit allow rules. Run the agent in observe mode first, read the audit log to see which tools it really calls, and turn those into allow rules. Then leave default_action: deny so anything new is denied until you vet it.
version: '1'
settings:
default_action: deny # fail-closed allow-list
rules:
- allow: 'Read'
- allow: 'database:SELECT'
- deny: 'database:DROP'
reason: 'No destructive SQL.'
How to choose
- Just exploring / want the local behaviour? Fix 1 or Fix 2 (
allow/ catch-all). Nothing is blocked except what you named. - Soft rollout? Fix 2 with
warn— see the blocks you would make without making them yet. - Production guardrail? Fix 3 — a real allow-list with
default_action: deny. Start in observe mode, learn the tool surface, then enforce.
Related
- Enforcement Behavior -- the four knobs, observe mode, and the decision codes in full.
- Troubleshooting: deny on every call -- when a policy should allow but every call still denies (stale bundle, resource-gate skip, vocabulary mismatch).
- Policies -- authoring rules, wildcards, and the catch-all.