---
title: "Features"
description: "Execution-layer features of agentsh — traffic steering, database proxying, data protection, deterministic enforcement, and operations."
doc_version: "1.0"
last_updated: "2026-05-29"
canonical: "https://www.agentsh.org/docs/features/"
---

# Features

Execution-layer capabilities of agentsh — traffic control, data protection, deterministic enforcement, and operational tools.

## Steering

Most systems can *deny* an action. agentsh can also **steer** it to an approved alternative.

When an agent tries the wrong approach (or brute-force workarounds), policy can steer it to the right path—keeping the agent productive and reducing wasted retries. The technical action is called `redirect` in policy files.

#### Steer npm to internal registry

```yaml
dns_redirects:
  - name: npm-mirror-dns
    match: "^registry\\.npmjs\\.org$"
    resolve_to: "10.0.1.50"

connect_redirects:
  - name: npm-mirror-connect
    match: "registry\\.npmjs\\.org:443"
    redirect_to: "npm.internal.corp:443"
    message: "Steered to internal registry"
```

#### Steer curl to an audited wrapper

```yaml
command_rules:
  - name: redirect-curl
    commands: [curl, wget]
    decision: redirect
    message: "Downloads routed through audited fetch"
    redirect_to:
      command: agentsh-fetch
      args: ["--audit"]
```

#### Steer writes outside workspace

```yaml
file_rules:
  - name: redirect-outside-writes
    paths: ["/home/**", "/tmp/**"]
    operations: [write, create]
    decision: redirect
    redirect_to: "/workspace/.scratch"
    message: "Writes outside workspace redirected to /workspace/.scratch"
```

#### Discourage unnecessary deletions

Instead of blocking destructive commands outright, steer the agent away with guidance:

```yaml
command_rules:
  - name: discourage-rm
    commands: [rm, rmdir]
    args_patterns: ["(-rf?|--recursive)"]
    decision: redirect
    redirect_to:
      command: echo
      args: ["Skipped: deletion not needed. Files can remain; they don't affect the build or tests."]
    message: "Cleanup is unnecessary—focus on the task instead of removing files."
```

The agent sees a successful operation (not an error), receives the guidance message, and moves on without wasting retries.

#### Prevent bypassing merge workflow

Steer agents away from shortcuts that bypass proper code review and merge workflows:

```yaml
command_rules:
  - name: no-direct-push-to-main
    commands: [git]
    args_patterns: ["push.*(origin\\s+)?(main|master)"]
    decision: redirect
    redirect_to:
      command: echo
      args: ["Push declined. Create a branch and open a pull request instead."]
    message: "Direct pushes to main/master are not allowed. Use feature branches and pull requests."

  - name: no-force-push
    commands: [git]
    args_patterns: ["push.*(--force|-f)"]
    decision: redirect
    redirect_to:
      command: echo
      args: ["Force push blocked. Resolve conflicts through proper merge."]
    message: "Force pushing is not allowed—merge changes through the standard workflow."

  - name: no-hard-reset
    commands: [git]
    args_patterns: ["reset.*(--hard)"]
    decision: redirect
    redirect_to:
      command: echo
      args: ["Hard reset blocked. Use git stash or git checkout to preserve changes."]
    message: "git reset --hard discards uncommitted work. Use non-destructive alternatives."

  - name: no-git-clean
    commands: [git]
    args_patterns: ["clean.*(-f|--force)"]
    decision: redirect
    redirect_to:
      command: echo
      args: ["git clean blocked. Untracked files may be needed—review before deleting."]
    message: "git clean -fd permanently deletes untracked files. Review them manually first."
```

The agent receives guidance to use the proper workflow instead of attempting workarounds.

### DNS & Connect Redirects

For transparent network-level routing without application changes, agentsh intercepts DNS resolution and TCP connections at the syscall level. This enables scenarios like routing API calls through internal proxies or gateways.

        Process-scoped, not system-wide

DNS and connect redirects only affect processes running under agentsh—not the entire system. This makes it safe to run alongside other applications.

#### DNS Redirects

Intercept DNS resolution and return alternative IP addresses based on hostname patterns.

```yaml
dns_redirects:
  # Route Anthropic API through internal proxy
  - name: anthropic-to-vertex
    match: ".*\\.anthropic\\.com"  # Regex pattern
    resolve_to: "10.0.0.50"       # IP to return
    visibility: audit_only
    on_failure: fail_closed
```

**Fields:**

- `match`: Regex pattern for hostname (full string match)

- `resolve_to`: IP address to return (IPv4 or IPv6)

- `visibility`: `silent`, `audit_only` (default), or `warn`

- `on_failure`: `fail_closed` (default), `fail_open`, or `retry_original`

#### Connect Redirects

Intercept TCP connections and route them to different destinations. Supports TLS/SNI rewriting for MITM proxy scenarios.

```yaml
connect_redirects:
  # Route API calls through corporate proxy
  - name: api-to-proxy
    match: "api\\.anthropic\\.com:443"  # Regex for host:port
    redirect_to: "vertex-proxy.internal:443"
    tls:
      mode: passthrough  # or rewrite_sni
    visibility: warn
    message: "Routed through Vertex AI proxy"
    on_failure: fail_closed
```

**Fields:**

- `match`: Regex pattern for `host:port` (or just host for any port)

- `redirect_to`: Destination `host:port`

- `tls.mode`: `passthrough` (keep original SNI) or `rewrite_sni`

- `tls.sni`: Required if mode is `rewrite_sni`—the SNI to use in ClientHello

- `visibility`, `message`, `on_failure`: Same as DNS redirects

#### SNI Rewriting

When routing through a MITM proxy that presents its own certificate, use SNI rewriting:

