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: my-policy
version: "1.0.0"
description: A custom policy for my workflow
authors: ["Your Name <you@example.com>"]
license: MIT
# Optional: Dependencies on other policies
dependencies:
base-macos: "1"
python-dev: "1.2"
local:
- ./shared-policy.yml
# Filesystem access rules
files:
rules:
- path: ./**
- path: ~/.config/**
operations: [read]
# Network access rules
network:
rules:
- host: api.goodagent.ai
# Process execution rules
exec:
rules:
- path: git
- path: /usr/bin/**
# 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:
base-macos: "1" # Any 1.x version
python-dev: "1.2" # Any 1.2.x version
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:
local:
- ./shared-policy.yml
- ../common/base.yml
Dependency Restrictions
Dependencies cannot use:
action: denyrulesprecedenceoverridesenvironment.rules.denyenvironment.rules.allow: all- Catch-all patterns (
/**,*)
This ensures dependencies 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:
base-macos: "1"
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.
Validating Policies
# 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