Chio/Docs
LOGIN · JOIN

HushSpec policy patterns

A block-by-default policy you can test call by call, with the receipt naming the rule behind every refusal.

Guide chio@5f63e6e

On this page

Write a policy that admits only the tools and paths your agent needs, then prove each boundary with chio check before an agent runs under it.

HushSpec is one of the two policy formats chio loads: a file with a top-level hushspec key and a rules block. The starter format in Write a Policy uses kernel, guards, and capabilities sections instead, so keep the two in separate files.

Start

1. Start from a block-by-default document

Create policy.yaml with the version line, a name, and one block per guard under rules. Every check below runs against this file.

policy.yamlyaml
hushspec: "0.1.0"
name: my-policy
description: What this policy is for.

rules:
  # One entry per guard. Omitted guards are disabled.
  tool_access:
    enabled: true
    default: block
    allow:
      - read_file
  forbidden_paths:
    enabled: true
    patterns:
      - "**/.env"
      - "**/.ssh/**"
  # ... path_allowlist, egress, shell_commands, secret_patterns,
  # patch_integrity, velocity, etc.

HushSpec validates strictly with deny_unknown_fields. The top level accepts only hushspec, name, description, extends, merge_strategy, rules, extensions, and metadata, and hushspec is required. A rule block you omit is disabled and returns allow; a policy with no rules block is valid and restricts nothing.

For a team baseline, extends builds on a base policy and merge_strategy (replace, merge, or deep_merge) sets how the overlay folds in. Keep shared forbidden_paths and secret_patterns in the base and project rules in the overlay. Version policies and review changes like code; metadata carries governance fields such as author, approved_by, and lifecycle_state.

2. Choose the rule blocks your agent needs

rules accepts exactly fourteen keys, so a typo or an invented block name fails validation. Every block has an enabled field and compiles to one or more guards.

Rule blockCompiled guardWhat it checks
forbidden_pathsforbidden-pathFile patterns that are always refused
path_allowlistpath-allowlistReadable, writable, and patchable trees
egressegress-allowlist, internal-networkOutbound domains; private and metadata addresses
secret_patternssecret-leak, SanitizerHookSecrets in writes and in returned results
patch_integritypatch-integrityPatch size, content, and balance
shell_commandsshell-commandForbidden command patterns
tool_accessmcp-toolWhich tools, and argument size
computer_usecomputer-useDesktop-control actions
remote_desktop_channelsremote-desktop-side-channelClipboard, file transfer, audio, drive mapping
input_injectioninput-injection-capabilitySynthetic keyboard and mouse input
browser_automationbrowser-automationBrowser domains and verbs
code_executioncode-executionLanguages, modules, network, run time
velocityvelocity, agent-velocityRequest and planned-spend rates
human_in_loopRequireApprovalAbove constraintApproval before matched calls

Two guards arrive without a key of their own. Enabling egress also attaches internal-network, an SSRF companion that blocks RFC1918 and cloud-metadata endpoints, and no rules.internal_network key exists to configure it. Per-agent rate limiting, agent-velocity, derives from velocity and from origin budgets under extensions.origins.

Fields, defaults, and an example for each block are in Pattern policies and rule block fields.

3. Layer rules so every enabled guard must allow the call

Guards combine conjunctively: every enabled guard must allow a request, and one deny refuses the whole call. The capability token is a separate check, so a call succeeds only with a grant that covers it and a policy that allows it. Build up from the restrictive end:

  1. Tools. Set tool_access.default: block and allow only the tools the agent uses. Under default: allow, any new tool the MCP server exposes passes this guard unless another rule blocks it.
  2. Directories. Enable path_allowlist with the narrowest tree that covers the workflow.
  3. Secrets on disk. Add forbidden_paths for .env, .ssh, and .pem files inside that tree. A forbidden match refuses a path even when the allowlist admits it.
  4. Network. Allow only the domains the workflow calls; egress blocks every other domain.
  5. Depth. Add shell_commands, secret_patterns, patch_integrity, and velocity. A velocity limit stops a looping agent from exhausting resources or budgets, and secret-leak catches credentials a misconfigured upstream server exposes.