```yaml
connect_redirects:
  - name: tls-mitm
    match: ".*:443"
    redirect_to: "proxy.internal:8443"
    tls:
      mode: rewrite_sni
      sni: "proxy.internal"  # SNI sent to proxy
```

#### Combined Example

Route Anthropic API calls through a Vertex AI proxy with DNS and connect redirects working together:

```yaml
# First, resolve anthropic.com to proxy IP
dns_redirects:
  - name: anthropic-dns
    match: ".*\\.anthropic\\.com"
    resolve_to: "10.0.0.50"
    visibility: audit_only

# Then, redirect the connection to proxy port
connect_redirects:
  - name: anthropic-connect
    match: "api\\.anthropic\\.com:443"
    redirect_to: "vertex-proxy.internal:443"
    tls:
      mode: passthrough
    visibility: warn
    message: "API call routed through Vertex AI"
```

#### Visibility & Failure Options

| Visibility | Behavior |
| --- | --- |
| `silent` | No event, no stderr output |
| `audit_only` | Event logged to audit system only (default) |
| `warn` | Event logged + message shown to stderr |

| On Failure | Behavior |
| --- | --- |
| `fail_closed` | Return error to application (default) |
| `fail_open` | Retry connection to original destination |
| `retry_original` | Same as `fail_open` |

#### Platform Support

DNS and connect redirect enforcement depends on the active backend:

- **Linux:** transparent DNS/connect interception through the Linux network monitor, ptrace path, or eBPF-backed enforcement depending on configuration

- **macOS:** policy parsing and audit support; transparent DNS/connect rewriting is not currently the primary macOS path

- **Windows:** WinDivert packet filtering with policy-aware NAT for connect redirects

## Network ACLs

Process-level network access control lists provide fine-grained control over which processes can make which network connections. ACL rules are evaluated per-process based on the executable name.

```bash
# Show active network ACL rules
agentsh network-acl list --json

# Add a rule allowing curl to reach an API
agentsh network-acl add curl api.example.com --port 443 --decision allow

# Test what decision would be made for a connection
agentsh network-acl test node registry.npmjs.org --port 443

# Remove a rule by index
agentsh network-acl remove 3 --process curl

# Watch live connection attempts
agentsh network-acl watch --json

# Learn network patterns for policy generation
agentsh network-acl learn --process node --duration 1h --output learned-rules.yaml
```

The `network-acl` command is also available as `nacl` or `pnacl` for convenience.

## LLM Proxy & DLP

agentsh includes an embedded proxy that intercepts all LLM API requests from agents.

- **Automatic routing:** Sets `ANTHROPIC_BASE_URL` and `OPENAI_BASE_URL` so SDKs route through the proxy

- **Custom providers:** Route to LiteLLM, Azure OpenAI, vLLM, or corporate gateways

- **DLP redaction:** PII (emails, phone numbers, API keys) is redacted before reaching providers

- **Usage tracking:** Token counts extracted and logged for cost attribution

```yaml
proxy:
  mode: embedded
  providers:
    anthropic: https://api.anthropic.com
    openai: https://api.openai.com

dlp:
  mode: redact
  patterns:
    email: true
    api_keys: true
  custom_patterns:
    - name: customer_id
      display: identifier
      regex: "CUST-[0-9]{8}"
```

### Rate Limiting

Control how fast agents can call LLM APIs with token-bucket rate limiters for both request count and token consumption.

```text
proxy:
  rate_limits:
    enabled: true
    requests_per_minute: 60    # RPM limit
    request_burst: 10           # Burst allowance (default: RPM/6)
    tokens_per_minute: 90000   # TPM limit
    token_burst: 15000          # Token burst (default: TPM/6)
```

Requests that exceed the rate limit receive an HTTP `429 Too Many Requests` response. Token consumption is tracked post-response — the actual token count from the provider response is deducted from the budget. A concurrency limiter (max 4 in-flight requests when TPM is enabled) bounds worst-case overspend.

## HTTP Service Gateway

agentsh proxies an agent's HTTP traffic to third-party APIs through a local gateway that enforces path-level and method-level access rules, substitutes credentials on the wire, and prevents credential exfiltration. Declare a service in your policy, and agentsh handles routing, filtering, and secret management in a single configuration block.

```text
# Allow the agent to read repo contents and manage issues on GitHub.
# The real token is fetched from Vault; the agent only sees a fake.
http_services:
  - name: github
    upstream: https://api.github.com
    secret:
      ref: vault://kv/github#token
      format: "ghp_{rand:36}"
    inject:
      header:
        name: Authorization
        template: "Bearer {{secret}}"
    default: deny
    rules:
      - name: read-contents
        methods: [GET]
        paths: ["/repos/myorg/*/contents/**"]
        decision: allow
      - name: manage-issues
        methods: [GET, POST, PATCH]
        paths: ["/repos/myorg/*/issues", "/repos/myorg/*/issues/*"]
        decision: allow
```

Key properties:

- **Fail-closed by default.** When `allow_direct: false` (the default), agentsh blocks direct connections to the upstream host at the network layer. The agent can *only* reach the API through the gateway.

