earl

Security Model

How earl isolates AI agents from secrets, enforces network policies, sandboxes code execution, and blocks SSRF attacks

Earl is designed so that AI agents can call APIs without ever seeing credentials or making unconstrained network requests. The security model has five independent layers that compose for defense in depth.

Template-Driven Access Control

Agents never make raw HTTP requests. Every API interaction must match a pre-authored HCL template that declares exactly:

  • The URL pattern and HTTP method
  • Required and optional parameters with types
  • Which secrets the template can access (by key reference)
  • Whether the operation is read-only or write (annotations.mode)
# This works -- matches the github.search_issues template
earl call github.search_issues --query "is:issue" --per_page 5

# There is no "raw request" command -- arbitrary URLs are impossible

Write-mode commands require explicit confirmation. At the CLI, users must pass --yes or type YES at the interactive prompt. The MCP server has a matching --yes flag for automated environments.

Keychain-Backed Secrets

Secret values are stored in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager). They are never written to disk in plaintext.

  • Templates reference secrets by key name during request rendering (e.g., secrets.github.token).
  • The earl secrets get command returns metadata only (key name, created/updated timestamps) -- never the plaintext value.
  • Output templates (result.output) have no access to the secrets.* namespace.

An AI agent with full shell access to the earl binary still cannot read your secret values. See Secrets for the complete safety model.

Network Egress Rules

By default, earl allows outbound HTTP/HTTPS requests to any public host. You can restrict egress to specific destinations using [[network.allow]] rules in config.toml:

[[network.allow]]
scheme = "https"
host = "api.github.com"
port = 443
path_prefix = "/"

[[network.allow]]
scheme = "https"
host = "api.stripe.com"
port = 443
path_prefix = "/v1"

When any [[network.allow]] entry is present, every outbound request must match at least one rule. A rule matches when all four fields match: scheme (case-insensitive), host (case-insensitive), port (including default ports like 443 for HTTPS), and path prefix. Requests to unlisted destinations are blocked.

SSRF Protection

Earl blocks requests to private, reserved, and metadata IP addresses to prevent Server-Side Request Forgery attacks. Validation happens after DNS resolution, so a public hostname that resolves to a private IP is still blocked.

Blocked IPv4 Ranges

RangeDescription
10.0.0.0/8RFC 1918 private
172.16.0.0/12RFC 1918 private
192.168.0.0/16RFC 1918 private
127.0.0.0/8Loopback
169.254.0.0/16Link-local
224.0.0.0/4Multicast
255.255.255.255Broadcast
0.0.0.0Unspecified
240.0.0.0/4Reserved (former Class E)
198.51.100.0/24, 203.0.113.0/24, 192.0.2.0/24Documentation (RFC 5737)
100.64.0.0/10Shared address space (RFC 6598)
198.18.0.0/15Benchmarking (RFC 2544)
192.0.0.0/24IETF protocol assignments
169.254.169.254Cloud instance metadata endpoint
100.100.100.200Cloud metadata (Alibaba)

Blocked IPv6 Addresses

RangeDescription
::1Loopback
::Unspecified
ff00::/8Multicast
fe80::/10Link-local
fc00::/7Unique local
fec0::/10Site-local (deprecated)
2001:db8::/32Documentation
fd00:ec2::254AWS EC2 IPv6 metadata
::ffff:169.254.169.254IPv4-mapped metadata

IPv6-mapped IPv4 addresses (e.g., ::ffff:127.0.0.1) are recursively checked against the IPv4 blocklist.

OAuth Endpoint Validation

OAuth endpoint URLs (token_url, authorization_url, device_authorization_url, and OIDC issuer URLs) are validated with the same SSRF checks. Additionally, only https URLs are permitted -- except http to loopback addresses (127.0.0.1, ::1, localhost) for local development.

Execution Sandboxing

Bash and SQL protocols run in sandboxed environments to limit what agent-invoked operations can do.

Bash Sandbox

Every Bash script runs inside an OS-level sandbox. Earl requires a sandbox tool to be installed:

