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

FieldTypeDefaultDescription
pathstringrequiredFilesystem path pattern
actionallow | denyallowWhether to allow or deny access
operationslistall opsOperations 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:

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

FieldTypeDefaultDescription
hoststringrequiredHost pattern (domain or IP)
actionallow | denyallowWhether to allow or deny
portslist | all[443]Ports this rule covers
transportstcp | udp | allallTransport 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.com matches api.example.com but not example.com
  • **.example.com matches both api.example.com and example.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

FieldTypeDefaultDescription
pathstringrequiredExecutable path pattern
actionallow | denyallowWhether to allow or deny
subcommandstringSubcommand to match (e.g., push for git push)
argslistArgument 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:

SelectorMatches
anyAny argument
any_flagAny flag (starts with -)
any_optionAny option (flag with a value)
any_positionalAny 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

FieldTypeDefaultDescription
allowlist | allnoneVariables to pass through
denylistVariables to block (only with allow: all)
setdictVariables 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:

VariableExpands To
$HOME or ~User’s home directory
$CWD or .Current working directory
$USERCurrent username
$ASH_SESSION_IDUnique 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:

SyntaxMeaning
1.2.3Compatible updates (caret implied)
^1.2.3Compatible updates (explicit)
~1.2.3Patch-level updates only
=1.2.3Exact version
>=1.0, <2.0Explicit range
*Any version

Local Dependencies

dependencies:
  local:
    - ./shared-policy.yml
    - ../common/base.yml

Dependency Restrictions

Dependencies cannot use:

  • action: deny rules
  • precedence overrides
  • environment.rules.deny
  • environment.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:

  1. More specific rules beat less specific rules
  2. Root policy rules beat dependency rules (at equal specificity)
  3. 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) beats 10.0.0.0/8 (4)

Exec rules:

  • git + subcommand: push + args: [flag: --force] beats git + subcommand: push
  • git + subcommand: push beats git

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