Error handling
How the Checkrd SDK signals a denied request, and the three ways to handle it without crashing your agent.
Error handling
When a policy denies a request, the Checkrd SDK raises an exception (Python) or throws (JavaScript) from inside the HTTP transport, before the bytes leave your machine. That's a fail-closed contract by design: if your code forgets to handle the deny, you don't silently send a request you said you'd block.
The trade-off is that a 1-line install plus an active enforce policy can crash the agent process on the first deny unless your code is prepared. This page explains what's raised, how to catch it, and the three ways to soften the contract when you want to.
What gets raised
checkrd.exceptions.CheckrdPolicyDenied: Request <uuid> denied: denied by rule 'block-mutations'
Request: https://hooks.zapier.com/hooks/catch/123/abc
Fix: Blocked by rule 'block-mutations'. Edit the rule in your policy file or dashboard to allow this request.
Dashboard: https://app.checkrd.io/agents/<agent-id>/events/<event-id>
Docs: https://checkrd.io/errors/policy_deniedThe exception carries every piece of context a caller might want:
| Attribute | Meaning |
|---|---|
rule_name | Which rule fired. None if it was the default-deny branch. |
reason | Human-readable deny reason from the WASM engine. |
request_id | UUID; correlates with the dashboard event and your telemetry. |
url | The request URL that was denied. |
dashboard_url | Deep link to the event in the Checkrd dashboard. |
suggestion | Actionable remediation hint. |
code | One of policy_denied, rate_limit_exceeded, default_policy_denied, kill_switch_active. |
JavaScript: same shape, exception class CheckrdPolicyDenied from checkrd.
Three ways to handle denies
Pick the one that matches your agent's existing error model.
1. Catch and continue (recommended)
The most idiomatic pattern. Treat a deny like any other transport-level exception: log it, optionally fall back, and move on. The agent's code stays in control of what happens next.
from checkrd.exceptions import CheckrdPolicyDenied
try:
r = http.post("https://api.example.com/data", json=payload)
except CheckrdPolicyDenied as exc:
log.warning(
"checkrd denied request",
rule=exc.rule_name,
request_id=exc.request_id,
dashboard=exc.dashboard_url,
)
# Decide: skip, return a default, raise to caller, etc.import { Checkrd, CheckrdPolicyDenied } from "checkrd";
const checkrd = new Checkrd({
agentId: process.env.CHECKRD_AGENT_ID,
apiKey: process.env.CHECKRD_API_KEY,
});
const fetch = checkrd.wrap(globalThis.fetch);
const payload = { task: "summarize", contents: "..." };
try {
const r = await fetch("https://api.example.com/data", {
method: "POST",
body: JSON.stringify(payload),
});
} catch (err) {
if (err instanceof CheckrdPolicyDenied) {
console.warn("checkrd denied request", {
rule: err.ruleName,
requestId: err.requestId,
dashboard: err.dashboardUrl,
});
// Decide: skip, return a default, rethrow, etc.
} else {
throw err;
}
}2. Run the SDK in observe-only mode
If your goal is to wire Checkrd into an existing agent without changing any of its error-handling code, pass enforce=False at init time. The SDK still evaluates the policy and emits telemetry, the dashboard still shows what would have been denied, but the request always completes and no exception is raised.
from checkrd import Checkrd
client = Checkrd(
api_key="ck_live_...",
agent_id="...",
enforce=False, # ← shadow mode at the client level
)import { Checkrd } from "checkrd";
const client = new Checkrd({
apiKey: "ck_live_...",
agentId: "...",
enforce: false,
});Use this when you want all the observability without any of the blocking. It's the same posture you get from mode: dry_run in the policy YAML, but applied at the SDK layer instead of the policy layer.
3. The on_deny callback
For agents that want a single chokepoint for every deny (centralised logging, paging, custom fallback), register an on_deny hook. The callback receives the same context as the exception. If the hook returns normally the request is still blocked; if you want the request to proceed anyway, combine the hook with enforce=False.
def on_deny(event):
metrics.incr("checkrd_deny", tags=[f"rule:{event.rule_name}"])
pager.notify_if_critical(event)
client = Checkrd(
api_key="ck_live_...",
agent_id="...",
on_deny=on_deny,
)Recommended rollout: dry-run, then enforce
The right pattern for shipping a new policy is the same one Envoy and OPA use for ext-authz: deploy in shadow mode first, validate the decisions against real traffic in the Events table, then flip the toggle to enforce.
# Step 1: ship this. Decisions appear in the dashboard with a "dry-run"
# badge; no request is blocked. Validate for a few hours.
mode: dry_run
default: deny
rules:
- name: allow-reads
allow:
method: [GET, HEAD, OPTIONS]
url: "**"
- name: block-mutations
deny:
method: [POST, PUT, PATCH, DELETE]
url: "**"# Step 2: flip the mode and re-activate the policy. Same rules,
# now enforced. Re-publish from the dashboard or `checkrd policies
# publish` after the diff is what you expect.
mode: enforce
default: deny
rules:
- name: allow-reads
allow:
method: [GET, HEAD, OPTIONS]
url: "**"
- name: block-mutations
deny:
method: [POST, PUT, PATCH, DELETE]
url: "**"The dashboard's "Create policy" dialog and every built-in policy template scaffold the YAML with mode: dry_run for exactly this reason. Custom YAML you author by hand inherits the schema default of dry_run; set mode: enforce explicitly when you're ready to block.
When the SDK does NOT raise
The SDK only raises CheckrdPolicyDenied when all four of these are true:
- The policy on the agent has
mode: enforce. - The SDK was initialised with
enforce=True(orenforce="auto", the default, which honours the policy mode). - A rule actually matched and resolved to a deny decision.
- The deny applies to the active request (not a rate-limit shadow or a kill-switch-overridden allow).
A mode: dry_run policy, an enforce=False client, or a default-allow path all complete normally with no exception and no behavioural change to your agent code.
See also
- Quickstart — install + first policy
- Policy YAML reference — full schema and matcher operators
- API errors — error code reference (
policy_denied,rate_limit_exceeded,kill_switch_active)