Chio/Docs
LOGIN · JOIN

BuildPolicy

Write a Policy

HushSpec policies define accessible tools, filesystem paths, network egress, and agent rate limits.


Prerequisites

  • The chio binary 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-scopes prints them for an MCP server; see Wrap an MCP Server.
  • A writable directory for two SQLite files. chio check refuses 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:.

policy.yamlyaml
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. 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.

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

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.

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 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_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. Every capture in this section comes from running it against the policy.yaml at the top of this page.

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

policy-checklist · no-dbtranscript
$ 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.
exit 1

Supply only --session-db and the command runs, but every verdict is a deny because there is no receipt store to write to.

policy-checklist · receipt-db-missingtranscript
$ 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
exit 2deny

With both flags, the allow case reads a workspace file that tool_access permits and forbidden_paths does not match:

policy-checklist · allowtranscript
$ 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
exit 0allow

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:

policy-checklist · deny-tooltranscript
$ 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
exit 2deny

A path matching forbidden_paths does reach the guards, and one of them denies it:

policy-checklist · deny-pathtranscript
$ 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
exit 2deny

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:

policy-checklist · evidencetranscript
$ 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"]}
exit 0

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:

policy-checklist · denylist-gaptranscript
$ 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
exit 0allow

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:

policy-checklist · session-reusetranscript
$ 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
exit 2deny

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

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

policy-checklist · unknown-blocktranscript
$ chio policy analyze ./bad-block.yaml
policy 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
exit 2

A field that is not one of that block's:

policy-checklist · unknown-fieldtranscript
$ chio policy analyze ./bad-field.yaml
policy 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
exit 2

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 seeWhat it meansFix
durable admission mode requires a databaseNo database flags.Pass both --session-db and --receipt-db.
durable receipt persistence unavailableOnly --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 operationThe session database has already served a check.Use a fresh --session-db path per check.
preflight cannot evaluate post-output guardsThe 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 evidencechio 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.

readonly-workspace.yamlyaml
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.yamlyaml
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.yamlyaml
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.yamlyaml
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