Write a Policy
Policies are HushSpec YAML files that configure the guard pipeline. They pin which tools are accessible, which paths are readable, what egress is allowed, and how fast an agent can operate. Eight guards, composed conjunctively, evaluated against every call.
HushSpec Format Basics
Every 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 core of the policy. |
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.The default pipeline
Eight guards evaluate every tool call: forbidden-path, path-allowlist, shell-command, egress-allowlist, mcp-tool, secret-leak, patch-integrity, and velocity. Three more are opt-in and session-aware: agent-velocity, internal-network, and data-flow. Each guard configures independently under rules:; each has an enabled field; omitted guards are off.
mcp-tool (tool_access)
Controls which tools an agent can invoke. This is the most fundamental guard: it determines the agent's tool surface.
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: 100 # Maximum calls within the window
window_seconds: 60 # Sliding window duration in seconds| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | - | Whether this guard is active. |
max_invocations | int | - | Maximum number of tool calls in the window. |
window_seconds | int | - | Duration of the sliding window in seconds. |
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. This means you compose a policy by layering independent constraints.
The recommended approach is to start restrictive and open up selectively:
- 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 just tool_access and add more guards as requirements clarify. The first deny in production is feedback, not a fault: a legitimate call hitting the wall means the allowlist is too narrow, an unauthorized call hitting it means the system is doing exactly what you wrote it to do.
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 every tool your server exposes, both allowed and forbidden calls. 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-paths" 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: 200
window_seconds: 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: 100
window_seconds: 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: 500
window_seconds: 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: 50
window_seconds: 60Policy Best Practices
- Start with
default: block: new tools exposed by a server should require explicit approval, not inherit 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 leak credentials your tools never knew were there
- 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