Guards are independent, so disabling one leaves the others unchanged. Start with tool_access and add guards as requirements clarify. When a call is refused in production, find out why: the allowlist may be too narrow, or it stopped a call nobody authorized.

Read more in Capabilities, which covers the token side of each call.

4. Check one call against the policy

chio check evaluates a single tool call against a policy without starting a server. Admission is durable, so the command records the operation in an admission database and the decision in a receipt database.

Terminalbash
chio --session-db <admission.db> --receipt-db <receipts.db> \
    check --policy ./policy.yaml \
    --tool <tool-name> \
    --params '<json-arguments>'

Run an allowed read first: a workspace file that tool_access permits and forbidden_paths does not match.

policy-checklist · allowtranscript
$ chio --session-db ./admission.db --receipt-db ./receipts.db \
  check --policy ./policy.yaml --tool read_file \
  --params '{"path": "./workspace/README.md"}'
verdict:    ALLOW
tool:       read_file
server:     *
receipt_id: 42044682fff6ecae3f0780888db59410364a475010f20c69f5328f915d63ee26
policy:     66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c
source:     aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec
mode:       preflight
fixture:    false
exit 0allow

You should see ALLOW, a receipt ID, the policy and source hashes, and two lines that print on every run. mode defaults to preflight, which runs the pre-invocation guards only; fixture says whether an output fixture was supplied. The command exits 0 on an allow and 2 on a deny, so a script can assert on the exit code.

Then check a tool outside tool_access.allow, and a path that matches forbidden_paths.

policy-checklist · deny-tooltranscript
$ chio --session-db ./admission.db --receipt-db ./receipts.db \
  check --policy ./policy.yaml --tool write_file \
  --params '{"path": "./workspace/out.txt", "content": "test"}'
verdict:    DENY
tool:       write_file
server:     *
reason:     requested tool write_file on server * is not in capability scope
receipt_id: 40db6902b2a0e924bb6c9c5233317557d75a2ec838800c622c4a7d43e4296315
policy:     66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c
source:     aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec
mode:       preflight
fixture:    false
exit 2deny
policy-checklist · deny-pathtranscript
$ chio --session-db ./admission.db --receipt-db ./receipts.db \
  check --policy ./policy.yaml --tool read_file \
  --params '{"path": "./workspace/.env"}'
verdict:    DENY
tool:       read_file
server:     *
reason:     guard denied the request: guard "guard-pipeline" denied the request
receipt_id: 4b70e17c82a2097a9ff56e347b278425cefa8daa9a016852a8f8d2ff99f586cf
policy:     66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c
source:     aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec
mode:       preflight
fixture:    false
exit 2deny

You should see two refusals with different reasons. The tool never reaches the guards: capability selection refuses it, and the reason says it is not in capability scope. The forbidden path reaches the guards, and the pipeline refuses it.

5. Confirm what the policy still leaves reachable

This policy carries forbidden_paths and no path_allowlist. Check a path that matches none of the forbidden patterns.

policy-checklist · denylist-gaptranscript
$ chio --session-db ./admission.db --receipt-db ./receipts.db \
  check --policy ./policy.yaml --tool read_file \
  --params '{"path": "/etc/shadow"}'
verdict:    ALLOW
tool:       read_file
server:     *
receipt_id: 766ad03fcc0dc8e3f03ad484f896cf1969624cf79e833458b1217d29d9d01eb6
policy:     66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c
source:     aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec
mode:       preflight
fixture:    false
exit 0allow

You should see ALLOW for /etc/shadow. forbidden_paths subtracts from whatever is reachable, and path_allowlist decides what is reachable, so add the allowlist when the agent belongs in one directory.

Keep the same two databases for the whole checklist. Each invocation receives a new request identity, so a previous check never prevents the next decision.

policy-checklist · session-reusetranscript
$ chio --session-db ./admission.db --receipt-db ./receipts.db \
  check --policy ./policy.yaml --tool read_file \
  --params '{"path": "./workspace/notes.md"}'
verdict:    ALLOW
tool:       read_file
server:     *
receipt_id: 1950f7e09037848745f36803b29a45cfaee325f00bdc60d45aea088fda77680c
policy:     66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c
source:     aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec
mode:       preflight
fixture:    false
exit 0allow

