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
network_rules:
- name: steer-npm
destinations: ["registry.npmjs.org"]
decision: redirect
redirect_to: "npm.internal.corp"
message: "Steered to internal registry"
Steer curl to an audited wrapper
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
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:
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:
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.
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.
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), orwarnon_failure:fail_closed(default),fail_open, orretry_original
Connect Redirects
Intercept TCP connections and route them to different destinations. Supports TLS/SNI rewriting for MITM proxy scenarios.
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 forhost:port(or just host for any port)redirect_to: Destinationhost:porttls.mode:passthrough(keep original SNI) orrewrite_snitls.sni: Required if mode isrewrite_sni—the SNI to use in ClientHellovisibility,message,on_failure: Same as DNS redirects
SNI Rewriting
When routing through a MITM proxy that presents its own certificate, use SNI rewriting:
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:
# 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 redirects are available on all platforms:
- Linux: eBPF-based interception of
getaddrinfo()andconnect()syscalls - macOS: Endpoint Security Framework integration
- Windows: WinDivert packet filtering with policy-aware NAT
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.
# 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_URLandOPENAI_BASE_URLso 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
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.
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.
# 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 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 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.
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.
To make non-cooperative SDKs work transparently, combine the HTTP service declaration with DNS & connect redirects to steer the traffic through the gateway automatically:
# Redirect direct api.stripe.com traffic through the gateway
dns:
overrides:
api.stripe.com: 127.0.0.1
http_services:
- name: stripe
upstream: https://api.stripe.com
default: deny
rules:
- name: read-customers
methods: [GET]
paths: ["/v1/customers", "/v1/customers/*"]
decision: allow
Workspace Checkpoints#
Create snapshots of workspace state for recovery from destructive operations:
# 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.
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:
# 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:
# 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:
# 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 codeswebauthn- 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.
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:
# 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.
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
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:
{
"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.
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.
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#
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.
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.,
sendtowith 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_CONTfor entry-only syscalls,PTRACE_SYSCALLonly for the 7 syscalls needing exit processing
Ptrace mode is not auto-selected. Enable it explicitly via configuration or environment variables. See 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,readlinkatare intercepted withSECCOMP_RET_USER_NOTIFso policy can control stat/access checks on protected paths. - io_uring blocking —
io_uring_setup,io_uring_enter, andio_uring_registerare blocked withEPERMto prevent bypass of the seccomp notification path. /proc/self/fd/Nresolution — paths like/proc/self/fd/5,/proc/thread-self/fd/N,/dev/fd/N, and/dev/stdinare resolved to their actual targets before policy evaluation to prevent fd-based bypasses.- TOCTOU bracketing —
SECCOMP_IOCTL_NOTIF_ID_VALIDchecks bracket supervisor-side operations to mitigate time-of-check/time-of-use races.
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 file_enforcement field reports which backend is active.
Configuration
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: truein 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).
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>.yamlhas 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
# 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
policies:
signing:
trust_store: "/etc/agentsh/keys/" # Directory of trusted public key JSON files
mode: "enforce" # "enforce" | "warn" | "off"
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
- Stage — drop
policy.yamlandpolicy.yaml.siginto<policy_dir>/.staging/ - Validate — the hot-reload watcher detects the new file, verifies the Ed25519 signature against the trust store
- Promote — signature file is moved to the live directory first, then the policy file (atomic on same filesystem)
- Reload —
fsnotifydetects 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. |
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
# Step 1: Run your workload (profile phase)
SID=$(agentsh session create --workspace . --policy permissive | 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
agentsh policy generate "$SID" --output=ci-locked.yaml --name=ci-build
# Step 3: Use the locked policy for future runs
SID=$(agentsh session create --workspace . --policy ci-build | 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: 1) |
--include-blocked | Include blocked operations as commented-out rules for review |
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
# 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:
# 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:
# 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 (instead of virtualizing under /workspace)
agentsh wrap --real-paths -- 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