Writing Policies
Ash policies are YAML files that define sandbox rules for process execution, filesystem access, network access, and environment variables.
Ash policies are deny-by-default. The only way to allow access to a system resource is to add a rule to allow it, or to add a dependency that does the same.
Policy Structure
# yaml-language-server: $schema=https://ashell.dev/schemas/policy.v1.json schema_version: 1 # Optional: Publishing metadata (required for `ash publish`) publish: name: alice/chowderbot-admin version: 1.0.0 description: Chowderbot admin tool rules authors: ["Alice Adams <alice@chowderbot.test>"] license: MIT # Optional: Dependencies on other policies dependencies: ash/base-macos: ^1.0 ash/python-dev: ^1.2 chowderbot: path: ~/projects/team/policies/chowderbot.yml # Filesystem access rules files: rules: - path: ./** - path: ~/.config/** operations: - read # Network access rules network: rules: - host: admin.chowderbot.test ports: - 443 # Process execution rules exec: rules: - path: git - path: /usr/bin/** # IO device rules io_devices: rules: - class: AppleParavirtDeviceUserClient # Environment variable rules environment: rules: allow: - PATH - HOME - LANG deny: - AWS_* - "*_SECRET" set: EDITOR: vim
File Rules
File rules control read, write, create, delete, and rename access to the filesystem.
Basic Syntax
files: rules: - path: ./** - path: ~/.ssh/** action: deny - path: ~/Documents/** operations: [read]
Fields
| Field | Type | Default | Description |
|---|---|---|---|
path | string | required | Filesystem path pattern |
action | allow | deny | allow | Whether to allow or deny access |
operations | list | all ops | Operations this rule covers |
Operations
Available operations: read, write, create, delete, rename
When operations is omitted, the rule applies to all operations.
files: rules: # Allow all operations in current directory - path: ./** # Only allow reading config files - path: ~/.config/** operations: [read] # Allow creating and writing logs, but not deleting - path: /var/log/myapp/** operations: [create, write]
Path Patterns
Ash supports * (wildcard) and ** (glob) patterns:
| Pattern | Matches |
|---|---|
* | Any single path component |
** | Any number of path components (recursive) |
files: rules: # All files in current directory (recursive) - path: ./** # All files in a specific directory (recursive) - path: ~/projects/myapp/** # All .log files anywhere - path: "*.log" # All files with a specific extension in one directory - path: ~/Downloads/*.pdf # Hidden config directory - path: ~/.config/**
Absolute vs relative paths:
- Absolute paths start with
/and match exactly from root - Relative paths (not starting with
/) are unanchored and match any path prefix
files: rules: # Matches only /usr/bin/python3 - path: /usr/bin/python3 # Matches node_modules anywhere in the filesystem - path: node_modules/**
Network Rules
Network rules control TCP and UDP connections to hosts.
Basic Syntax
network: rules: - host: api.goodagent.ai - host: "*.github.com" ports: [22, 443] - host: evil.example.com action: deny
Fields
| Field | Type | Default | Description |
|---|---|---|---|
host | string | required | Host pattern (domain or IP) |
action | allow | deny | allow | Whether to allow or deny |
ports | list | all | [443] | Ports this rule covers |
transports | tcp | udp | all | all | Transport protocols |
Host Patterns
network: rules: # Exact domain match - host: api.goodagent.ai # Wildcard subdomain (matches foo.example.com, NOT example.com) - host: "*.example.com" # Glob subdomain (matches example.com AND foo.example.com) - host: "**.example.com" # IPv4 address - host: 192.168.1.1 # IPv4 CIDR range - host: 10.0.0.0/8 # IPv6 address - host: "::1" # Localhost - host: localhost
**Difference between * and **:**
*.example.commatchesapi.example.combut notexample.com**.example.commatches bothapi.example.comandexample.com
Ports
network: rules: # Default port is 443 - host: api.example.com # Specific ports - host: "*.github.com" ports: [22, 443] # All ports - host: internal.corp ports: all
Exec Rules
Exec rules control which processes can be executed and with what arguments.
Basic Syntax
exec: rules: - path: git - path: /usr/bin/python3 - path: rm args: - flag: -r - flag: --recursive action: deny
Fields
| Field | Type | Default | Description |
|---|---|---|---|
path | string | required | Executable path pattern |
action | allow | deny | allow | Whether to allow or deny |
subcommand | string | — | Subcommand to match (e.g., push for git push) |
args | list | — | Argument selectors (OR logic) |
Path Patterns
Exec paths follow the same pattern rules as file paths:
exec: rules: # Specific binary by name (matches anywhere) - path: python3 # Absolute path - path: /usr/bin/python3 # All binaries in a directory - path: /usr/local/bin/* # Homebrew binaries (recursive) - path: /opt/homebrew/bin/** # User's cargo binaries - path: ~/.cargo/bin/*
Subcommand Matching
The subcommand field matches literal command tokens that follow the executable:
exec: rules: # Only allow git push - path: git subcommand: push # Multi-word subcommand - path: gh subcommand: workflow run # Docker compose commands - path: docker subcommand: compose up
Subcommands are literal strings only—patterns are not supported.
Argument Selectors
The args field is a list of selectors. A rule matches if any selector matches (OR logic).
String literal selectors:
| Selector | Matches |
|---|---|
any | Any argument |
any_flag | Any flag (starts with -) |
any_option | Any option (flag with a value) |
any_positional | Any positional argument |
Struct selectors:
exec: rules: # Match specific flag - path: rm args: - flag: --force - flag: -f # Match option with any value - path: curl args: - option: --output # Match option with value pattern - path: gh args: - option: --repo value: "myorg/*" # Match positional argument pattern - path: cat args: - positional: /etc/** # Match positional at specific index - path: cp args: - positional: /etc/** index: 0
Flag matching notes:
- Long flags (e.g.,
--force) require exact match - Short flags (e.g.,
-f) also match when bundled (e.g.,-rf)
Exec Rule Examples
exec: rules: # Allow git with no restrictions - path: git # Deny rm with dangerous flags - path: rm args: - flag: -r - flag: -R - flag: --recursive - flag: -f - flag: --force - positional: / - positional: ~ action: deny # Allow rm otherwise - path: rm # Allow git push without flags, deny with flags - path: git subcommand: push - path: git subcommand: push args: - any_flag - any_option action: deny # Allow gh pr only for specific repo - path: gh subcommand: pr args: - option: --repo value: myorg/myrepo - option: -R value: myorg/myrepo
Environment Rules
Environment rules control which variables are visible to sandboxed processes.
Basic Syntax
environment: rules: allow: - PATH - HOME - USER - LANG - "LC_*" deny: - "AWS_*" - "*_SECRET" - "*_KEY" set: EDITOR: vim TERM: xterm-256color
Fields
| Field | Type | Default | Description |
|---|---|---|---|
allow | list | all | none | Variables to pass through |
deny | list | — | Variables to block (only with allow: all) |
set | dict | — | Variables to inject |
Allow Patterns
By default, all environment variables are filtered (blocked). Use allow to pass specific variables:
environment: rules: allow: # Exact match - PATH - HOME # Prefix wildcard - "LC_*" # Suffix wildcard - "*_PATH" # Contains wildcard - "*PYTHON*" # Allow all (use with deny list) # - all
Deny Patterns
The deny field is only valid when allow: all is set:
environment: rules: allow: all deny: - "AWS_*" - "GITHUB_*" - "*_SECRET" - "*_KEY" - "*_TOKEN" - "*PASSWORD*"
Set Variables
Inject variables into the sandbox:
environment: rules: set: EDITOR: vim PAGER: less TERM: xterm-256color
Variable Interpolation
Ash supports these variables in file and exec paths:
| Variable | Expands To |
|---|---|
$HOME or ~ | User's home directory |
$CWD or . | Current working directory |
$USER | Current username |
$ASH_SESSION_ID | Unique sandbox session ID |
files: rules: - path: ./** # Current directory - path: $CWD/** # Equivalent to above - path: ~/.config/** # User's config directory - path: $HOME/.config/** # Equivalent to above - path: /tmp/$ASH_SESSION_ID/** # Session-specific temp directory exec: rules: - path: ~/go/bin/* # User's Go binaries
Dependencies
Policies can depend on other policies from the registry or local files.
Registry Dependencies
dependencies: ash/base-macos: "1" # Any 1.x version ash/python-dev: "1.2" # Any 1.2.x version ash/pytorch-dev: "=2.4.1" # Exact version
SemVer requirement syntax:
| Syntax | Meaning |
|---|---|
1.2.3 | Compatible updates (caret implied) |
^1.2.3 | Compatible updates (explicit) |
~1.2.3 | Patch-level updates only |
=1.2.3 | Exact version |
>=1.0, <2.0 | Explicit range |
* | Any version |
Local Dependencies
dependencies: shared: path: ./shared-policy.yml base: path: ../common/base.yml
Dependency Restrictions
Dependencies cannot use:
action: denyrulesprecedenceoverridesenvironment.rules.denyenvironment.rules.allow: all- Catch-all patterns (
/**,*)
These restrictions ensure that dependencies may only grant specific, purpose-based capabilities.
Rule Precedence
When multiple rules match a request, Ash uses precedence to determine the outcome:
- More specific rules beat less specific rules
- Root policy rules beat dependency rules (at equal specificity)
- Deny beats allow (at equal precedence)
Specificity Examples
File paths:
/usr/bin/python3(30) beats/usr/bin/**(21)~/.ssh/**(23) beats~/**(11)
Network hosts:
api.github.com(19) beats*.github.com(4)192.168.1.0/24(12) beats10.0.0.0/8(4)
Exec rules:
git+subcommand: push+args: [flag: --force]beatsgit+subcommand: pushgit+subcommand: pushbeatsgit
Overriding Dependencies
Your root policy always takes precedence over dependencies at equal specificity:
# Dependency allows ~/.config/** # Root policy can deny specific paths: files: rules: - path: ~/.config/sensitive/** action: deny
Best Practices
Start with Dependencies
Use existing policies as a foundation:
dependencies: ash/base-macos: "1" ash/js-dev: "1"
Deny Sensitive Paths
Explicitly deny access to sensitive locations:
files: rules: - path: ~/.ssh/** action: deny - path: ~/.gnupg/** action: deny - path: ~/.aws/** action: deny - path: "**/.env" action: deny
Use Specific Network Rules
Whitelist specific hosts rather than allowing broad access:
network: rules: - host: api.goodagent.ai - host: registry.npmjs.org - host: "**.github.com" ports: [22, 443]
Filter Environment Variables
Don't pass secrets to the sandbox:
environment: rules: allow: - PATH - HOME - USER - LANG - "LC_*" - TERM # Implicitly omitted: AWS_*, GITHUB_TOKEN, *_SECRET, etc.
Use Observe Mode First
Run in observe mode to understand what access an agent needs:
ash observe -- your-agent
Review the logs and build your policy based on actual requirements.
Check For Correctness
# Check policy syntax ash check --policy policy.yml # Expand and view resolved policy ash expand --policy policy.yml # Test specific decisions ash test file read /etc/passwd ash test network api.example.com:443 ash test exec /usr/bin/curl