You should see another ALLOW, and the receipt log now holds each independent decision. Keep admission and receipt storage in separate files; they serve different roles. Script these checks and run them in CI, so every policy change is tested before it deploys.

6. Read which rule refused each call in its receipt

A guard refusal's reason line always names guard-pipeline, the one guard the CLI registers for the whole policy, whichever rule block refused. The rule that fired is recorded in the receipt.

Where a refused or held call stopped, from its symptomsymptomtoken expireduntrusted issuerwrong agentnot in scopeguard refusedshare exceededrevokedbudget spentneeds approvaldecision core, in order1capability2subject3scope4guards5sibling shareadmitthe hosted kernel also checks (order varies by entry path)revocationbudget storeapproval
OutOfScope
requested tool read on server workspace is not in capability scope
Request a grant that covers the tool. A policy change cannot add it, because guards only narrow a grant. Write a Policy
v=Deny at k∗=min⁡{ k:ck(r) fails }=3v = \htmlClass{role-deny}{\text{Deny}}\ \text{at}\ k^{*} = \min\{\,k : c_k(r)\ \text{fails}\,\} = 3
Where a refused or held call stopped, from its symptomsymptomtoken expireduntrusted issuerwrong agentnot in scopeguard refusedshare exceededrevokedbudget spentneeds approvaldecision core, in order1capability2subject3scope4guards5sibling sharethen admittedhosted kernel also checksrevocationbudget storeapproval
OutOfScope
requested tool read on server workspace is not in capability scope
Request a grant that covers the tool. A policy change cannot add it, because guards only narrow a grant. Write a Policy

not in scope: stops at core stage 3, scope. Request a grant that covers the tool. A policy change cannot add it, because guards only narrow a grant.

v=Deny at k∗=min⁡{ k:ck(r) fails }=3v = \htmlClass{role-deny}{\text{Deny}}\ \text{at}\ k^{*} = \min\{\,k : c_k(r)\ \text{fails}\,\} = 3
In Rust · evaluate.rs:40021 lines
// Step 2: subject binding.
if verified.subject_hex != input.request.agent_id {
    // … SubjectMismatch { expected, actual }
    return deny(core_err, None, Some(verified));
}

// Step 3: scope match.
let matched_grant_index = match resolve_matched_grant_index(&verified.scope, input.request) {
    Ok(index) => index,
    Err(error) => return deny(error, None, Some(verified)),
};

// Step 4: guard pipeline.
// … each guard in order; a deny returns GuardDenied, an error GuardError

// Step 5: mutate delegated sibling budget only after every deny-capable
// local check has passed.
if let Err(error) = admit_delegated_budget(input.capability, budgets) {
    let core_err = KernelCoreError::InvalidCapability(error);
    return deny(core_err, Some(matched_grant_index), Some(verified));
}
The core order and its error variants are from the kernel source; messages are the kernel’s error text, abbreviated where they name a value. The hosted kernel adds revocation, budget-store, and approval checks at points that depend on the entry path, so they are drawn without an order.The state a verdict depends on

List the refusals from the receipt store and pull out each one's guard evidence.

policy-checklist · evidencetranscript
$ chio --receipt-db ./receipts.db receipt list --admin-all \
  | jq -c 'select(.decision.verdict == "deny")
$            | {tool: .tool_name, path: .action.parameters.path,
$               reason: .decision.reason, guard: .decision.guard,
$               evidence: [.evidence[]?.guard_name]}'
{"tool":"write_file","path":"./workspace/out.txt","reason":"requested tool write_file on server * is not in capability scope","guard":"kernel","evidence":[]}
{"tool":"read_file","path":"./workspace/.env","reason":"guard denied the request: guard \"guard-pipeline\" denied the request","guard":"kernel","evidence":["forbidden-path"]}
exit 0

You should see two receipts. The scope refusal carries no evidence, because no guard ran. The path refusal carries forbidden-path in evidence[].guard_name, the compiled kebab-case guard name rather than the snake_case rule key. Both set decision.guard to kernel, which names the component that decided.