On Linux, earl uses bubblewrap (bwrap) with:

  • Targeted read-only mounts of /usr, /bin, /sbin, /lib, /lib64, /etc (not the entire root filesystem)
  • /tmp as tmpfs -- an isolated temporary directory
  • /dev and /proc mounted for basic process operation
  • PID namespace isolation (--unshare-pid)
  • IPC namespace isolation (--unshare-ipc)
  • UTS namespace isolation (--unshare-uts)
  • Network isolation (--unshare-net) by default -- templates must explicitly set sandbox.network = true to allow network access
  • --die-with-parent ensures child processes are killed if earl exits
  • Working directory is mounted read-only; only paths listed in sandbox.writable_paths get read-write mounts

On macOS, earl uses sandbox-exec with a dynamically generated Seatbelt profile:

  • deny-default policy -- everything is denied unless explicitly allowed
  • Network denied by default -- templates must set sandbox.network = true to allow network* operations
  • Process execution allowed (process-exec, process-fork)
  • File reads allowed system-wide (required for system libraries and dyld shared cache)
  • File writes denied except for /dev/null, /dev/tty, and paths listed in sandbox.writable_paths
  • Mach IPC scoped to a curated list of system services (com.apple.system.logger, com.apple.SecurityServer, etc.)
  • sysctl-read allowed for basic system queries

Templates can configure sandbox behavior per-command:

operation {
  bash {
    script = "curl -s https://api.example.com/data"
    sandbox {
      network      = true           # Allow network access (default: false)
      writable_paths = ["output"]   # Paths relative to cwd that can be written
      max_time_ms  = 30000          # Kill script after 30 seconds
      max_output_bytes = 1048576    # Limit stdout/stderr to 1 MB
    }
  }
}

If neither sandbox.max_time_ms nor sandbox.max_output_bytes is set, the transport-level timeout_ms and max_response_bytes apply as fallbacks.

The sandbox kills the entire process group on timeout using SIGKILL via killpg(), preventing child process leaks.

SQL Sandbox

SQL queries execute through parameterized queries with configurable safety controls:

  • Read-only mode (sandbox.read_only = true) enforces read-only at the database level:
    • PostgreSQL: Wraps the query in BEGIN READ ONLY / ROLLBACK
    • MySQL: Uses START TRANSACTION READ ONLY / ROLLBACK
    • SQLite: Appends mode=ro to the connection URL
  • Row limits (sandbox.max_rows) cap the number of returned rows
  • Timeouts (sandbox.max_time_ms or transport-level timeout_ms) abort long-running queries
  • Connection credentials are stored in the OS keychain and referenced by secret key (connection_secret) -- never embedded in templates
operation {
  sql {
    connection_secret = "mydb.connection_url"
    query = "SELECT id, name FROM users WHERE active = ?"
    params = [true]
    sandbox {
      read_only = true
      max_rows  = 100
    }
  }
}

SQL error messages are passed through the redactor before being shown to the user, preventing accidental credential leakage in error output.

Secret Redaction

If a secret value appears anywhere in command output -- for example, an API that echoes a token in its response -- earl's redactor replaces every occurrence with [REDACTED] before displaying the result.

The redactor detects secrets in multiple encodings:

  • Raw -- the literal secret string
  • Base64 (standard and URL-safe)
  • Hex (lowercase and uppercase)
  • URL-encoded (%-encoded form)

For each secret with 6 or more characters, all encoding variants are generated and searched (six forms total: raw plus five encodings). Longer matches are replaced first to avoid partial replacements.

Redaction applies to both human-readable output and --json structured output (the redactor walks the entire JSON value tree and redacts every string field).

File Permissions

The security model assumes that AI agents cannot modify earl's configuration or template files. If an agent can edit config.toml, it can add permissive network rules. If it can edit templates, it can exfiltrate secrets.

Lock down file permissions after setup:

# Protect config directory
chmod 700 ~/.config/earl
chmod 600 ~/.config/earl/config.toml
chmod -R go-w ~/.config/earl/templates

# Protect workspace templates
chmod -R 700 ./templates/

The secrets metadata index at ~/.local/state/earl/secrets-index.json is written with 0600 permissions on Unix systems (owner read-write only). It stores only key names and timestamps, never secret values.

For stronger isolation, run the AI agent as a separate, less-privileged user that can execute earl but cannot modify its configuration files.

On this page