BuildPolicy
Write a Policy
HushSpec policies define accessible tools, filesystem paths, network egress, and agent rate limits.
Prerequisites
- The
chiobinary on your path. Nothing else on this page needs a server, a network or a credential. - The exact tool names your server exposes. A policy names tools, and a name that does not match is silently inert.
chio mcp wrap --print-scopesprints them for an MCP server; see Wrap an MCP Server. - A writable directory for two SQLite files.
chio checkrefuses to evaluate anything without both a session database and a receipt database.
HushSpec Format Basics
A policy file starts with hushspec: "0.1.0" at the top. HushSpec uses strict schema validation (deny_unknown_fields), so only the keys listed here are accepted at the top level: hushspec, name, description, extends, merge_strategy, rules, extensions, and metadata. All guard configuration lives under rules:.
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.| Field | Required | Description |
|---|---|---|
hushspec | Yes | Schema version. Always "0.1.0". |
name | No | Human-readable name for the policy. |
description | No | Description of the policy's intent. |
extends | No | Base policy plus an overlay. Supports merge_strategy of replace, merge, or deep_merge. |
rules | No | Guard configurations. |
The rules block contains one entry per guard. Guards that are not listed are disabled and return allow by default. A policy with no rules block is valid: it simply applies no restrictions.
Policy inheritance
extends to build on a base policy plus an overlay. Set merge_strategy to replace, merge, or deep_merge to control how the overlay folds into the base. This is useful for team-wide baselines with project-specific overrides.Rule blocks
rules: accepts exactly fourteen keys, and no others. HushSpec parses with deny_unknown_fields, so a typo or an invented block name fails validation outright: 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, and human_in_loop. Each block compiles to one or more guards; every block has an enabled field; omitted blocks are off.
The compiler attaches some guards without a corresponding rule block. Enabling egress also attaches an internal-network SSRF companion that blocks RFC1918 and cloud-metadata endpoints. There is no rules.internal_network key to configure it. Per-agent rate limiting (agent-velocity) is derived from velocity and from origin budgets under extensions.origins, not from a standalone rule key. Eight blocks are covered in depth here; the remaining six are summarized under Additional Rule Blocks.
mcp-tool (tool_access)
Controls which tools an agent can invoke.
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| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether this guard is active. |
default | string | allow | Action for tools not in allow or block lists. |
allow | list | [] | Tool names that are explicitly allowed. |
block | list | [] | Tool names that are explicitly blocked. |
require_confirmation | list | [] | Tools that require human-in-the-loop approval. |
max_args_size | int | none | Maximum size of serialized tool arguments in bytes. |
default: block is strongly recommended
default: allow, any new tool exposed by the MCP server is automatically accessible. Use default: block and explicitly add tools to the allow list.path-allowlist
Restricts filesystem access to declared directory trees, with separate lists for read, write, and patch operations.
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/**"| Field | Type | Default | Description |
|---|---|---|---|
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. |
When enabled with empty lists, no filesystem access is allowed. This effectively creates a sandbox with no file access.
forbidden-path
Blocks access to specific file patterns regardless of the allowlist. Forbidden paths take precedence: if a path matches both the allowlist and forbidden patterns, it is denied.
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"| Field | Type | Default | Description |
|---|---|---|---|
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
Validates or blocks shell command execution. When enabled, any tool call that contains shell command content is checked against the forbidden patterns.
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"| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether this guard is active. |
forbidden_patterns | list | [] | Regex patterns that trigger a deny verdict. |
Patterns use standard regex syntax. Use (?i) for case-insensitive matching.
egress-allowlist
Controls outbound network access. The default action is block: domains must be explicitly allowed.
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"| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether this guard is active. |
default | string | block | Action for domains not in allow or block lists. |
allow | list | [] | Domain patterns that are permitted. Supports * wildcards. |
block | list | [] | Domain patterns that are explicitly blocked. |
secret-leak
One block, two components with different verdicts. On the write path, SecretLeakGuard scans the content of a file write or a patch and denies the call when a pattern matches; every other action passes through untouched (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 rather than 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 is output-sensitive, 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 against.
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/**"| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether this guard is active. |
patterns | list | [] | Custom secret patterns. Each needs name, pattern (regex) and severity (critical/error/warn), and may carry description. severity is required by the parser and read by neither compiler path, so it documents intent rather than changing behavior. |
skip_paths | list | [] | Glob patterns for paths excluded from secret scanning. |
When enabled without custom patterns, Chio uses built-in detectors for common secret formats (AWS keys, GitHub tokens, private keys, etc.). Add custom patterns to catch project-specific secrets.
patch-integrity
Validates patches and file modifications for safety. Controls patch size, forbidden content in diffs, and addition/deletion balance.
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| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether this guard is active. |
max_additions | int | 1000 | Maximum number of added lines in a single patch. |
max_deletions | int | 500 | Maximum number of 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 allowed ratio of additions to deletions. |
velocity
Rate-limits tool invocations per time window. Prevents runaway agents from exhausting resources.
rules:
velocity:
enabled: true
max_invocations_per_window: 100 # Maximum calls within the window
window_secs: 60 # Sliding window duration in seconds
max_spend_per_window: 5000 # Optional spend cap, minor units (e.g. cents)
max_requests_per_agent: 100 # Optional per-agent request cap
max_requests_per_session: 500 # Optional per-session request cap
burst_factor: 1.0 # Token-bucket burst multiplier| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether this guard is active. |
max_invocations_per_window | int | none | Maximum number of tool calls in the window. |
window_secs | int | 60 | Duration of the sliding window in seconds. |
max_spend_per_window | int | none | Spend cap per window, in integer minor units (e.g. cents). |
max_requests_per_agent | int | none | Maximum requests attributed to a single 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.
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:
- "app.example.com"
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 block | Compiled guard | Key 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 is complementary to tool_access.require_confirmation: the require_confirmation globs collapse matched tools to an approval threshold of 0 (every call needs sign-off), while approve_above gates only calls whose declared cost exceeds the threshold. Both compile to a RequireApprovalAbove constraint on the matched tool grants.
Composing Rules
Guards use a conjunctive (AND) model: every enabled guard must allow a request for it to proceed. A single deny from any guard rejects the entire call. Compose a policy by layering independent constraints.
Start with a restrictive policy, then add the permissions you need:
- Step 1: Set
tool_access.default: blockand add only the tools your agent needs - Step 2: Enable
path_allowlistwith the narrowest directory tree that covers your workflow - Step 3: Add
forbidden_pathsfor sensitive files that might exist inside the allowlist - Step 4: Configure
egressto allow only the domains your workflow needs, blocking everything else - Step 5: Add
shell_commands,secret_patterns,patch_integrity, andvelocityas defense-in-depth layers
Because guards are independent, disabling one does not affect the others. Start with tool_access and add guards as requirements clarify. Investigate the first production deny: it may show that the allowlist is too narrow, or that it blocked an unauthorized call.
Testing Policies with chio check
The chio check command evaluates a single tool call against a policy without starting any server. Every capture in this section comes from running it against the policy.yaml at the top of this page.
$ chio --session-db <admission.db> --receipt-db <receipts.db> \
check --policy ./policy.yaml \
--tool <tool-name> \
--params '<json-arguments>'Both database flags are required. Admission is durable, so the command refuses to evaluate anything until it has somewhere to record the operation and the receipt.
$ chio check --policy ./policy.yaml --tool read_file \
--params '{"path": "./workspace/README.md"}'error [urn:chio:error:cli:other]: durable admission mode requires a database so operations and tool outcomes survive restart
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.Supply only --session-db and the command runs, but every verdict is a deny because there is no receipt store to write to.
$ chio --session-db ./admission-0.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: 8c2c739c82cb3adf52ec587b974e9eb5ed840b2da198563788aa366c1333b446 policy: 66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c source: aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec mode: preflight fixture: false
With both flags, the allow case reads a workspace file that tool_access permits and forbidden_paths does not match:
$ chio --session-db ./admission-1.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: 853eb496dffe8e33fd9e931d2ce4ec3c96b46dce97fd8598578152aad950cacd policy: 66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c source: aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec mode: preflight fixture: false
mode: and fixture: print on every run. preflight is the default and evaluates the pre-invocation guards only. A policy that enables secret_patterns or patch_integrity refuses preflight and needs --mode full --output-fixture instead. That flag takes a path to a JSON file, not inline JSON.
A tool outside tool_access.allow never reaches the guards. It is refused at capability selection, and the reason says so:
$ chio --session-db ./admission-2.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: cd771be35b83ea3f43b5d3e292b3dd01ac85407b8b24d0ca1dfa5bbee7ea1ea5 policy: 66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c source: aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec mode: preflight fixture: false
A path matching forbidden_paths does reach the guards, and one of them denies it:
$ chio --session-db ./admission-3.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: 2896eacae71231dcaee2de93ccf907f15ba290fd7b0de5285d1ac0155ba494f2 policy: 66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c source: aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec mode: preflight fixture: false
Reading which guard fired
The reason: line names the top-level registered guard, which is always the pipeline. The kernel formats it 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). Every guard deny reads the same, whichever rule block produced it.
The individual guard is recorded in the receipt instead, under evidence[].guard_name, using the compiled kebab-case guard identifier rather than the snake_case rule-block key. The pipeline appends that entry as it short-circuits (crates/guards/chio-guards/src/pipeline.rs:79-87). Read the receipt to find out which rule fired:
$ 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"]}The capability-scope deny carries no evidence, because no guard ran. The forbidden_paths deny carries forbidden-path. Both set decision.guard to kernel (crates/kernel/chio-kernel/src/kernel/responses/deny_responses.rs:84-86), which names the component that made the decision rather than the rule that failed.
What a denylist alone does not cover
This policy carries forbidden_paths and no path_allowlist. A path that matches none of the forbidden patterns is allowed, wherever it lives:
$ chio --session-db ./admission-4.db --receipt-db ./receipts.db \
check --policy ./policy.yaml --tool read_file \
--params '{"path": "/etc/shadow"}'verdict: ALLOW tool: read_file server: * receipt_id: c40ea5a6ff40fd2002a8bd886367b12afc0280980023a87c1518c0582b6475d2 policy: 66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c source: aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec mode: preflight fixture: false
Add path_allowlist when the agent should be confined to a directory. forbidden_paths subtracts from whatever is reachable; it does not decide what is reachable.
One session database per check
chio check issues its request under the fixed id check-001 (crates/products/chio-cli/src/cli/runtime.rs:703). The first successful check retains an operation under that id, so a second check against the same session database is denied by admission before the policy is consulted, whatever tool it names:
$ chio --session-db ./admission-1.db --receipt-db ./receipts.db \
check --policy ./policy.yaml --tool read_file \
--params '{"path": "./workspace/notes.md"}'verdict: DENY tool: read_file server: * reason: durable admission failed: request id conflicts with retained operation c9c7b4cb169b09d60c813747cb55bbc92ccead29e521dcfa16f79c39a699d86d receipt_id: 07be08de691b6acb87af4780c563893410ef423780ee9ef232b40684ae7c1b79 policy: 66f1f496f9d4305dfd19e0b6269a51ddcd0c3fc4cd3b2af6cadf3eba4413e09c source: aaca00de9bc252e43a089ce0d4e0b6edf813d61fd6712d86915a4dffd917baec mode: preflight fixture: false
Give each check its own --session-db path. One shared --receipt-db is fine, and it is what lets a checklist read every verdict back at the end.
Automate policy testing
chio check calls and run them in CI. Treat policy changes like code changes: test before deploying. Assert on the exit code, which is 0 for an allow and 2 for a deny.Failures and Recovery
HushSpec parses with deny_unknown_fields at every level, so a wrong key is a load failure rather than a silently ignored line, and the error prints the accepted set. A block name that is not one of the fourteen:
$ chio policy analyze ./bad-block.yamlpolicy analysis failed: failed to load ./bad-block.yaml: failed to parse HushSpec document at ~/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
A field that is not one of that block's:
$ chio policy analyze ./bad-field.yamlpolicy analysis failed: failed to load ./bad-field.yaml: failed to parse HushSpec document at ~/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
The same check runs from chio policy analyze, which loads and resolves a policy without evaluating a call, so it is the cheapest way to find a parse error in CI.
| What you see | What it means | Fix |
|---|---|---|
durable admission mode requires a database | No database flags. | Pass both --session-db and --receipt-db. |
durable receipt persistence unavailable | Only --session-db was passed. | Add --receipt-db. Do not add --revocation-db or --budget-db, which conflict with durable admission. |
request id conflicts with retained operation | The session database has already served a check. | Use a fresh --session-db path per check. |
preflight cannot evaluate post-output guards | The policy enables secret_patterns or patch_integrity. | Add --mode full --output-fixture <path-to-json-file>. |
A deny you expected to be an allow, with path-allowlist in the evidence | chio check opens a session that declares no filesystem roots, and the guard treats an empty root set as matching nothing. | Exercise a path_allowlist policy through a live edge, where the client supplies roots, rather than through the dry run. |
Common Policy Patterns
Read-Only Workspace
An agent reviewing a codebase, summarizing a directory tree, or answering questions over local files. No writes, no shell, no network.
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: 120API-Only (No Filesystem, Network Allowed)
An agent that talks to GitHub, OpenAI, and Anthropic and never touches a local file. The filesystem is closed; egress is open to three named hosts.
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: 60Development (Permissive with Logging)
A developer iterating on agent code locally. Everything is allowed by default, but receipts still ship and the worst footguns ( rm -rf /, curl | bash, secret leaks, production envs) are still caught.
hushspec: "0.1.0"
name: development
description: Permissive local development policy with safety rails.
rules:
tool_access:
enabled: true
default: allow
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: 60Production Lockdown
An agent running unattended against production data. Two tools, one directory, no shell, no egress, 50 calls per minute. Anything not explicitly named here is denied.
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: 50
window_secs: 60Policy Best Practices
- Start with
default: block: new tools exposed by a server should require explicit approval. Do not grant them ambient access. - Use forbidden-path as a safety net: even if your allowlist is tight, add forbidden patterns for
.env,.ssh, and.pemfiles as defense-in-depth - Set velocity limits: even trusted agents can enter loops. A velocity limit prevents runaway invocations from exhausting resources or budgets
- Enable secret-leak in production: a misconfigured upstream server can expose credentials outside the tool configuration
- Test with
chio check: validate your policy against all expected tool calls before deploying. Script these checks and run them in CI - Use policy inheritance for teams: define a team-wide base policy with
forbidden_pathsandsecret_patterns, then extend it with project-specific rules usingextends - Version your policies: treat policies as code. Store them in version control, review changes, and tag releases. The
metadatasection supports governance fields likeauthor,approved_by, andlifecycle_state - Monitor receipts: denied calls reveal either policy misconfiguration (legitimate calls being blocked) or boundary probing (unauthorized access attempts). Both are valuable signals
Policies are not capability tokens
Next Steps
- Native Tool Server · build a chio-native server with built-in guard integration
- Policy Schema Reference · complete schema specification for all fields
- Guards · deep dive into guard evaluation, ordering, and short-circuit behavior
- Wrap an MCP Server · apply your policy to a live MCP server