A malformed policy fails at load and prints the accepted keys

HushSpec parses with deny_unknown_fields at every level, so a wrong key stops the load instead of being ignored, and the error lists the accepted set. A block name that is not one of the fourteen:

policy-checklist · unknown-blocktranscript
$ chio policy analyze ./bad-block.yaml
policy analysis failed: failed to load ./bad-block.yaml: failed to parse HushSpec document at /private~/chio/bad-block.yaml: rules: unknown field `payments_write`, expected one of `forbidden_paths`, `path_allowlist`, `egress`, `secret_patterns`, `patch_integrity`, `shell_commands`, `tool_access`, `computer_use`, `remote_desktop_channels`, `input_injection`, `browser_automation`, `code_execution`, `velocity`, `human_in_loop` at line 4 column 3
exit 2

A field the block does not define:

policy-checklist · unknown-fieldtranscript
$ chio policy analyze ./bad-field.yaml
policy analysis failed: failed to load ./bad-field.yaml: failed to parse HushSpec document at /private~/chio/bad-field.yaml: rules.tool_access: unknown field `deny`, expected one of `enabled`, `allow`, `block`, `require_confirmation`, `default`, `max_args_size`, `require_runtime_assurance_tier`, `prefer_runtime_assurance_tier`, `require_workload_identity`, `prefer_workload_identity` at line 6 column 5
exit 2

chio policy analyze loads and resolves a policy without evaluating a call, which makes it the cheapest parse check in CI. Run it on the corrected policy.yaml:

Terminalbash
chio policy analyze ./policy.yaml
Output
policy_sha256  9c41…e07b

ID           SEVERITY  KIND                  BLOCK                 RULE

summary: 0 error(s), 0 warning(s), 0 notice(s)

You should see the hash of the resolved document and no findings. A block it cannot reason about statically, such as a velocity window, appears as a not_analyzed notice, which does not fail the run. The remaining errors come from chio check itself.

  • durable admission mode requires a database. No database flags were passed. Pass both --session-db and --receipt-db.
  • durable receipt persistence unavailable. Only --session-db was passed, so every verdict is a deny. Add --receipt-db; --revocation-db and --budget-db conflict with durable admission.
  • request id conflicts with retained operation. One request identity was reused for a different retained operation. Preserve the retained operation and diagnose the reused identity before retrying an effect; independent checks receive fresh identities.
  • preflight cannot evaluate post-output guards. The policy enables secret_patterns or patch_integrity. Add --mode full --output-fixture <path-to-json-file>; the flag takes a path to a JSON file holding the output to evaluate, never inline JSON.
  • An unexpected deny with path-allowlist in the evidence. chio check opens a session that declares no filesystem roots, and the guard matches nothing against an empty root set. Exercise a path_allowlist policy through a live edge, where the client supplies roots.
Captured output for the two database errors
policy-checklist · no-dbtranscript
$ chio check --policy ./policy.yaml --tool read_file \
  --params '{"path": "./workspace/README.md"}'
error[CHIO-CLI-ADMISSION-STORE-REQUIRED]: durable admission needs a session database so operations and tool outcomes survive a restart
  help: pass --session-db <path>
  code: urn:chio:error:cli:admission-store-required
  docs: https://chio.computer/docs/reference/errors#error-cli-admission-store-required
exit 1
policy-checklist · receipt-db-missingtranscript
$ chio --session-db ./admission.db \
  check --policy ./policy.yaml --tool read_file \
  --params '{"path": "./workspace/README.md"}'
verdict:    DENY
tool:       read_file
server:     *
reason:     internal error: durable receipt persistence unavailable: no receipt store configured
receipt_id: 7ba0d1ef263cd0790eaa68e9c3dc307e8d1ec387dd2decb1d7a93ffc30852f59
policy:     66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c
source:     aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec
mode:       preflight
fixture:    false
exit 2deny

Pattern policies and rule block fields

Read-only workspace

An agent that reviews a codebase, summarizes a directory tree, or answers questions over local files. No writes, no shell, no network.

readonly-workspace.yamlyaml
hushspec: "0.1.0"
name: readonly-workspace
description: Read-only access to a project workspace.

