Chio/Docs

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:.

policy.yaml
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.
FieldRequiredDescription
hushspecYesSchema version. Always "0.1.0".
nameNoHuman-readable name for the policy.
descriptionNoDescription of the policy's intent.
extendsNoBase policy plus an overlay. Supports merge_strategy of replace, merge, or deep_merge.
rulesNoGuard 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

Use 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.

yaml
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
enabledbooltrueWhether this guard is active.
defaultstringallowAction for tools not in allow or block lists.
allowlist[]Tool names that are explicitly allowed.
blocklist[]Tool names that are explicitly blocked.
require_confirmationlist[]Tools that require human-in-the-loop approval.
max_args_sizeintnoneMaximum size of serialized tool arguments in bytes.

default: block is strongly recommended

With 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.

yaml
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
enabledboolfalseWhether this guard is active.
readlist[]Glob patterns for readable paths.
writelist[]Glob patterns for writable paths.
patchlist[]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.

yaml
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
enabledbooltrueWhether this guard is active.
patternslist[]Glob patterns that are always denied.
exceptionslist[]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.

yaml
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
enabledbooltrueWhether this guard is active.
forbidden_patternslist[]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.

yaml
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
enabledbooltrueWhether this guard is active.
defaultstringblockAction for domains not in allow or block lists.
allowlist[]Domain patterns that are permitted. Supports * wildcards.
blocklist[]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.

yaml
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
enabledbooltrueWhether this guard is active.
patternslist[]Custom secret patterns. Each has name, pattern (regex), severity (critical/error/warn).
skip_pathslist[]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.

yaml
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
enabledbooltrueWhether this guard is active.
max_additionsint1000Maximum number of added lines in a single patch.
max_deletionsint500Maximum number of deleted lines in a single patch.
forbidden_patternslist[]Regex patterns that must not appear in diffs.
require_balanceboolfalseWhether additions and deletions must be roughly balanced.
max_imbalance_ratiofloat10.0Maximum allowed ratio of additions to deletions.

velocity

Rate-limits tool invocations per time window. Prevents runaway agents from exhausting resources.

yaml
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
FieldTypeDefaultDescription
enabledbooltrueWhether this guard is active.
max_invocations_per_windowintnoneMaximum number of tool calls in the window.
window_secsint60Duration of the sliding window in seconds.
max_spend_per_windowintnoneSpend cap per window, in integer minor units (e.g. cents).
max_requests_per_agentintnoneMaximum requests attributed to a single agent.
max_requests_per_sessionintnoneMaximum requests within one session.
burst_factorfloat1.0Token-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.

yaml
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 blockCompiled guardKey fields
computer_useComputerUseGuardmode (observe/guardrail/fail_closed), allowed_actions
remote_desktop_channelsRemoteDesktopSideChannelGuardclipboard, file_transfer, audio, drive_mapping
input_injectionInputInjectionCapabilityGuardallowed_types, require_postcondition_probe
browser_automationBrowserAutomationGuardallowed_domains, blocked_domains, allowed_verbs, credential_detection
code_executionCodeExecutionGuardlanguage_allowlist, module_denylist, network_access, max_execution_time_ms
human_in_loopRequireApprovalAbove constraintrequire_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: block and add only the tools your agent needs
  • Step 2: Enable path_allowlist with the narrowest directory tree that covers your workflow
  • Step 3: Add forbidden_paths for sensitive files that might exist inside the allowlist
  • Step 4: Configure egress to allow only the domains your workflow needs, blocking everything else
  • Step 5: Add shell_commands, secret_patterns, patch_integrity, and velocity as 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.

bash
$ chio check --policy ./policy.yaml \
    --tool <tool-name> \
    --params '<json-arguments>'

Test allowed and denied calls for each server tool. Build a checklist:

bash
# 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

Script your 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.

readonly-workspace.yaml
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 (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.

api-only.yaml
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 (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.

development.yaml
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: 60

Production 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.

production.yaml
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: 60

Policy 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 .pem files 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_paths and secret_patterns, then extend it with project-specific rules using extends
  • Version your policies: treat policies as code. Store them in version control, review changes, and tag releases. The metadata section supports governance fields like author, approved_by, and lifecycle_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

Policies define what guards evaluate. Capability tokens define what agents are authorized to invoke. Both must allow a call for it to succeed. See Capabilities for the token side of the equation.

Next Steps