BuildPolicy
Write a Policy
HushSpec policies define accessible tools, filesystem paths, network egress, and agent rate limits.
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. The subsections below cover the seven most common blocks in depth; the remaining blocks 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
Scans tool arguments and responses for leaked secrets. When a secret pattern matches, the call is denied.
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 has name, pattern (regex), severity (critical/error/warn). |
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. Use it to verify your policy before going live.
$ chio check --policy ./policy.yaml \
--tool <tool-name> \
--params '<json-arguments>'Test allowed and denied calls for each server tool. Build a checklist:
# Verify allowed tools pass
$ chio check --policy ./policy.yaml --tool read_file \
--params '{"path": "./workspace/README.md"}'
verdict: ALLOW
tool: read_file
server: *
receipt_id: rcpt-019dbbf8-33db-...
policy: a40c24d0930d773e060fac86dd77e24e68af4cb0a59b1b836759ed63fbaa23b8
source: d14550004f854d4131839bd2388b3ec9aa3784c898a47f261d434bffbc88d799
# Verify forbidden tools are denied
$ chio 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: rcpt-019dbbf8-33ef-...
# Verify forbidden paths are caught
$ chio check --policy ./policy.yaml --tool read_file \
--params '{"path": "./workspace/.env"}'
verdict: DENY
tool: read_file
server: *
reason: guard denied the request: guard "forbidden-path" denied the request: path matches forbidden pattern **/.env
receipt_id: rcpt-019dbbf8-3405-...
# Verify paths outside the allowlist are denied
$ chio check --policy ./policy.yaml --tool read_file \
--params '{"path": "/etc/shadow"}'
verdict: DENY
tool: read_file
server: *
reason: guard denied the request: guard "path-allowlist" denied the request
receipt_id: rcpt-019dbbf8-3421-...Automate policy testing
chio check calls and run them in CI. Treat policy changes like code changes: test before deploying.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