rules:
  tool_access:
    enabled: true
    default: block
    allow:
      - read_file
      - list_directory
      - search_files

  path_allowlist:
    enabled: true
    read:
      - "./workspace/**"
    write: []
    patch: []

  forbidden_paths:
    enabled: true
    patterns:
      - "**/.env"
      - "**/.env.*"
      - "**/*.pem"
      - "**/*.key"
      - "**/.ssh/**"

  shell_commands:
    enabled: true
    forbidden_patterns:
      - ".*"

  egress:
    enabled: true
    allow: []

  secret_patterns:
    enabled: true

  patch_integrity:
    enabled: true

  velocity:
    enabled: true
    max_invocations_per_window: 200
    window_secs: 120

API only, with no filesystem

An agent that talks to GitHub, OpenAI, and Anthropic and touches no local file. The filesystem is closed; egress is open to three named hosts.

api-only.yamlyaml
hushspec: "0.1.0"
name: api-only
description: Network API access with no filesystem operations.

rules:
  tool_access:
    enabled: true
    default: block
    allow:
      - fetch
      - search_repositories
      - create_issue

  path_allowlist:
    enabled: true
    read: []
    write: []
    patch: []

  forbidden_paths:
    enabled: true
    patterns:
      - "**/*"

  egress:
    enabled: true
    allow:
      - "api.github.com"
      - "*.openai.com"
      - "api.anthropic.com"

  shell_commands:
    enabled: true
    forbidden_patterns:
      - ".*"

  secret_patterns:
    enabled: true
    patterns:
      - name: bearer_token
        pattern: 'Bearer\s+[A-Za-z0-9\-._~+/]+=*'
        severity: critical

  patch_integrity:
    enabled: true

  velocity:
    enabled: true
    max_invocations_per_window: 100
    window_secs: 60

Development with named tools

A broader local policy for named development tools. It allows workspace edits and mediated network calls, adds explicit secret and command checks, and requires approval for deletion. Test the classified actions your adapter submits: pattern checks neither sandbox a process nor recognize every equivalent command.

development.yamlyaml
hushspec: "0.1.0"
name: development
description: Local development policy with explicit tools and approval for deletion.

rules:
  tool_access:
    enabled: true
    default: block
    allow:
      - read_file
      - list_directory
      - write_file
      - apply_patch
      - delete_file
      - execute_command
      - fetch
    block:
      - execute_command_as_root
    require_confirmation:
      - delete_file

  path_allowlist:
    enabled: true
    read:
      - "./**"
    write:
      - "./**"
    patch:
      - "./**"

  forbidden_paths:
    enabled: true
    patterns:
      - "**/.ssh/id_*"
      - "**/.aws/credentials"
      - "**/.env.production"

  shell_commands:
    enabled: true
    forbidden_patterns:
      - '(?i)rm\s+-rf\s+/'
      - '(?i)\bchmod\s+777\b'
      - '(?i)curl.*\|.*bash'

  egress:
    enabled: true
    default: allow
    block:
      - "*.malware.com"

  secret_patterns:
    enabled: true

  patch_integrity:
    enabled: true
    max_additions: 2000
    max_deletions: 1000

  velocity:
    enabled: true
    max_invocations_per_window: 500
    window_secs: 60

Production lockdown

An agent running unattended against production data: two tools, one directory, no admitted shell or egress actions, and a 60-token invocation bucket refilling at one token per second. Apply it on the host's mediated paths; isolation comes from the host.

production.yamlyaml
hushspec: "0.1.0"
name: production-lockdown
description: Maximum restriction for production deployments.

