Chio/Docs

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

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

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.

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.

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: 100    # Maximum calls within the window
    window_seconds: 60      # Sliding window duration in seconds
FieldTypeDefaultDescription
enabledbool-Whether this guard is active.
max_invocationsint-Maximum number of tool calls in the window.
window_secondsint-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: 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 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.

bash
$ 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:

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

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: 200
    window_seconds: 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: 100
    window_seconds: 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: 500
    window_seconds: 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: 50
    window_seconds: 60

Policy 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 .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 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_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