- **Path and method filtering.** Rules use [glob patterns](https://github.com/gobwas/glob) on the request path and restrict by HTTP method. First matching rule wins.

- **Credential isolation.** The agent receives a format-matched fake token (e.g., `ghp_aB3xZk9...`). agentsh swaps the fake for the real credential on the wire and scrubs it from responses. A [leak guard](https://www.agentsh.org/docs/policy-reference/#http-services-leak-guard) blocks any attempt to exfiltrate the fake to a different host.

For the full schema, credential substitution model, and worked examples, see [Policy Reference → HTTP Services](https://www.agentsh.org/docs/policy-reference/#http-services).

### Cooperative vs. Non-Cooperative Services

How the agent's traffic reaches the gateway depends on whether the service's SDK supports a custom base URL.

#### Cooperative services

A cooperative service is any API whose SDK or CLI lets you override the base URL via an environment variable, constructor parameter, or config file. agentsh sets the appropriate variable automatically (e.g., `GITHUB_API_URL=http://127.0.0.1:<port>/svc/github/`), so the SDK routes all traffic through the gateway without code changes. This is the cleanest integration path.

| Category | Service | Base URL override |
| --- | --- | --- |
| **Source control** | GitHub | `GITHUB_API_URL` |
| GitLab | `GITLAB_URL` / self-hosted base URL |  |
| **Payments** | Stripe | `stripe.api_base` (SDK constructor) |
| **Cloud** | AWS | `AWS_ENDPOINT_URL`, per-service `AWS_ENDPOINT_URL_<SVC>` |
| Google Cloud | `CLOUDSDK_API_ENDPOINT_OVERRIDES_<SVC>` |  |
| Azure | `AZURE_AUTHORITY_HOST` / per-service endpoint config |  |
| **Communication** | Slack | `base_url` (SDK constructor) |
| PagerDuty | `base_url` (SDK constructor) |  |
| **Infrastructure** | HashiCorp Vault | `VAULT_ADDR` |
| Kubernetes | `--server` flag / kubeconfig `clusters[].cluster.server` |  |
| **Observability** | Datadog | `DD_DD_URL`, `DD_SITE` |
| Sentry | Host encoded in `SENTRY_DSN` |  |
| Elasticsearch | `ELASTICSEARCH_URL` |  |
| **Package registries** | npm | `NPM_CONFIG_REGISTRY` |
| pip / PyPI | `PIP_INDEX_URL` |  |
| Cargo / crates.io | `CARGO_REGISTRIES_<NAME>_INDEX` |  |

#### Non-cooperative services

For services that do not support a base URL override, agentsh uses fail-closed network enforcement. When a service is declared in `http_services:` with `allow_direct: false` (the default), agentsh blocks any direct connection to the upstream host at the network monitor level. The agent receives a 403 with a message directing it to use the gateway URL.

For SDKs that cannot change their base URL, keep direct access blocked and use a small adapter or wrapper that calls the gateway URL exposed for the service (for example `STRIPE_API_URL=http://127.0.0.1:<port>/svc/stripe`). DNS and connect redirects can steer TCP to such an adapter, but `http_services` itself expects requests on the `/svc/<name>/...` gateway path.

```yaml
http_services:
  - name: stripe
    upstream: https://api.stripe.com
    allow_direct: false
    expose_as: STRIPE_API_URL
    default: deny
    rules:
      - name: read-customers
        methods: [GET]
        paths: ["/v1/customers", "/v1/customers/*"]
        decision: allow
```

## Database Proxy

agentsh can route declared Postgres-family database traffic through a per-session proxy. The proxy evaluates connection policy during startup, classifies SQL statements into effects, enforces strict per-object statement rules, applies optional `require_where` guards for scoped mutations, and emits database audit events.

```yaml
db_services:
  appdb:
    family: postgres
    dialect: postgres
    upstream: pg.internal:5432
    tls_mode: terminate_reissue

database_connection_rules:
  - name: allow-app-user
    db_service: appdb
    db_user: ["app"]
    database: app
    decision: allow

database_rules:
  - name: deny-mutations
    db_service: appdb
    operations: [write, modify, delete]
    decision: deny

  - name: allow-resolved-reads
    db_service: appdb
    operations: [read]
    relations: ["public.users", "public.orders"]
    match_object_resolution: catalog_resolved
    decision: allow

policies:
  db:
    unavoidability: enforce
    log_statements: parameters_redacted
```

Current runtime support is Postgres-family only: PostgreSQL and Aurora Postgres are the supported targets; Redshift and CockroachDB use the same path with beta coverage. Statement rules require terminate-mode TLS. Passthrough services can still use connection-level rules, but the proxy cannot inspect SQL inside the encrypted stream.

For mutation allowlists, add `require_where: true` to `modify` and `delete` rules that should only match top-level `UPDATE` or `DELETE` statements with a top-level `WHERE`. This is a syntactic guard; pair it with narrow relation selectors and avoid overlapping unguarded mutation rules.

When `policies.db.unavoidability` is `observe` or `enforce`, agentsh generates the routing and bypass-prevention bundle for declared services. That bundle uses `connect_redirects` with `redirect_to_unix` to route agent connects to the proxy, denies direct egress to protected database destinations, blocks common local Postgres Unix sockets, and emits `db_bypass_attempt` events for denied direct access.

For the full schema, redirect behavior, audit event shape, and current limitations, see [Database Proxy](https://www.agentsh.org/docs/database-proxy/) and [Policy Reference → Database Access](https://www.agentsh.org/docs/policy-reference/#database-access).

## Workspace Checkpoints

Create snapshots of workspace state for recovery from destructive operations:

```bash
# Create a checkpoint before risky operations
agentsh checkpoint create --session $SID --workspace /workspace --reason "before cleanup"

# List checkpoints for a session
agentsh checkpoint list --session $SID

# Show what changed since a checkpoint
agentsh checkpoint show <cp-id> --session $SID --workspace /workspace --diff

# Preview what rollback would restore (dry-run)
agentsh checkpoint rollback <cp-id> --session $SID --workspace /workspace --dry-run

# Restore workspace to checkpoint state
agentsh checkpoint rollback <cp-id> --session $SID --workspace /workspace

# Clean up old checkpoints
agentsh checkpoint purge --session $SID --older-than 24h --keep 5
```

All commands accept `--storage-dir` to override the default checkpoint storage location. The `list` and `show` commands support `--json` for machine-readable output.

        Auto-checkpoint

When enabled, agentsh automatically creates checkpoints before risky commands (`rm`, `mv`, `git reset`, `git checkout`, `git clean`, `git stash`). Configure in `sessions.checkpoints.auto_checkpoint`. Snapshots use copy-on-write with SHA-256 hash verification on restore. Individual files over 100 MB are skipped.

## Trash & Soft Delete

When a policy uses the `soft_delete` decision, files are diverted to a trash directory instead of being permanently deleted. Soft-delete is supported across both the **FUSE** and **ptrace** enforcement backends—in ptrace mode, `unlinkat` calls are intercepted and replaced with `mkdirat` + `renameat2` syscall injection to atomically move files to trash. Manage diverted files with the `trash` command:

```bash
# List diverted items
agentsh trash list --session $SID

# Restore a diverted item by token
agentsh trash restore <token>

# Restore to a different location
agentsh trash restore <token> --dest /workspace/recovered.txt --force

# Purge old trash entries
agentsh trash purge --ttl 7d --quota 5GB
```

## Backup & Restore

Create and restore backups of agentsh data including configuration, policies, sessions, and audit logs:

```bash
# Create a backup
agentsh backup -o agentsh-backup.tar.gz --verify

# Restore from backup (dry-run first)
agentsh restore -i agentsh-backup.tar.gz --dry-run

# Restore and verify
agentsh restore -i agentsh-backup.tar.gz --verify
```

## Process Taint Tracking

agentsh tracks process ancestry to determine which processes are “tainted” (spawned by or descended from an AI agent). Use the `taint` command to inspect and debug taint propagation:

```bash
# List all tainted processes
agentsh taint list --json

# Show taint details for a specific process
agentsh taint show <pid>

# Show full ancestry trace
agentsh taint trace <pid>

# Stream taint events in real-time
agentsh taint watch --agent-only

# Simulate taint evaluation for testing
agentsh taint simulate --ancestry bash,node,claude --command curl --args "https://example.com"
```

## Authentication

agentsh supports multiple authentication methods:

| Type | Use Case |
| --- | --- |
| `api_key` | Simple deployments with static keys |
| `oidc` | Enterprise SSO (Okta, Azure AD, etc.) |
| `hybrid` | Both methods accepted |

**Approval modes** for human-in-the-loop verification:

- `local_tty` - Terminal prompt (default)

- `totp` - Authenticator app codes

- `webauthn` - Hardware security keys (YubiKey)

- `api` - Remote approval via REST

### OIDC Configuration

When using OIDC, the server discovers the provider's endpoints at startup. The `discovery_timeout` field controls how long to wait for the OIDC discovery request (default: `5s`). Increase this if your identity provider is slow to respond, or decrease it to fail fast in environments where the provider may be unreachable.

```text
auth:
  type: oidc
  oidc:
    issuer: https://login.example.com
    audience: agentsh
    discovery_timeout: 5s   # default; accepts Go duration format
```

### WebAuthn Credentials

Manage hardware security key credentials for the `webauthn` approval mode:

```bash
# Register a new security key
agentsh auth webauthn register --name "YubiKey 5"

# List registered credentials
agentsh auth webauthn list

# Delete a credential
agentsh auth webauthn delete --credential-id <base64-id>
```

## Threat Intelligence Feeds

agentsh can block network connections to known-malicious domains using external threat intelligence feeds and local blocklists. When a DNS query or connection attempt matches a threat feed entry, the configured action (deny or audit) is applied and recorded with the feed name and matched domain in the event metadata.

        How it works

Feeds are downloaded at startup and periodically refreshed in the background. The on-disk cache uses ETag-based HTTP caching to minimize bandwidth. Parent-domain matching is supported—blocking `evil.com` also blocks `sub.evil.com`. An allowlist lets you override specific domains.

### Configuration

```text
threat_feeds:
  enabled: true
  action: deny              # deny or audit
  feeds:
    - name: urlhaus
      url: https://urlhaus.abuse.ch/downloads/hostfile/
      format: hostfile        # hostfile or domain-list
    - name: custom-blocklist
      url: https://internal.corp/threat-domains.txt
      format: domain-list
  local_lists:
    - /etc/agentsh/local-blocklist.txt
  allowlist:
    - safe.example.com
  sync_interval: 6h         # background refresh interval (default: 6h)
  cache_dir: ""             # on-disk cache directory
```

| Field | Default | Description |
| --- | --- | --- |
| `enabled` | `false` | Enable threat feed checking |
| `action` | `deny` | `deny` blocks the connection; `audit` logs but permits |
| `feeds[].name` | — | Unique feed identifier (alphanumeric, dots, hyphens, underscores) |
| `feeds[].url` | — | Feed URL (http:// or https://) |
| `feeds[].format` | — | `hostfile` (127.0.0.1 domain) or `domain-list` (one per line) |
| `local_lists` | `[]` | Local file paths containing domains to block (one per line) |
| `allowlist` | `[]` | Domains that override threat feed matches (always permitted) |
| `sync_interval` | `6h` | How often to refresh remote feeds |

### Event Metadata

When a DNS query or network connection matches a threat feed, the event's policy info includes threat-specific fields:

```text
{
  "type": "dns_query",
  "domain": "malware.evil.com",
  "policy": {
    "decision": "deny",
    "threat_feed": "urlhaus",
    "threat_match": "evil.com",
    "threat_action": "deny"
  }
}
```

## Package Install Security

agentsh intercepts package manager commands (`npm install`, `pip install`, `pnpm add`, `yarn add`, `uv pip install`, `poetry add`) and evaluates their dependency trees against multiple security providers before the install proceeds. A policy-based rule engine determines whether to allow, warn, require approval, or block each package.

        Fail-closed by default

If no policy rule matches a finding, the default verdict is **block**. This ensures that unrecognized threats are not silently permitted. Configure explicit allow rules for packages you trust.

### Providers

Providers run in parallel and return findings that the policy engine evaluates. Each provider can be independently enabled, configured with timeouts, and assigned a failure action.

| Provider | Finding Types | API Key Required | Description |
| --- | --- | --- | --- |
| `osv` | vulnerability | No | OpenSSF Vulnerability Database with CVSS v3 scoring |
| `depsdev` | license, reputation | No | deps.dev for license metadata and OpenSSF Scorecard scores |
| `local` | license | No | Offline license metadata scanning (zero network calls) |
| `socket` | malware, reputation | Yes | Socket.dev for supply-chain malware and typosquatting detection |
| `snyk` | vulnerability, license | Yes | Snyk vulnerability and license scanning |
| `exec` | *any* | — | Custom external checker via stdin/stdout JSON protocol |

### Policy Rules

Package rules use first-match-wins evaluation. Each rule matches on package name, finding type, severity, ecosystem, reason codes, or SPDX license identifiers.

```text
package_rules:
  # Allow known internal packages
  - match:
      name_patterns: ["@acme/*"]
    action: allow

  # Block critical vulnerabilities
  - match:
      finding_type: vulnerability
      severity: critical
    action: block

  # Require approval for malware findings
  - match:
      finding_type: malware
    action: approve
    reason: "Malware detected in dependency"

  # Block copyleft licenses
  - match:
      finding_type: license
      license_spdx:
        deny: [GPL-3.0-only, AGPL-3.0-only]
    action: block

  # Warn on low-reputation packages
  - match:
      finding_type: reputation
      severity: high
    action: warn
```

| Match Field | Description |
| --- | --- |
| `packages` | Exact package name list |
| `name_patterns` | Glob patterns for package names (e.g. `@acme/*`) |
| `finding_type` | `vulnerability`, `license`, `provenance`, `reputation`, `malware` |
| `severity` | `critical`, `high`, `medium`, `low`, `info` |
| `ecosystem` | `npm` or `pypi` |
| `reasons` | Match specific provider reason codes |
| `license_spdx` | SPDX allow/deny lists for license identifiers |

Verdict actions: `allow`, `warn` (log and permit), `approve` (require human approval), `block` (deny the install).

### Configuration

```text
package_checks:
  enabled: true
  scope: new_packages_only   # new_packages_only or all_installs
  cache:
    ttl:
      vulnerability: 1h
      license: 24h
      provenance: 24h
      reputation: 24h
      malware: 1h
  providers:
    osv:
      enabled: true
      timeout: 10s
      on_failure: warn        # warn, deny, allow, or approve
    depsdev:
      enabled: true
      timeout: 10s
      on_failure: warn
    local:
      enabled: true
      on_failure: warn
    socket:
      enabled: false
      api_key_env: SOCKET_API_KEY
      timeout: 10s
      on_failure: warn
    snyk:
      enabled: false
      api_key_env: SNYK_TOKEN
      timeout: 10s
      on_failure: warn
  registries:
    npmjs.org:
      trust: check_full       # check_full, check_local_only, or trusted
    internal.corp:
      trust: trusted
      scopes: ["@acme"]
```

| Field | Default | Description |
| --- | --- | --- |
| `enabled` | `false` | Enable package install security checks |
| `scope` | `new_packages_only` | `new_packages_only` skips already-installed packages; `all_installs` checks every install |
| `cache.ttl.*` | varies | Per-finding-type cache lifetimes for provider results |
| `providers.*.enabled` | varies | Enable/disable each provider independently |
| `providers.*.timeout` | `10s` | Per-provider request timeout |
| `providers.*.on_failure` | `warn` | Action when provider fails: `warn`, `deny`, `allow`, `approve` |
| `providers.*.api_key_env` | — | Environment variable containing the API key |
| `registries.*.trust` | `check_full` | `trusted` skips checks; `check_local_only` uses only the local provider |

## Ptrace Enforcement

When seccomp user-notify, eBPF, and FUSE are unavailable — AWS Fargate, restricted Kubernetes, gVisor, Modal — agentsh can use the Linux `ptrace` API to intercept syscalls and enforce policy. Ptrace mode provides the same enforcement guarantees as full mode without requiring privileged kernel features.

For configuration and attach modes, see [Setup → Ptrace Mode](https://www.agentsh.org/docs/setup/#ptrace-mode).

#### Target environments

| Environment | Why ptrace |
| --- | --- |
| AWS Fargate | seccomp user-notify, eBPF, and FUSE are all blocked; `SYS_PTRACE` available with shared PID namespace |
| Restricted Kubernetes | Security contexts prevent seccomp/eBPF; ptrace via `SYS_PTRACE` capability |
| gVisor / Firecracker | Custom kernel does not support seccomp user-notify or Landlock |
| Modal | gVisor runtime; ptrace provides full policy enforcement |
| exe.dev | Hybrid mode: ptrace for execve, seccomp wrapper for file/network/signal |

#### Performance

Overhead is ~5.4x with all optimizations enabled (versus ~23x without). Key optimizations:

- **seccomp pre-filter** — BPF filter reduces ptrace stops to ~25 policy-relevant syscalls

- **Argument-level BPF filtering** — checks syscall arguments in BPF before triggering ptrace stops (e.g., `sendto` with NULL destination bypasses ptrace entirely)

- **Lazy BPF escalation** — narrow initial filter; escalated per-process only when TracerPid masking or TLS SNI rewrite is needed

- **Per-syscall resume** — `PTRACE_CONT` for entry-only syscalls, `PTRACE_SYSCALL` only for the 7 syscalls needing exit processing

        Opt-in only

Ptrace mode is not auto-selected. Enable it explicitly in configuration under `sandbox.ptrace`. See [Setup → Ptrace Mode](https://www.agentsh.org/docs/setup/#ptrace-mode).

#### Hybrid ptrace + seccomp mode

On platforms where both ptrace and seccomp are available (e.g., exe.dev), agentsh supports a hybrid mode:

- **ptrace for execve** — every binary execution goes through ptrace with seccomp pre-filter

- **seccomp file_monitor for file I/O** — file operations intercepted via seccomp-notify, avoiding ptrace overhead on high-frequency syscalls

- **Landlock for filesystem boundaries** — kernel-enforced directory restrictions

Configure by enabling ptrace with `trace.execve: true` and seccomp `file_monitor` with `enforce_without_fuse: true`, while disabling ptrace file/network/signal tracing.

## Seccomp-Notify File Enforcement

When FUSE and Landlock are both unavailable (e.g., nested containers, Firecracker microVMs), agentsh can enforce file policies using the **seccomp user-notify** mechanism. The supervisor process intercepts file-related syscalls, evaluates them against policy, and either allows or blocks the operation—all without a virtual filesystem layer.

#### How it works

The seccomp-notify file enforcement backend intercepts syscalls at the BPF filter level, then evaluates file paths against your existing `file_rules` policy:

- **openat interception** — for allowed opens, the supervisor opens the file itself and injects the fd into the tracee via `SECCOMP_ADDFD_FLAG_SEND` (AddFD emulation). This prevents the tracee from racing the policy check.

- **Metadata syscalls** — `statx`, `newfstatat`, `faccessat2`, `readlinkat` are intercepted with `SECCOMP_RET_USER_NOTIF` so policy can control stat/access checks on protected paths.

- **io_uring blocking** — `io_uring_setup`, `io_uring_enter`, and `io_uring_register` are blocked with `EPERM` to prevent bypass of the seccomp notification path.

- **`/proc/self/fd/N` resolution** — paths like `/proc/self/fd/5`, `/proc/thread-self/fd/N`, `/dev/fd/N`, and `/dev/stdin` are resolved to their actual targets before policy evaluation to prevent fd-based bypasses.

- **TOCTOU bracketing** — `SECCOMP_IOCTL_NOTIF_ID_VALID` checks bracket supervisor-side operations to mitigate time-of-check/time-of-use races.

        Kernel requirements

Requires Linux kernel ≥ 5.9 for seccomp user-notify and ≥ 5.14 for AddFD emulation (`SECCOMP_ADDFD_FLAG_SEND`). Use `agentsh detect` to check the active file-domain backend and capability map.

#### Configuration

```yaml
sandbox:
  seccomp:
    enabled: true
    file_monitor:
      enabled: true
      enforce_without_fuse: true  # Enable file enforcement via seccomp-notify
      intercept_metadata: true   # Intercept statx, faccessat2, etc. (default: true)
      openat_emulation: true     # AddFD emulation for openat (default: true)
      block_io_uring: true       # Block io_uring to prevent bypass (default: true)
```

#### What gets intercepted

| Category | Syscalls | Enforcement |
| --- | --- | --- |
| File open | `openat`, `open`, `creat` | AddFD emulation (supervisor opens, injects fd) |
| File open (v2) | `openat2` | CONTINUE + ID validation (never emulated) |
| Metadata | `statx`, `newfstatat`, `faccessat2`, `readlinkat` | CONTINUE + ID validation |
| File modify | `unlinkat`, `renameat2`, `linkat`, `mknodat` | CONTINUE + ID validation |
| io_uring | `io_uring_setup`, `io_uring_enter`, `io_uring_register` | Blocked with `EPERM` |

#### Emulation gating

AddFD emulation only activates when *all* of these conditions are met:

- `enforce_without_fuse: true` in config

- Kernel ≥ 5.14 (verified via `uname`)

- Landlock is **not** configured (prevents supervisor opens from bypassing Landlock)

- No active FUSE mounts (FUSE handles its own enforcement)

When any condition is unmet, file syscalls fall back to `SECCOMP_RET_CONTINUE` with ID validation (audit-only).

## Socket Family Blocking

agentsh can block creation of specific `AF_*` socket families on `socket(2)` and `socketpair(2)`. This mitigates the recurring CVE class where opening a niche kernel protocol family (like `AF_ALG`, `AF_VSOCK`, or `AF_RDS`) is the entry point to a kernel exploit — see the [copy.fail](https://copy.fail/#mitigation) writeup for the `AF_ALG` case that motivated this feature.

#### Configuration

```yaml
sandbox:
  seccomp:
    enabled: true
    # Unset      → recommended-default list applied (12 families at errno).
    # []         → opt out of all family blocking entirely.
    # Non-empty  → overrides defaults.
    blocked_socket_families:
      - family: AF_ALG       # by name (preferred) or numeric ("38")
        action: errno        # errno | kill | log | log_and_kill (default: errno)
      - family: AF_VSOCK
        action: log_and_kill
```

#### Default list (when field unset)

When `blocked_socket_families` is omitted, agentsh applies a recommended-default list of 12 families at `action: errno`. Set the field to `[]` to opt out entirely; set it to a non-empty list to override the defaults.

| Family | Number | Why default |
| --- | --- | --- |
| `AF_ALG` | 38 | copy.fail mitigation; near-zero legitimate userspace use |
| `AF_VSOCK` | 40 | Niche (VM-host); multiple historical CVEs |
| `AF_RDS` | 21 | Reliable Datagram Sockets; multiple CVEs; effectively dead |
| `AF_TIPC` | 30 | Niche cluster protocol; multiple CVEs |
| `AF_KCM` | 41 | Kernel Connection Multiplexor; niche; CVEs |
| `AF_X25`, `AF_AX25`, `AF_NETROM`, `AF_ROSE`, `AF_DECnet`, `AF_APPLETALK`, `AF_IPX` | various | Legacy/dead protocols; pure attack surface |

**Not** in defaults (too widely used; opt-in only): `AF_NETLINK`, `AF_PACKET`, `AF_BLUETOOTH`, `AF_CAN`.

#### Family resolution

Each entry's `family` field accepts either a name (`AF_ALG`) or a numeric string (`"38"`). Names resolve via a built-in table; numbers in `[0, 64)` are accepted as a fallback for families the table doesn't yet know. Unknown names and out-of-range numbers are rejected at config-load time:

```text
sandbox.seccomp.blocked_socket_families[0].family: "AF_ALGOG" is not a valid AF_* name or number
sandbox.seccomp.blocked_socket_families[1].action: "deny" is not valid (allowed: errno, kill, log, log_and_kill)
```

#### Action mapping

| Action | Effect on `socket(AF_X, …)` | Audit event |
| --- | --- | --- |
| `errno` | Returns `EAFNOSUPPORT` (97) — the standard "this family isn't supported" code | none (kernel-side) |
| `kill` | Process killed by `SCMP_ACT_KILL_PROCESS` | none (kernel-side) |
| `log` | Returns `EAFNOSUPPORT` + emits audit event | `seccomp_socket_family_blocked`, outcome `denied` |
| `log_and_kill` | Process killed by `SIGKILL` + emits audit event | `seccomp_socket_family_blocked`, outcome `killed` |

`errno` is the right default for security tooling — well-behaved userspace falls back gracefully when a family isn't supported.

#### Two enforcement engines

Family blocking has two engines that share the same config and emit identical audit-event shapes:

1. **seccomp-bpf (primary)** — adds an `AddRuleConditional` rule on `socket(2)` arg0 to the existing seccomp filter. Cheap, kernel-side. Used when seccomp is available *and* the agentsh-unixwrap binary will run.

2. **ptrace (fallback + defensive)** — when `sandbox.ptrace.enabled: true`, the family checker is also wired into the ptrace tracer regardless of which engine the selector reports. Runtime dispatch is mutually exclusive between engines, so this is safe and ensures coverage in hybrid configurations where the seccomp wrapper is skipped.

If neither engine is available on the host, agentsh logs a startup warning and continues — families are not blocked.

#### Coexistence with other seccomp rules

When a family is in `blocked_socket_families` *and* `socket` is in `blocked_syscalls`, the family rule wins (more specific). When `unix_socket.enabled: true` is also set, libseccomp's action precedence ensures family `errno`/`kill` rules outrank the unconditional `ActNotify` on `socket(2)` — `AF_UNIX` traffic still flows through unix-socket monitoring; `AF_ALG` (etc.) is denied.

#### Audit event

```text
{
  "type": "seccomp_socket_family_blocked",
  "timestamp": "2026-04-29T18:00:00Z",
  "session_id": "sess_abc123",
  "source": "seccomp",
  "pid": 12345,
  "fields": {
    "family_name": "AF_ALG",
    "family_number": 38,
    "syscall": "socket",
    "action": "log_and_kill",
    "outcome": "killed",
    "engine": "seccomp"
  }
}
```

| Field | Meaning |
| --- | --- |
| `family_name` | Original config name (e.g. `AF_ALG`); empty if the entry was numeric-only |
| `family_number` | Resolved `AF_*` number |
| `syscall` | `socket` or `socketpair` |
| `action` | Value of `action` that matched (`log` or `log_and_kill`) |
| `outcome` | `denied` (errno path) / `killed` (kill landed) / `vanished` (tracee gone before enforcement) / `deny_failed` / `deny_fallback_failed` |
| `engine` | `seccomp` or `ptrace` — same audit shape regardless |

## Policy Signing

agentsh supports **Ed25519 detached signatures** for policy files, providing cryptographic proof of authorship and integrity. This ensures agents only enforce policies from trusted authorities — a signed policy cannot be modified without invalidating the signature.

#### How it works

- Each policy file `<name>.yaml` has a companion signature file `<name>.yaml.sig` (JSON format)

- Signatures cover the raw bytes of the policy YAML — no normalization or parsing

- A **trust store** (directory of public key files) holds verification keys with optional expiry

- Key IDs are derived deterministically: `hex(SHA256(ed25519_public_key_bytes))`

#### CLI workflow

```bash
# 1. Generate an Ed25519 keypair
agentsh policy keygen --output /etc/agentsh/keys/ --label "security-team"

# 2. Sign a policy file (produces policy.yaml.sig)
agentsh policy sign policy.yaml --key /etc/agentsh/keys/private.key.json --signer "security-team"

# 3. Verify a signed policy
agentsh policy verify policy.yaml --key-dir /etc/agentsh/keys/
# → {"status":"valid","key_id":"a1b2...","signer":"security-team","signed_at":"2026-03-18T..."}
```

#### Verification modes

| Mode | Behavior |
| --- | --- |
| `enforce` | Reject policies with missing or invalid signatures. Server refuses to start or create sessions. |
| `warn` | Log a warning on verification failure, but load the policy anyway. |
| `off` | Skip signature verification entirely (default). |

#### Configuration

```yaml
policies:
  signing:
    trust_store: "/etc/agentsh/keys/"   # Directory of trusted public key JSON files
    mode: "enforce"                    # "enforce" | "warn" | "off"
```

        Security hardening

Private key files must have mode `0600` (rejected otherwise). In `enforce` mode, world-writable trust store directories and files are rejected. Expired keys (via `expires_at` field) are automatically rejected.

### Staged Hot-Reload

Policy changes can be deployed via a **staged validation pipeline** that prevents invalid or tampered policies from reaching live enforcement. Drop signed policies into a `.staging/` directory; agentsh validates the signature, promotes to the live directory, and triggers a reload—all atomically.

#### How it works

1. **Stage** — drop `policy.yaml` and `policy.yaml.sig` into `<policy_dir>/.staging/`

2. **Validate** — the hot-reload watcher detects the new file, verifies the Ed25519 signature against the trust store

3. **Promote** — signature file is moved to the live directory first, then the policy file (atomic on same filesystem)

4. **Reload** — `fsnotify` detects the promoted policy in the live directory and triggers a policy reload

If signature validation fails, the staged files are rejected and an error is logged. If the policy move fails after the signature was already promoted, the orphaned `.sig` is removed from the live directory to prevent a retry loop.

#### Staging behavior by signing mode

| Signing Mode | Staging Behavior |
| --- | --- |
| `enforce` | Signature required. Invalid or missing `.sig` is rejected. |
| `warn` | Signature validated if present. Warning logged on failure, but policy is promoted. |
| `off` | No signature check. Staged files are promoted directly. |

        Deployment pattern

The `.staging/` directory is created automatically when the hot-reload watcher starts. In CI/CD, use `agentsh policy sign` then `cp` both files into `.staging/`—the watcher handles the rest. A 2-second debounce ensures the `.sig` file has time to arrive before validation begins.

## Policy Generation

The **profile-then-lock** workflow lets you generate restrictive policies from observed session behavior. Run your workload once in permissive mode, then lock down future runs to only what was observed.

### The Profile-Then-Lock Workflow

#### 1. Profile

Run your build/test/agent task with a permissive policy. agentsh records every operation.

#### 2. Generate

Use the recorded session to generate a policy that allows only what was observed.

#### 3. Lock

Apply the generated policy to future runs. Any deviation is blocked or flagged.

#### 4. Iterate

If legitimate behavior is blocked, re-profile and regenerate.

### Generating a Policy

```bash
# Step 1: Run your workload (profile phase)
SID=$(agentsh session create --workspace . --policy agent-observe --json | jq -r .id)
agentsh exec "$SID" -- npm install
agentsh exec "$SID" -- npm run build
agentsh exec "$SID" -- npm test

# Step 2: Generate policy from observed behavior
mkdir -p ~/.config/agentsh/policies
agentsh policy generate "$SID" --output="$HOME/.config/agentsh/policies/ci-build.yaml" --name=ci-build

# Step 3: Use the locked policy for future runs
SID=$(agentsh session create --workspace . --policy ci-build --json | jq -r .id)
agentsh exec "$SID" -- npm install  # Only allowed packages from profile
```

### Generation Options

| Flag | Description |
| --- | --- |
| `--output` | Write policy to file (default: stdout) |
| `--name` | Policy name (default: generated-<session-id>) |
| `--threshold` | Minimum occurrences before including a path pattern (default: 5) |
| `--include-blocked` | Include blocked operations as commented-out rules for review (default: true) |

### What Gets Generated

The generated policy:

- **File rules:** Allows only paths that were accessed, grouped into globs (e.g., `/workspace/src/**/*.ts`)

- **Network rules:** Allows only domains/IPs that were contacted, with subdomain wildcards (e.g., `*.github.com`)

- **Command rules:** Flags risky commands (`curl`, `wget`, `rm`) with the exact arg patterns observed

- **Comments:** Documents why each rule exists with session timestamp

### Example Generated Policy

```yaml
# Generated from session sess-abc123 on 2025-01-15
# Profile: npm install && npm run build && npm test

version: 1
name: ci-build

file_rules:
  - name: workspace-src
    paths: ["/workspace/src/**"]
    operations: [read, open, stat, list]
    decision: allow

  - name: workspace-dist
    paths: ["/workspace/dist/**"]
    operations: [read, write, create, mkdir]
    decision: allow

  - name: node-modules
    paths: ["/workspace/node_modules/**"]
    operations: ["*"]
    decision: allow

network_rules:
  - name: npm-registry
    domains: ["registry.npmjs.org", "*.npmjs.org"]
    ports: [443]
    decision: allow

  - name: default-deny
    domains: ["*"]
    decision: deny
```

### Use Cases

- **CI/CD lockdown:** Profile a build/test run, lock future runs to that exact behavior

- **Agent sandboxing:** Let an AI agent run a task once, generate policy for future runs

- **Container profiling:** Profile a workload, generate minimal policy for production

- **Compliance:** Document exactly what a process does and enforce it doesn't change

## Daemon Management

Manage the agentsh background daemon for continuous monitoring:

```bash
# Install startup integration (systemd/launchd)
agentsh daemon install

# Check daemon status
agentsh daemon status --json

# Restart with fresh session
agentsh daemon restart

# Remove startup integration
agentsh daemon uninstall
```

## Agent Wrapping

The `wrap` command provides a single-command shortcut to run an AI agent under agentsh with exec interception. It creates a session, sets up the environment, runs the agent, and generates a report on exit:

```bash
# Wrap an AI agent with default policy
agentsh wrap -- claude --dangerously-skip-permissions -p "implement the feature"

# Use a specific policy and workspace root
agentsh wrap --policy ci-locked --root /workspace -- npm test

# Preserve real host paths by wrapping into a real-paths session
SID=$(agentsh session create --workspace . --real-paths --json | jq -r .id)
agentsh wrap --session "$SID" -- claude --dangerously-skip-permissions -p "implement the feature"

# Use the agent-default policy with comprehensive guardrails
agentsh wrap --policy agent-default -- codex --full-auto "fix the failing tests"

# Reuse an existing session
agentsh wrap --session $SID -- codex --full-auto "fix the failing tests"

# Skip the exit report
agentsh wrap --report=false -- node script.js
```

## Sitemap

- [Canonical HTML](https://www.agentsh.org/docs/features/)
- [Site map](https://www.agentsh.org/sitemap.md)
- [Full documentation](https://www.agentsh.org/llms-full.md)