rules:
  tool_access:
    enabled: true
    default: block
    allow:
      - read_file
      - list_directory
    max_args_size: 2048

  path_allowlist:
    enabled: true
    read:
      - "/app/data/**"
    write: []
    patch: []

  forbidden_paths:
    enabled: true
    patterns:
      - "**/.env*"
      - "**/*.pem"
      - "**/*.key"
      - "**/.ssh/**"
      - "**/.aws/**"
      - "**/.gcloud/**"
      - "**/credentials*"
      - "**/secrets*"

  shell_commands:
    enabled: true
    forbidden_patterns:
      - ".*"

  egress:
    enabled: true
    allow: []

  secret_patterns:
    enabled: true
    patterns:
      - name: aws_key
        pattern: "AKIA[0-9A-Z]{16}"
        severity: critical
      - name: private_key
        pattern: "-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----"
        severity: critical
      - name: jwt
        pattern: 'eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.'
        severity: error

  patch_integrity:
    enabled: true
    max_additions: 200
    max_deletions: 100
    forbidden_patterns:
      - 'eval\('
      - 'Function\('
      - '__import__\('
    require_balance: true
    max_imbalance_ratio: 3.0

  velocity:
    enabled: true
    max_invocations_per_window: 60
    window_secs: 60

Top-level fields

FieldRequiredDescription
hushspec
Yes
Schema version, always "0.1.0"
name
No
Human-readable name for the policy
description
No
The policy's intent
extends
No
Base policy plus an overlay; see merge_strategy
merge_strategy
No
replace, merge, or deep_merge
rules
No
Guard configurations

mcp-tool (tool_access)

Controls which tools an agent can invoke. With default: allow, a new tool passes this guard unless another rule blocks it; the caller still needs a matching capability and must pass the other checks.

policy.yaml · rulesyaml
rules:
  tool_access:
    enabled: true
    default: block          # "block" or "allow"
    allow:                  # Tools explicitly permitted
      - read_file
      - list_directory
      - search_files
    block:                  # Tools explicitly denied (overrides allow)
      - delete_file
      - execute_command
    require_confirmation:   # Tools that need human approval
      - write_file
    max_args_size: 4096     # Max size of tool arguments in bytes
FieldTypeDefaultDescription
enabled
bool
true
Whether this guard is active.
default
string
allow
Action for tools in neither list.
allow
list
[]
Tool names explicitly allowed.
block
list
[]
Tool names explicitly blocked; overrides allow.
require_confirmation
list
[]
Tools that require human approval.
max_args_size
int
none
Maximum serialized argument size, in bytes.

path-allowlist

Restricts filesystem access to declared directory trees, with separate lists for read, write, and patch operations. Enabled with empty lists, it refuses every filesystem request the policy evaluates. The host routes the relevant operations through Chio and supplies operating-system isolation.

policy.yaml · rulesyaml
rules:
  path_allowlist:
    enabled: true
    read:                   # Paths the agent can read from
      - "./workspace/**"
      - "./config/settings.json"
    write:                  # Paths the agent can write to
      - "./workspace/output/**"
    patch:                  # Paths the agent can apply patches to
      - "./workspace/src/**"
FieldTypeDefaultDescription
enabled
bool
false
Whether this guard is active.
read
list
[]
Glob patterns for readable paths.
write
list
[]
Glob patterns for writable paths.
patch
list
[]
Glob patterns for patchable paths.

forbidden-path

Refuses specific file patterns regardless of the allowlist. Forbidden paths take precedence: a path that matches both the allowlist and a forbidden pattern is denied.

policy.yaml · rulesyaml
rules:
  forbidden_paths:
    enabled: true
    patterns:               # Glob patterns that are always denied
      - "**/.env"
      - "**/.env.*"
      - "**/*.pem"
      - "**/*.key"
      - "**/.ssh/**"
      - "**/node_modules/**"
    exceptions:             # Paths exempt from forbidden patterns
      - "/workspace/.ssh/known_hosts"
FieldTypeDefaultDescription
enabled
bool
true
Whether this guard is active.
patterns
list
[]
Glob patterns that are always denied.
exceptions
list
[]
Specific paths exempt from forbidden patterns.

shell-command

Checks any tool call that carries shell command content against the forbidden patterns. Patterns use standard regex syntax; (?i) makes a match case-insensitive.

policy.yaml · rulesyaml
rules:
  shell_commands:
    enabled: true
    forbidden_patterns:     # Regex patterns that cause a deny
      - '(?i)rm\s+-rf\s+/'
      - '(?i)\b(curl|wget)\b.*\|.*\b(bash|sh)\b'
      - '(?i)\bchmod\s+777\b'
      - "(?i)\b(DROP|DELETE|TRUNCATE)\b"
FieldTypeDefaultDescription
enabled
bool
true
Whether this guard is active.
forbidden_patterns
list
[]
Regex patterns that trigger a deny verdict.

egress-allowlist

Controls outbound network access. The default action is block, so a domain must be allowed explicitly.

policy.yaml · rulesyaml
rules:
  egress:
    enabled: true
    default: block          # "block" or "allow"
    allow:                  # Domains the agent can reach
      - "api.github.com"
      - "*.openai.com"
      - "registry.npmjs.org"
    block:                  # Domains explicitly denied (overrides allow)
      - "evil.com"
FieldTypeDefaultDescription
enabled
bool
true
Whether this guard is active.
default
string
block
Action for domains in neither list.
allow
list
[]
Permitted domain patterns; supports * wildcards.
block
list
[]
Domain patterns explicitly blocked; overrides allow.

secret-leak

One block, two components with different verdicts. On the write path, SecretLeakGuard scans the content of a file write or patch and denies the call when a pattern matches; every other action passes through (crates/guards/chio-guards/src/secret_leak.rs:335-339). On the read path, a post-invocation SanitizerHook scans the tool result and returns Redact, so the response is rewritten instead of refused (crates/guards/chio-guards/src/post_invocation.rs:106-127). Enabling secret_patterns attaches both (crates/guards/chio-policy/src/compiler/rules.rs:130-146).

Because the hook reads output, a policy with this block cannot be dry-run in the default preflight mode; use --mode full --output-fixture with a JSON file holding the output to evaluate. Without custom patterns, Chio uses built-in detectors for common secret formats such as AWS keys, GitHub tokens, and private keys.

policy.yaml · rulesyaml
rules:
  secret_patterns:
    enabled: true
    patterns:               # Custom secret detection patterns
      - name: aws_access_key
        pattern: "AKIA[0-9A-Z]{16}"
        severity: critical
        description: "AWS Access Key ID"
      - name: github_token
        pattern: "gh[ps]_[A-Za-z0-9_]{36}"
        severity: critical
        description: "GitHub Personal Access Token"
      - name: generic_api_key
        pattern: '(?i)(api[_-]?key|apikey)\s*[:=]\s*[''"]?[A-Za-z0-9]{20,}'
        severity: warn
    skip_paths:             # Paths excluded from scanning
      - "**/fixtures/**"
      - "**/test/data/**"
FieldTypeDefaultDescription
enabled
bool
true
Whether this guard is active.
patterns
list
[]
Custom patterns: name, pattern (regex), severity, optional description.
skip_paths
list
[]
Glob patterns for paths excluded from scanning.

Each pattern's severity is critical, error, or warn. The parser requires it and neither compiler path reads it, so it records intent without changing behavior.

patch-integrity

Checks patches and file modifications: patch size, forbidden content in diffs, and the balance of additions to deletions.

policy.yaml · rulesyaml
rules:
  patch_integrity:
    enabled: true
    max_additions: 1000       # Max lines added per patch
    max_deletions: 500        # Max lines deleted per patch
    forbidden_patterns:       # Regex patterns forbidden in diffs
      - 'eval\('
      - 'Function\('
      - '__import__\('
    require_balance: false    # Require additions/deletions to be balanced
    max_imbalance_ratio: 10.0 # Max ratio of additions to deletions
FieldTypeDefaultDescription
enabled
bool
true
Whether this guard is active.
max_additions
int
1000
Maximum added lines in a single patch.
max_deletions
int
500
Maximum deleted lines in a single patch.
forbidden_patterns
list
[]
Regex patterns that must not appear in diffs.
require_balance
bool
false
Whether additions and deletions must be roughly balanced.
max_imbalance_ratio
float
10.0
Maximum ratio of additions to deletions.

velocity

Bounds request and planned-spend rates with refillable token buckets. The window sets the refill calculation; it is not a sliding-window counter. max_invocations_per_window is subject to integer quantization, and max_spend_per_window counts planned cost, never the external charges a tool incurs. Rate Limit Guards covers bucket keys, burst capacity, and refill quantization.

policy.yaml · rulesyaml
rules:
  velocity:
    enabled: true
    max_invocations_per_window: 120  # Input to capacity and refill calculations
    window_secs: 60                  # Refill period in seconds
    max_spend_per_window: 5000       # Planned-cost tokens; requires grant cost metadata
    max_requests_per_agent: 120      # Optional per-agent bucket
    max_requests_per_session: 480    # Optional per-session bucket
    burst_factor: 1.0                # Token-bucket burst multiplier
FieldTypeDefaultDescription
enabled
bool
true
Whether this guard is active.
max_invocations_per_window
int
none
Base invocation capacity and refill-rate input.
window_secs
int
60
Refill period, in seconds.
max_spend_per_window
int
none
Planned-cost bucket input; needs max_cost_per_invocation on the matched grant.
max_requests_per_agent
int
none
Maximum requests attributed to one agent.
max_requests_per_session
int
none
Maximum requests within one session.
burst_factor
float
1.0
Token-bucket burst multiplier over the base rate.

Additional rule blocks

Six more blocks gate interactions beyond files, shell commands, and network access. Each is disabled unless configured and compiles to a dedicated guard.

policy.yaml · rulesyaml
rules:
  # Computer-use / desktop-control action gating.
  computer_use:
    enabled: true
    mode: guardrail              # observe | guardrail | fail_closed
    allowed_actions:
      - screenshot
      - left_click

  # Remote-desktop side channels. Each channel is off unless listed true;
  # audio defaults to true.
  remote_desktop_channels:
    enabled: true
    clipboard: false
    file_transfer: false
    audio: true
    drive_mapping: false

  # Synthetic keyboard/mouse input injection.
  input_injection:
    enabled: true
    allowed_types:
      - keyboard
    require_postcondition_probe: true

  # Browser-automation domain and verb gating.
  browser_automation:
    enabled: true
    allowed_domains:
      - "pricing.larkfield.example"
    blocked_domains:
      - "accounts.google.com"
    allowed_verbs:
      - navigate
      - click
    credential_detection: true

  # Sandboxed code-interpreter restrictions.
  code_execution:
    enabled: true
    language_allowlist:
      - python
    module_denylist:
      - os
      - subprocess
    network_access: false
    max_execution_time_ms: 5000

  # Human-in-the-loop approval gating.
  human_in_loop:
    enabled: true
    require_confirmation:         # Globs that always require approval
      - "deploy_*"
    approve_above: 10000          # Cost threshold, integer minor units
    approve_above_currency: USD
    timeout_seconds: 300
    on_timeout: deny              # deny | defer
Rule blockCompiled guardKey fields
computer_use
ComputerUseGuard
mode (observe, guardrail, fail_closed), allowed_actions
remote_desktop_channels
RemoteDesktopSideChannelGuard
clipboard, file_transfer, audio, drive_mapping
input_injection
InputInjectionCapabilityGuard
allowed_types, require_postcondition_probe
browser_automation
BrowserAutomationGuard
allowed_domains, blocked_domains, allowed_verbs, credential_detection
code_execution
CodeExecutionGuard
language_allowlist, module_denylist, network_access, max_execution_time_ms
human_in_loop
RequireApprovalAbove constraint
require_confirmation, approve_above, approve_above_currency, timeout_seconds, on_timeout

human_in_loop complements tool_access.require_confirmation. Its require_confirmation globs collapse matched tools to an approval threshold of 0, so every call needs sign-off, while approve_above gates only calls whose declared maximum meets or exceeds the threshold. Both compile to a RequireApprovalAbove constraint on the matched tool grants.

Refusal names in source

The kernel formats reason from guard.name() (crates/kernel/chio-kernel/src/kernel/dispatch.rs:400-407), and the CLI registers GuardPipeline, whose name is the literal guard-pipeline (crates/guards/chio-guards/src/pipeline.rs:58-61). The pipeline appends the evidence entry as it short-circuits (crates/guards/chio-guards/src/pipeline.rs:79-87). decision.guard is set to kernel in crates/kernel/chio-kernel/src/kernel/responses/deny_responses.rs:84-86.

Next: see what the policy compiles to