Docs

Run controlled chaos experiments against your services. Inject real faults - network, Layer 7, resource, process/kernel, or cloud - watch a guardrail, abort automatically.

Install

The agent runs on Linux and needs CAP_NET_ADMIN to manipulate the kernel queueing discipline (tc/netem). The control plane is portable Go and runs anywhere.

Linux host

# single-line installer (puts /usr/local/bin/chaos in place)
curl -fsSL https://get.straightchaos.com | sh

# verify
chaos version

Docker

The agent needs network admin capabilities. Use one of:

# minimum permissions
docker run --rm \
  --cap-add=NET_ADMIN --network=host \
  straightchaos/agent:latest \
  agent --control-plane $CP --token $ENROLL_TOKEN

# or fully privileged (simpler, less isolated)
docker run --rm --privileged --network=host straightchaos/agent:latest \
  agent --control-plane $CP --token $ENROLL_TOKEN
--network=host is required. The agent shapes traffic on real network interfaces, not on Docker's bridge.

Kubernetes (DaemonSet)

A reference manifest is in deploy/daemonset.yaml. The pod template needs:

securityContext:
  capabilities:
    add: ["NET_ADMIN"]
hostNetwork: true
env:
  - { name: CHAOS_CONTROL_PLANE, value: https://cp.example.com }
  - { name: CHAOS_TOKEN, valueFrom: { secretKeyRef: { name: chaos-enroll, key: token } }
macOS: real fault injection is Linux-only - macOS doesn't have tc/netem. The control plane and dashboard run natively; for the agent, either run it in a Linux VM/container, or use --simulate for the wiring without kernel changes.

Quickstart

From zero to a guarded agent in three steps.

1. Install

curl -fsSL https://get.straightchaos.com | sh

One binary, macOS or Linux. (To self-host the control plane instead of the hosted one, see Operations.)

2. Sign up & connect

Open /dashboard, pick Sign up, then Connect an AI agent, it mints a workspace token and shows the exact command to run, with checkboxes for the protections you want (Mirror, data firewall, intent firewall, …).

3. Run your agent under guard

chaos guard claude                              # monitor: see everything it does
chaos guard --mirror --dlp --enforce claude     # block prompt-injection + secret exfil

Every API call, file access, tool call, and token spend shows up in the dashboard within a second. Start in monitor mode to see the truth, then add --enforce to block.

Tip: Add --simulate for a dry pass that wires everything up without touching the kernel, useful for verifying setup before you enforce. And chaos run is the other half of the toolkit: chaos engineering for your own infrastructure.

Concepts

Guard

chaos guard -- <agent> wraps an AI agent and proxies its egress, the enforcement point for every policy below. Start in monitor mode, then flip to enforce.

Guardrail / policy

The rules the guard applies: allow / deny / hold-for-approval by host, path, SQL, model, or secret, plus the data firewall, intent firewall, and Mirror. Off by default; monitor → enforce.

Conduct

A tamper-evident, hash-chained record of how each agent actually behaved, scored, and mintable into a portable, signed credential you can verify and gate on.

Agent

The Go binary on the host. Under guard it wraps your AI agent; standalone (chaos agent) it runs chaos experiments.

Control plane

HTTP API + workspace/auth model + dashboard. Holds policy, dispatches experiments, ingests activity, exposes everything via REST.

Fault

The chaos-engineering side: twelve injectable fault types, network (latency, loss, partition, DNS, flow-drop), Layer 7 (HTTP/TLS), resource (CPU, memory, disk), process/kernel, and cloud, for stress-testing your own infrastructure.

Blast radius

How much traffic the fault affects: scoped (one destination host or CIDR) or whole device (all egress on the interface).

Deadman

A detached process that removes the fault unconditionally after duration + grace. Survives even if the agent crashes. Last line of defense.

Steady state

What "healthy" means for your service. Define it as an endpoint that returns 200, or a PromQL query that stays under a threshold.

Orchestration

Chain faults into an ordered scenario (GameDay), fan one across a label-selected fleet capped by a blast radius, or run them on a schedule. See Orchestration.

Autopilot

Auto-builds a fault gauntlet from an agent's discovered dependencies and reports which ones the service can't tolerate. See Autopilot.

Guard an AI agent

The same egress interception that injects faults can be pointed the other way - to govern what an AI agent (Claude, ChatGPT/Codex, an autonomous agent, a CI step) is allowed to do. A guardrail policy matches each outbound request on method / host / path and allows, denies, or holds it for a human (approve). Two modes: monitor (log only - roll out safely) and enforce (actually block).

One command, no setup

chaos guard wraps a process as a local HTTP proxy and runs it with HTTP_PROXY/HTTPS_PROXY pointed at the guardrail - on macOS or Linux, no root. Inline rules need no policy file or control plane:

# let the agent reach Anthropic + your APIs, block secrets, hold any call to OpenAI
chaos guard --enforce \
  --deny /secrets/ \
  --approve api.openai.com \
  -- claude

A pattern with a / matches a path substring; otherwise it's a host. Rules apply deny → approve → allow; --default-deny flips to an allow-list. Drop --enforce to start in monitor mode and just watch what the agent does first.

Recipes

# Claude Code - observe everything it calls (monitor), nothing blocked yet
chaos guard -- claude

# ChatGPT / Codex CLI - allow-list: only Anthropic/OpenAI + your internal API
chaos guard --enforce --default-deny \
  --allow api.openai.com --allow api.anthropic.com --allow api.internal \
  -- codex

# A Python agent - hold every production write for a human in the dashboard
chaos guard --control-plane $CHAOS_CONTROL_PLANE --token $CHAOS_TOKEN \
  --enforce --approve /v1/prod/ \
  -- python agent.py
HTTPS arrives as CONNECT host:port, so host rules (--deny api.openai.com) work on TLS as-is. To enforce path/method rules on HTTPS, add --terminate-tls: the proxy decrypts the tunnel with a leaf the agent CA signs for the host, evaluates the full request, and re-encrypts to the real upstream. The wrapped child trusts the chaos CA automatically: the guard sets NODE_EXTRA_CA_CERTS for Node tools (Claude Code, many AI CLIs) and SSL_CERT_FILE + REQUESTS_CA_BUNDLE for OpenSSL/curl/Go/Python tools - no manual step. (Setting the latter two is safe because termination intercepts every HTTPS connection, so the agent only ever speaks TLS to the guard, never to the public roots they'd replace.) Most CLIs and SDKs honor the proxy env vars; for Node-based tools chaos guard also sets NODE_USE_ENV_PROXY=1 (Node 24+).

Chaos-test the agent's dependencies

The same egress interception that governs an agent can degrade it - chaos engineering for AI. An LLM agent's most critical dependency is the model API itself, and almost nobody tests what happens when it gets slow or starts erroring. Point a fault at the agent's egress and watch how it copes (does it retry? back off? fall back? wedge?):

# the model API gets slow + rate-limited 30% of the time
chaos guard --terminate-tls \
  --fault-host api.anthropic.com --fault-delay 2s --fault-status 429 --fault-pct 30 -- claude

# an MCP / tool endpoint returns 500
chaos guard --fault-host localhost:3000 --fault-path /tools/ --fault-status 500 -- claude

The fault actions are --fault-delay (added latency), --fault-status N (a synthetic error like 429/503 instead of forwarding), --fault-reset (drop the connection), and --fault-truncate N (cut the response body after N bytes - a streaming/SSE interruption, HTTPS only). Scope with --fault-host / --fault-path, sample with --fault-pct, or declare many with --faults FILE. Faulting an HTTPS host (model APIs are HTTPS) needs --terminate-tls so the response can be shaped; faults apply only to allowed requests, so they compose with the guardrail policy.

Provider-accurate model failures

Hand-writing the exact error a model API returns is fiddly, and you don't want to spend tokens on a real call just to make it fail. --fault-llm PRESET synthesizes the failure in the real wire format the SDK parses, so you exercise the agent's actual error path:

chaos guard --terminate-tls --fault-host api.anthropic.com --fault-llm overloaded -- claude
#   → HTTP 529  {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}

The presets are overloaded (Anthropic 529 / OpenAI 503), rate-limit (429 + a Retry-After header), server-error (500), bad-gateway (502 infra HTML), timeout (a 30-second hang then 504), and stream-cut - a valid SSE stream that stops abruptly mid-response, the failure mode that most often wedges a streaming client. --fault-llm-provider anthropic|openai selects the envelope. Combine with --fault-pct to make it intermittent, the way a real outage is.

This is the fault engine and the agent guard meeting in the middle: the wrap-mode proxy that governs egress also injects the L7-style faults (delay / status / reset / truncate) into it - so you can run a GameDay against your AI agent's resilience, not just your servers'.

Filesystem monitoring

Network egress is half the picture - an agent also reads and rewrites files. Add --watch-fs to chaos guard to monitor what the wrapped process creates, modifies, and deletes under a set of roots (the current directory by default; scope with repeatable --watch-path). It's monitor-only - it observes and reports, never blocks - mirroring the guardrail monitor posture. Each change is logged live and a summary prints on exit; with a control plane connected, every change is recorded to the workspace audit log.

chaos guard --watch-fs --watch-path ./src --deny /secrets/ -- claude
fs: create ./src/feature.go (1.2 KB)
fs: modify ./src/app.go (3.4 KB)
fs: agent touched 2 file(s) - 1 created, 1 modified, 0 deleted
The portable observer snapshots and diffs the watched roots (no root, no kernel support, macOS + Linux), so it sees net modifications. High-churn dirs (.git, node_modules, build output) are pruned.

Real-time enforcement (Linux)

Monitoring sees changes after the fact; on Linux, --enforce-fs goes further and blocks the agent's file opens and execs before they happen, in real time, via fanotify. Deny reads of sensitive paths, or execution of tools the agent shouldn't run - matched as path substrings; --fs-default-deny flips to an allow-list.

chaos guard --enforce-fs \
            --fs-deny /etc/shadow --fs-deny /.ssh/ \
            --fs-deny-exec /usr/bin/curl -- claude
Enforcement is scoped to the wrapped agent and its children (the guard's own I/O is never gated). On Linux it uses fanotify (needs root / CAP_SYS_ADMIN); on macOS it uses the App Sandbox (Seatbelt), kernel-enforced with no entitlement or signing, the agent simply launches under a generated sandbox profile. The kernel interception is validated on a real host; the policy engine is unit-tested.

Kubernetes-aware rules

The Kubernetes API is HTTPS, so with the request visible (cleartext, or HTTPS under --terminate-tls) the engine derives the logical operation from the method and path - verb, resource, namespace, object name - and rules match on that instead of brittle URL globbing. GET /api/v1/namespaces/prod/secrets/db becomes get secrets in prod; POST /api/v1/namespaces/prod/pods/web/exec becomes create pods/exec (kubectl exec). Match on k8s_verb / k8s_resource / k8s_namespace (subresources use the RBAC pods/exec form, and these AND with the HTTP matchers):

{ "mode": "enforce", "default_allow": true, "rules": [
  { "name": "no-prod-secret-deletes", "k8s_verb": "delete",
    "k8s_resource": "secrets", "k8s_namespace": "prod", "action": "deny" },
  { "name": "exec-needs-approval", "k8s_resource": "pods/exec", "action": "approve" }
]}

The dashboard's Test a request box echoes the derived interpretation, so you can paste a k8s URL and see exactly how a rule reads it.

SQL-aware rules (Postgres)

SQL isn't HTTP, so it gets a real wire-protocol terminator. chaos guard --pg-listen 127.0.0.1:5432 --pg-upstream db.internal:5432 sits between the agent and Postgres, parses each statement (simple and extended/prepared queries), and gates it on the verb and the tables it touches - a class of control an HTTP or LLM proxy fundamentally can't provide. Point the agent's database connection at the listen address (the guard sets PGHOST/PGPORT for the wrapped child); denied statements never reach the database - the client receives a Postgres error.

{ "mode": "enforce", "default_allow": true, "rules": [
  { "name": "no-drops",       "sql_verb": "drop",     "action": "deny" },
  { "name": "billing-locked", "sql_table": "billing", "action": "approve" }
]}
A statement that touches several tables matches a sql_table rule if any of them is the named table. The parser is a pragmatic heuristic (it strips comments and string literals so a table name can't hide in a value); it sees what the agent sends, not the database's view.

Encrypting the guard→database leg

The agent connects to the guard in cleartext over localhost, but the guard→database hop can run over TLS - encrypt it for a remote database (especially since that leg now carries the injected credential). --pg-upstream-sslmode mirrors libpq: disable (default), require (encrypt), or verify-full (encrypt + verify the server cert and hostname, with --pg-upstream-ca FILE for a private CA). The guard performs the Postgres SSL negotiation as the client.

chaos guard --pg-listen 127.0.0.1:5432 --pg-upstream db.internal:5432 \
            --pg-upstream-sslmode verify-full --creds creds.json -- claude
Pair upstream TLS with --creds (credential injection). In plain passthrough the database offers SCRAM channel binding (SCRAM-SHA-256-PLUS) over the encrypted upstream while the agent's leg is cleartext, which clients reject; with credential injection the guard completes SCRAM itself over the TLS leg, so it just works.

ClickHouse (over HTTP)

ClickHouse's HTTP interface carries the query in the request (the ?query= param and/or the POST body), so it rides the existing HTTP guardrail rather than a separate terminator. Mark the ClickHouse host(s) with --clickhouse-host and the guard extracts the SQL, derives the same sql_verb/sql_table facts, and gates the request - blocked queries get a 403 and never reach the database. It buffers only the head of the body (enough for the verb + tables) and streams the rest, so large INSERTs aren't held in memory or truncated.

chaos guard --enforce --policy policy.json \
            --clickhouse-host analytics.internal -- claude
# policy.json: { "rules": [ { "sql_verb": "drop", "action": "deny" },
#                           { "sql_table": "billing", "action": "approve" } ] }
Plain HTTP (port 8123) works directly; ClickHouse over HTTPS (8443) needs --terminate-tls so the body is visible. The native TCP protocol (9000) is a separate terminator, not yet built.

LLM-aware rules (model & token caps)

The agent's own call to its model API is just another egress request - and its body carries the two facts you most want to govern: which model it's invoking and how many output tokens it's asking for. Mark the model host(s) with --llm-host and the guard parses the request body (the Anthropic Messages API and OpenAI's chat/responses APIs share the {"model": …, "max_tokens": …} shape - OpenAI's newer field is max_completion_tokens), derives llm_model and llm_max_tokens facts, and gates on them. It buffers only the head of the body - the model and token fields sit in the small top-level object, ahead of any long prompt - and streams the rest, so a large prompt is neither held in memory nor truncated.

{ "mode": "enforce", "default_allow": true, "rules": [
  { "name": "no-opus",   "llm_model": "claude-opus-4-8", "action": "deny" },
  { "name": "token-cap", "llm_max_tokens_over": 4096,    "action": "deny" }
]}

Or inline, with no policy file - pin the model and cap per-call spend in one command (a model API is HTTPS, so add --terminate-tls to see the body):

chaos guard --enforce --terminate-tls \
            --llm-host api.anthropic.com \
            --deny-model claude-opus-4-8 --max-tokens 4096 -- claude

llm_max_tokens_over matches when the request asks for more than N output tokens - a per-call cost ceiling. Even in monitor mode (nothing blocked) the model rides into the activity log as the request's verb, so you can see - and cost - exactly which model each agent used.

Spend budgets (cumulative caps)

A llm_max_tokens_over rule caps one call; a budget caps the stream - the cumulative requests and requested output tokens across an agent's whole session - so a looping or runaway agent can't quietly burn the month's token spend, the one thing a per-request rule fundamentally can't express. The guard sums the parsed max_tokens across calls on an --llm-host and denies once the cap is reached (per identity, so each agent gets its own budget):

chaos guard --enforce --terminate-tls --llm-host api.anthropic.com \
            --llm-token-budget 2000000 --llm-request-budget 500 -- claude

Budgets are cumulative for the wrapped session by default; --llm-budget-window 1h makes them a rolling window instead (the last hour's spend). They bill the actual output tokens parsed from each response (usage.output_tokens, across both JSON and streaming SSE responses) - not the requested ceiling - so spend reflects what the agent really used. The token gate is reactive: calls are admitted until the cap is reached, then denied, and a budget-deny rides into the activity log just like a rule deny.

Because the guard already sees each response, it also records the real per-call input/output tokens in the activity log (and prints llm: <model> in=N out=M as it goes) - so you can see exactly what each model call cost, even with no budget set. This needs the response visible: a model API is HTTPS, so pair it with --terminate-tls.

Fleet-wide budgets. A local budget resets per process and only sees its own guard. --llm-budget-shared enforces the cap across every guard reporting under one --identity: each guard already reports its usage to the control plane, so the guard polls the fleet-wide spend and denies once the shared total crosses the cap. It needs a control plane and a real rolling window (--llm-budget-window 24h) - a per-session window can't be shared. It's eventually consistent (overshoot bounded by the ~15s poll), and the local in-process gate still runs alongside to catch a single guard's burst between polls.

# every agent under "claude@team" shares a 5M-token/day cap
chaos guard --control-plane $CP --token $TOK --identity claude@team \
            --enforce --terminate-tls --llm-host api.anthropic.com \
            --llm-token-budget 5000000 --llm-budget-window 24h --llm-budget-shared -- claude

Database credential injection

Just like API keys, the agent doesn't need to hold the database password either. Add a postgres entry to the --creds file and the guard authenticates to Postgres itself - completing the real SCRAM-SHA-256 (or MD5 / cleartext) handshake with the true password - while the agent connects with no password at all. The password can be Shamir-split across sources like any other secret, and an optional user rewrites the startup user so the agent needn't even know the real login.

// creds.json - the agent connects to the guard with no DB password
{ "postgres": {
    "user": "app",
    "password": { "type": "split", "threshold": 2, "shares": [
      { "type": "env",  "name": "PG_SHARE_1" },
      { "type": "file", "path": "/etc/chaos/pg.share2" } ]}}}

chaos guard --pg-listen 127.0.0.1:5432 --pg-upstream db.internal:5432 \
            --creds creds.json -- claude

Stop data exfiltration - outbound DLP

An agent with a long context can leak a credential or personal data outward - pasted into a prompt, an API key echoed into a tool call. chaos guard --dlp scans each outbound request body for secrets and PII before it leaves the box, and blocks the request when it finds one. Detectors are high-precision (prefixed credential formats and structurally-validated PII): cloud keys (AKIA…), provider tokens (sk-…, ghp_…, xoxb-…, Google API keys), JWTs, private keys, emails, credit cards (Luhn-checked), and US SSNs.

chaos guard --enforce --dlp -- claude                # block any outbound secret/PII
chaos guard --dlp --dlp-action redact -- claude      # strip the secret, let the call proceed
chaos guard --dlp --dlp-action monitor -- claude     # just flag what's leaking

Three actions: deny blocks the whole request; redact masks the secret out of the body (an equal-length replacement, so Content-Length is untouched) and forwards the now-safe request - the agent's call still works, minus the leak; monitor only logs. A finding rides in as the dlp_findings fact and a deny is recorded as the dlp-block rule - only the finding type and a masked hint are ever logged, never the secret itself. Scope it with --dlp-host HOST (default: all egress). In a policy file, match specific types: { "dlp_type": "aws_access_key", "action": "deny" } or "dlp_type": "any" for a catch-all.

DLP scans the head of the body (256 KB) and restores it intact for forwarding. HTTPS egress needs --terminate-tls so the body is visible; cleartext is scanned directly.

Bring your own policy - WebAssembly plugins

When the built-in rules aren't enough, write your own decision logic and drop it in as a WebAssembly plugin: chaos guard --wasm-policy policy.wasm consults it as an extra allow/deny gate for every request the static policy would otherwise allow. The module runs in a wazero sandbox with no filesystem, network, clock, or environment access - it's a pure decision function - and is loaded once for the guard's lifetime. It can add a denial (defense in depth); it can't override a policy deny.

The host writes the intercepted request (the same facts the rules see - method, host, path, llm_model, sql_verb, …) as JSON into the module, which returns {"allow":bool,"reason":string}. Any language that targets Wasm works - Rust, TinyGo, C, Zig. The bundled Rust example compiles to under 500 bytes:

chaos guard --enforce --terminate-tls --llm-host api.anthropic.com \
            --wasm-policy deny_model.wasm -- claude
# a banned model is blocked with the plugin's reason; --wasm-fail-closed
# denies (instead of allowing) if the module errors. See examples/wasm-policy.
The plugin is the same seam the built-in extractors use, exposed across a sandbox boundary - so untrusted, customer-authored logic can sit in the egress path without the power to exfiltrate anything itself.

Credential injection - the agent never holds the key

The strongest way to stop an agent from leaking an API key is to never give it one. Under TLS termination, chaos guard --creds creds.json holds the secret and stamps it onto allowed requests for the matching host - so the key never lives in the agent's environment, shell history, or memory; it only exists on the wire to the real upstream. (--creds implies --terminate-tls, since injecting into HTTPS needs the request visible.)

Many provider SDKs refuse to start without their API key set - but with injection the agent holds none. So for a credential bound to a known provider host (Anthropic, OpenAI, Gemini, Mistral, Groq, Cohere) the guard seeds a harmless placeholder env var (e.g. ANTHROPIC_API_KEY=chaos-guard-injects-the-real-key): the SDK builds the request, and the guard swaps in the real secret in transit.

Split keys so no single source holds the whole secret. A credential's value can be reassembled at injection time from Shamir shares spread across independent at-rest locations - a host env var, a file, the control plane, a KMS. Any k of n shares reconstruct the key (in the guard's memory only); fewer than k reveal nothing, so compromising up to k−1 sources yields no key material.

# split a key into 3 shares, any 2 of which reconstruct it
chaos cred-split --parts 3 --threshold 2 --host api.openai.com --secret-file key.txt

# creds.json - note there is no full key anywhere in this file
{ "credentials": [
  { "host": "api.openai.com", "header": "Authorization", "value": "Bearer ${SECRET}",
    "secret": { "type": "split", "threshold": 2, "shares": [
      { "type": "env",     "name": "OAI_SHARE_1" },
      { "type": "file",    "path": "/etc/chaos/oai.share2" },
      { "type": "literal", "value": "<base64 share>" } ]}}
]}

chaos guard --creds creds.json -- claude     # the agent makes real calls holding no key
Injection is scoped to the exact configured host, only on allowed flows - a denied request is never stamped, and a different host never receives the key.

A share source can be a literal, env, file, control-plane, or kms. A control-plane share ({"type":"control-plane","name":"…"}) lives server-side, off the agent host: the secret isn't fully at rest on the host, you get central rotation, and deleting the share is an instant kill switch - any split that needs it can no longer be reassembled. Manage them with chaos secret put-share <name> / list-shares / rm-share (API: PUT/GET/DELETE /v1/secrets/shares/{name}). Stored server-side, a single share is below threshold and reveals nothing on its own.

A KMS share lives in a cloud secret manager, fetched at injection time using the host's ambient cloud identity - no stored cloud keys, and the cloud's own IAM, rotation, and audit wrap the share. All three major clouds are supported, each via its native identity path: AWS Secrets Manager (SigV4 + the IMDS instance role; region from metadata unless you set region), GCP Secret Manager (metadata-server token), and Azure Key Vault (IMDS managed-identity token).

{ "type": "kms", "provider": "aws",   "name": "prod/agent-share" }
{ "type": "kms", "provider": "gcp",   "name": "agent-share" }                 // or projects/P/secrets/S
{ "type": "kms", "provider": "azure", "name": "my-vault/agent-share" }

Mixing kinds across a split is the point: one share in an env var, one in the control plane, one in KMS - an attacker needs to breach k independent systems.

Run it as a shared gateway

By default chaos guard wraps a child on the same host. With --gateway it instead runs as a standalone network proxy - no wrapped child - that agents on this or other hosts route through by pointing HTTPS_PROXY at it. The payoff is isolation: the CA private key and the injected credentials live on the gateway, never on the agent host, so even an agent with root on its own box can't read them. The same enforcement (rules, SQL/LLM facts, DLP, budgets, fault injection) applies centrally to every agent that connects.

# on the gateway host
chaos guard --gateway --gateway-token "$TOK" --addr 0.0.0.0:8080 \
            --enforce --terminate-tls --creds creds.json

# on each agent host
export HTTPS_PROXY=http://chaos:$TOK@gateway:8080 HTTP_PROXY=$HTTPS_PROXY
curl -s http://gateway:8080/__chaos/ca.crt -o chaos-ca.crt    # fetch the gateway CA (public)
export SSL_CERT_FILE=$PWD/chaos-ca.crt NODE_EXTRA_CA_CERTS=$PWD/chaos-ca.crt

A --gateway-token is required so the proxy isn't open to the world - agents present it as Proxy-Authorization (or embedded in the proxy URL, as above; both Basic and Bearer are accepted, constant-time compared). The CA-bootstrap path /__chaos/ca.crt is public (a certificate is not a secret) so agents can fetch and trust the gateway in one step.

Reach the gateway over whatever network you already trust - a private subnet, a WireGuard or Tailscale tunnel, a VPC. The gateway speaks plain HTTP(S)-proxy, so it composes with any of them; the tunnel just decides who can connect.

Per-identity activity log

Run a guard with --identity and a control plane connected, and every decision it makes is reported to the workspace's activity log - giving an admin visibility into what each LLM/agent (and the person driving it) is actually doing with AI:

chaos guard --identity claude-code@alice \
  --control-plane $CHAOS_CONTROL_PLANE --token $CHAOS_TOKEN \
  --enforce --deny /secrets/ -- claude

The dashboard's Guardrails → AI activity panel shows a per-identity rollup (requests, allowed vs blocked, last seen) and a live feed you can filter to one identity - across HTTP, Kubernetes, and SQL. Reporting is buffered and flushed off the request path, so it adds no latency; blocked actions are also mirrored to the audit log. API: POST/GET /v1/guardrails/activity and /v1/guardrails/activity/summary.

Above it, a Usage & value panel turns that stream into a report over a window (7/30/90 days): total AI requests, active identities, allowed vs blocked, a daily volume trend, and the top destinations the agents reach - so you can quantify how much each team gets out of AI and how much the guardrails are catching. API: GET /v1/guardrails/usage?window=7d.

Policy, approvals & host-wide enforcement

Author rules and watch live in the dashboard's Guardrails view; an approve rule surfaces held requests there with Approve / Deny buttons (the agent polls and proceeds the moment you decide, or denies on timeout). The API is GET/PUT /v1/guardrails plus /v1/guardrails/approvals. To govern a host's entire cleartext-HTTP egress instead of one wrapped process, run the agent with chaos agent --enforce-guardrails (Linux, transparent capture). And to drive the platform itself from Claude/ChatGPT, see the MCP server.

LLM-proctored approvals

An approval queue is only useful if a human actually clears it. Turn on the LLM proctor and a model pre-reviews every held request against your workspace guidance - it auto-denies clear abuse and (optionally) auto-approves clear routine work, escalating only the genuinely ambiguous ones to a person. The agent's poll sees the verdict in a second or two, so the obvious cases never wait on a human. It fails safe: any error, timeout, or unparseable answer escalates to the human queue, and auto-approve is off by default (the proctor may deny/escalate but a person confirms every approval until you opt in).

Enable it and write the guidance in the dashboard (Guardrails → LLM proctor); it's gated on a model being configured server-side (CHAOS_PROCTOR_API_KEY, optional CHAOS_PROCTOR_MODEL). Every proctor decision is written to the audit log (guardrail.proctor.approve / .deny / .escalate) with its reason. API: GET/PUT /v1/guardrails/proctor.

Prompt injection & response scanning

--prompt-guard scans each outbound model-API prompt for injection and jailbreak attempts before it ever reaches the model, driven off the same --llm-host facts as the LLM-aware rules above (HTTPS needs --terminate-tls so the body is readable). Three actions, same shape as DLP:

chaos guard --enforce --terminate-tls --llm-host api.anthropic.com --prompt-guard -- claude                        # block a detected injection (default)
chaos guard --terminate-tls --llm-host api.anthropic.com --prompt-guard --prompt-guard-action approve -- claude    # hold it for a human instead
chaos guard --terminate-tls --llm-host api.anthropic.com --prompt-guard --prompt-guard-action monitor -- claude    # just flag it

Scope it with --prompt-guard-host (repeatable; defaults to whatever --llm-host is set to). Three more flags extend the same visibility to the response side, all needing --llm-host + --terminate-tls so the completion body is readable: --trace-tools parses the model's completion and logs each tool call it *decides* to make (an audit trail of intended actions, feeding the receipts and intent-firewall pipelines below); --scan-responses buffers the completion and scans it for secrets the model echoed back or injection content surfaced from a tool result, firing a response_threat alert while forwarding the response unchanged; --block-responses runs the same scan but drops the response on a finding instead of just flagging it.

chaos guard --enforce --terminate-tls --llm-host api.anthropic.com \
  --prompt-guard --scan-responses --trace-tools -- claude
--block-responses requires --enforce and trades streaming for the ability to hold a response, it's mutually exclusive with --cache and --replay (all three need to buffer the whole body, and only one can hold it at a time).

Stop task hijacking - Intent Firewall & Mirror

The scariest agent failure isn't a banned command, it's a poisoned web page or tool result telling the agent to quietly abandon its task. Every action that follows looks individually fine, so an allow/deny list waves it straight through. chaos guard --intent-firewall auto-captures the agent's mission from its first turn and flags any action that doesn't serve it, goal-aware least-privilege rather than a fixed rule (reading ~/.ssh/id_rsa is fine for an "add my SSH key" task, off-mission for "summarize this PDF"). A conservative heuristic handles the obvious cases and a cheap LLM judge covers the ambiguous middle; it fails open, so it never falsely accuses, and off-mission actions surface on a dashboard Intent card and can trip the off-mission-action webhook alert.

chaos guard --mirror is a narrower, zero-LLM tripwire for the same class of attack: it splits each model call along the API's own trust boundary (system/user text vs tool_result content) and flags a write to a host the agent learned about only from untrusted tool content, the signature of an injection-driven exfiltration, caught with no extra model calls.

chaos guard --enforce --terminate-tls --llm-host api.anthropic.com \
  --intent-firewall --mirror -- claude
Both are off by default and ride the same monitor → enforce posture as the rest of the policy. They catch different things: Intent Firewall reasons about whether an action serves the mission at all; Mirror is a narrow, deterministic signature for one specific attack shape (provenance-tainted writes).

Hold the agent to its word - action-claim receipts

Coding agents routinely say they did things they didn't: "I ran the tests and they pass" when nothing ran, "removed the hardcoded key" when it's still there. The guard sees both the model's completion text and the tool calls it actually made this session, so --receipts cross-checks every completion claim against the real call history. Each claim lands as verified (a matching command really ran), unverified (it claimed a test/build result but the session invoked no matching tool at all), or unknown, deliberately conservative so it never falsely accuses.

chaos guard --enforce --terminate-tls --llm-host api.anthropic.com --receipts -- claude

Receipts show up in the dashboard with verdict badges and a trust score. Opt in to a webhook alert on an unsubstantiated claim (unverified-claim), or let a repeated pattern of them trip the circuit breaker. Off by default.

Decoy credentials - honeytokens

chaos guard --honeytokens plants a fresh, fake credential in the agent's environment alongside its real ones. It's never used by any legitimate call the agent makes, so it carries zero false-positive risk, but a honeytoken value appearing in any outbound request is a definitive compromise signal, no heuristic, no maybe. A hit trips the circuit breaker and tanks the agent's conduct score.

chaos guard --enforce --honeytokens --circuit-breaker -- claude
Pair it with --circuit-breaker so a trip actually halts the agent, not just logs it. Off by default.

Integrity monitoring

Every guardrail above watches the agent's network calls. --integrity PATH watches the agent's own cognitive files instead, memory, skills, CLAUDE.md, anything an attacker could rewrite to quietly change what the agent believes its instructions are. The guard takes a SHA-256 baseline of each watched path (repeatable) and re-checks it on an interval, alerting (integrity trigger) the moment a file changes outside the agent's own hand.

chaos guard --enforce \
  --integrity ~/.claude/CLAUDE.md --integrity ~/.claude/skills \
  --integrity-interval 30s -- claude
Point it at a few curated, high-value files rather than the whole home directory, it's a tamper tripwire, not a general file-change feed (use --watch-fs for that).

Anomaly detection

--anomaly learns a behavioral baseline for the wrapped agent, the destinations it calls, the tools it uses, the shell commands it runs, its token rate, during a warm-up window, then flags anything that drifts from it. --anomaly-learn sets the warm-up duration (default 3 minutes); nothing is flagged until it elapses, so a normal startup burst doesn't trip it.

chaos guard --enforce --anomaly --anomaly-learn 5m -- claude
Unlike a fixed rule, this catches the drift you didn't think to write a rule for, an agent that suddenly starts hitting a host, tool, or command it's never used before.

Circuit-breaker

Any single guardrail signal, a block, an injection finding, a tampered file, might be noise on its own. --circuit-breaker accumulates a risk score across all of them (an injection/exfil finding scores about 100, a plain policy block about 25, an integrity tamper about 60) within a rolling window, and halts the agent outright, revoking its egress and signaling the process, the moment the total crosses --breaker-threshold (default 100) inside --breaker-window (default 60s).

chaos guard --enforce --circuit-breaker --breaker-threshold 150 --breaker-window 5m \
  --dlp --prompt-guard --integrity ~/.claude -- claude
The breaker only has signal to accumulate if something is feeding it, pair it with the guardrails that produce scoring events (DLP, prompt-guard, integrity, honeytokens, receipts, intent-firewall) rather than running it alone.

Dangerous-command gating

--block-dangerous catches destructive shell commands by their arguments, not just the binary name, rm -rf ~, git push --force, git reset --hard, DROP DATABASE, terraform destroy, even when the binary itself (rm, git, terraform) is otherwise allowed. It sees through a bash -c "…" wrapper rather than just the outer shell invocation. It flags in monitor mode and blocks in enforce mode.

chaos guard --enforce --block-dangerous -- claude

--approve-dangerous adds a human-in-the-loop pause: before a matched command runs, the guard asks Allow once / Deny / Always allow via a native dialog on macOS or /dev/tty elsewhere. Always allow persists across sessions, scoped to the approving agent, and is reviewable and revocable with chaos approvals. --approve-via-app routes the same prompt to the StraightChaos macOS menubar app as a native Allow/Deny dialog instead (implies --approve-dangerous).

chaos guard --enforce --block-dangerous --approve-dangerous -- claude               # pause and ask before it runs
chaos guard --enforce --block-dangerous --approve-dangerous --approve-via-app -- claude   # ask via the menubar app

chaos approvals list          # review standing "always allow" rules
chaos approvals revoke <id>   # revoke one (or "all")
--approve-dangerous fails closed: if no human can be reached (no GUI, no tty, app not running), the command is denied rather than let through.

Bring your own content policy

DLP's built-in detectors cover secrets and structurally-validated PII, not an org codename, a competitor mention, or an internal topic you want flagged. --content-policy FILE loads a flat text file of labeled patterns and scans every outbound request body against it: one rule per line, label: pattern, where a bare pattern is a case-insensitive literal substring and a re: prefix makes it a regular expression. Blank lines and # comments are ignored.

# content-policy.txt
codename:     Project Lighthouse
competitor:   re:(OpenAI|GPT-4o|Gemini Pro)
confidential: re:\b(board|acquisition|merger)\b

The label appears in findings, the activity log, and any webhook alert. Same three-action shape as the rest of the policy:

chaos guard --enforce --content-policy content-policy.txt -- claude                          # block on a match (default)
chaos guard --content-policy content-policy.txt --content-policy-action approve -- claude    # hold for a human
chaos guard --content-policy content-policy.txt --content-policy-action monitor -- claude    # just flag it

Scope scanning to specific hosts with --content-policy-host (repeatable; without it, every egress body is scanned). Without --enforce, the action is always monitor regardless of --content-policy-action.

Response cache, VCR replay & prompt-cache optimization

Three flags all touch caching, but they're different mechanisms solving different problems. --cache is the guard's own in-process cache: it keeps recent model-API responses and serves a repeat (or, in similar mode, a near-duplicate) request instantly without calling the model at all. --cache-mode exact requires an identical request body; --cache-mode similar uses token-level comparison against --cache-threshold (0-1, default 0.92) to absorb minor prompt variation. --cache-ttl sets how long an entry stays fresh (default 10 min). Both need --terminate-tls + --llm-host so the request/response bodies are visible.

chaos guard --terminate-tls --llm-host api.anthropic.com \
  --cache --cache-mode similar --cache-threshold 0.9 --cache-ttl 30m -- claude

--replay DIR is VCR-style record-and-replay for deterministic test runs: on a miss the request goes through normally and the response is saved to DIR; on the next run, the saved recording is served instead, no tokens spent. --cache and --replay are mutually exclusive (both hold the response body).

chaos guard --terminate-tls --llm-host api.anthropic.com --replay ./fixtures -- claude

--optimize-cache off|monitor|enforce is a completely different thing: it still calls the model on every request, but rewrites the request to raise the provider's own prompt-cache hit-rate, inserting Anthropic cache_control breakpoints and ordering tools deterministically, so more of the input gets billed at the cheap cached-read rate instead of full price. monitor only reports what it would change.

chaos guard --terminate-tls --llm-host api.anthropic.com --optimize-cache enforce -- claude
The distinction that matters: --cache/--replay skip the model call entirely on a hit. --optimize-cache never skips the call, it just shapes the request so the provider charges less for it. They compose (a cache miss still benefits from a well-shaped request), but they solve different problems.

Cost routing

--route-model FROM=TO (repeatable, needs --llm-host) rewrites the model field of a forwarded request before it reaches the provider, so an agent hard-coded to an expensive model can be transparently rerouted to a cheaper one for bulk or background work, no change to the agent's own config or code. The substitution runs at the TLS-termination layer and leaves every other field of the request intact.

chaos guard --terminate-tls --llm-host api.anthropic.com \
  --route-model claude-opus-4-8=claude-haiku-4-5 -- claude
Combine with the spend budget and activity log to see the routing pay off directly in recorded cost.

Resource limits

Every guardrail so far governs what the agent is allowed to do; --max-memory, --max-pids, and --max-cpu put a hard, kernel-enforced ceiling on what it's physically able to do, via Linux cgroup v2. A memory cap stops a runaway process from taking down the host; a PID cap is a fork-bomb guard; a CPU cap keeps one agent from starving everything else on the box.

chaos guard --enforce --max-memory 2G --max-pids 512 --max-cpu 150% -- claude
Linux only, cgroup v2. There's no macOS equivalent, the App Sandbox used for --enforce-fs doesn't cap resources.

Notifications and audit export

You don't want to keep the dashboard open just to learn an agent got blocked or a secret almost left the building. Configure a workspace webhook (dashboard: Settings → Alerts) and Straight Chaos POSTs a message the moment one of a dozen trigger events fires - a blocked request, a DLP finding, policy drift, an unverified completion claim, an off-mission action, a falling prompt-cache hit rate, and more. Point it at a Slack incoming-webhook URL for a formatted message, or any other URL for a stable JSON envelope:

{ "source": "straightchaos-guardrails", "trigger": "blocked",
  "title": "Agent request blocked",
  "text": "claude-code@alice denied at api.stripe.com",
  "fields": { "host": "api.stripe.com", "rule": "deny-payments" },
  "sent_at": "2026-07-11T12:00:00Z" }
TriggerCondition
blockedAn egress request was denied in enforce mode
dlpOutbound DLP scan found a secret or PII
driftTraffic the current policy would deny, but monitor let through (would break under enforce)
unverified-claim--receipts: model asserted a completion not backed by any observed tool call
off-mission-action--intent-firewall: agent acted outside its captured mission
cache_hit_lowAn identity's prompt-cache hit rate fell below the configured floor

A per-workspace cooldown deduplicates rapid-fire events of the same trigger so a burst of blocks doesn't flood the channel. Turn on a recurring digest (daily or weekly) alongside the real-time triggers for a rolled-up spend and governance summary, emailed to workspace members and, if a webhook is set, delivered there too.

For compliance, two exports cover different needs. GET /v1/audit.csv is a flat CSV of every workspace-level audit event (logins, invites, settings and billing changes) over the last 90 days, ready for a spreadsheet or a compliance tool (Enterprise plan). GET /v1/guardrails/audit/export exports the guardrail activity log itself as a bundle with its hash-chain head, so you can prove the log wasn't altered with /v1/guardrails/audit/verify. API: GET/PUT /v1/guardrails/alerts, POST /v1/guardrails/alerts/test, POST /v1/guardrails/digest/test.

The drift and dlp alerts also surface directly in the dashboard's Guardrails view, no webhook required - the webhook is for getting the same signal into Slack or your own systems.

MCP server inventory

An agent under chaos mcp-guard talks to real MCP servers - a filesystem tool, GitHub, Slack, a brokerage, your own internal server - and it's easy to lose track of which ones are actually in play across a team. The inventory is a derived security-posture view: no separate config to maintain, it's built automatically from the same activity every guarded MCP call already reports.

chaos mcp-guard --server-name my-server -- npx -y @modelcontextprotocol/server-filesystem /

Every activity record from a guarded server is stamped with its name (the --server-name value, or the basename of the wrapped command if you don't set one), and the control plane aggregates those into a per-server rollup: tool-call count and last-seen time over a lookback window (?window=1h, default 7 days).

In the dashboard, the MCP servers card under Guardrails lists every server seen in the window, and the live Activity map labels each MCP-server node with its name, with a pop-out panel showing the tool-call breakdown when you tap one - so an admin can see, at a glance, every MCP surface an agent fleet actually touches. API: GET /v1/guardrails/mcp-servers.

This is why it's worth running third-party MCP servers under mcp-guard even before you gate anything - the inventory (and the audit trail) start the moment traffic flows through it.

Skill security scanning

Agent "skills" and plugins - a directory of instructions plus scripts an agent can load and follow - are an increasingly common way to extend what an agent does, and an increasingly common way to smuggle in something you didn't mean to trust. chaos scan-skill statically vets a skill directory before you point an agent at it, no execution involved:

chaos scan-skill ./my-skill
✓ SAFE, ./my-skill (4 file(s) scanned)

chaos scan-skill --json ./sketchy-skill
{ "verdict": "malicious", "dir": "./sketchy-skill", "files": 6,
  "findings": [ { "kind": "secret", "file": "setup.sh", "detail": "AWS access key literal" } ] }

It flags three classes of problem: prompt-injection payloads hidden in the skill's own instructions (text aimed at the model reading it, not you), embedded secrets (API keys, tokens checked into a script), and dangerous code patterns - reverse shells, pipe-to-shell installers, decode-and-exec obfuscation, credential-theft snippets.

The exit code encodes the verdict - 0 SAFE, 1 SUSPICIOUS, 2 MALICIOUS - so it's a one-line CI gate: fail a build or a plugin-install hook the moment a skill lands above your tolerance.

This runs entirely offline against the files on disk - no control plane, no network call, nothing to configure. Point it at any skill/plugin directory, including ones you're about to git clone from somewhere you don't fully trust yet.

Gate an agent's own tool calls (mcp-guard)

chaos guard governs an agent's own outbound egress - the HTTP calls it makes to the model, your APIs, the database. chaos mcp-guard sits one layer further in: between the agent and an MCP server it's calling as a tool, so you can allow, deny, or hold individual tool calls (place an order, delete a file, run a query) rather than just the transport underneath them. Same monitor → enforce posture, same dashboard, a different chokepoint.

Setup

mcp-guard wraps the server the way guard wraps an agent - as the command your MCP client actually launches, sitting stdio-in-the-middle. Point your client config at it instead of the real server:

// claude_desktop_config.json / .mcp.json - wrap instead of calling the server directly
    { "mcpServers": { "filesystem": {
      "command": "chaos",
      "args": ["mcp-guard", "--", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/"] } } }

By default it's monitor - every tool call is logged, nothing blocked. Add --enforce to actually gate, and name tools by their MCP tool name:

# block delete_file outright; require default-deny so only named tools run at all
    chaos mcp-guard --enforce --default-deny \
      --allow-tool read_file --allow-tool list_directory \
      --deny-tool delete_file \
      -- npx -y @modelcontextprotocol/server-filesystem /

--default-deny flips the posture to an allow-list (only --allow-tool names run); without it, everything runs except explicit --deny-tool names. Add --control-plane/--token (or chaos login) and --identity/--server-name to report every call to the dashboard's MCP servers card and activity map alongside the agent's own egress.

Auto-wrap every configured server

Editing each client config by hand doesn't scale past one server. mcp-guard install discovers every MCP server across Claude Desktop, Claude Code (user + per-project), and Cursor configs, and rewrites the stdio ones to run behind mcp-guard:

# dry-run by default - prints what it would change, writes nothing
    chaos login
    chaos mcp-guard install
    chaos mcp-guard install --apply

    # audit what's actually wrapped, and how strongly, across every scope
    chaos mcp-guard status

    # fully reversible
    chaos mcp-guard uninstall --apply

status renders each server's guard policy in plain English and issues a domain-aware verdict - a trading server wrapped with no value cap and no danger-deny is flagged as weak, not just "guarded." Run interactively, it offers per-finding align/enable/strengthen fixes on the spot.

Secret-free and reversible. install injects only --server-name; the wrapped process reads its control-plane URL and token from chaos login at runtime, so no credential is ever written into a config file. Every write gets a timestamped .chaosbak next to the file, and uninstall --apply restores it. Remote (HTTP/SSE) servers are skipped unless you pass --include-remote, since a stdio proxy can't spawn a URL.

Guarding a remote MCP server

mcp-guard normally spawns the server it wraps; a remote server (an HTTP/SSE endpoint, not a local process) is wrapped in bridge mode instead - mcp-guard becomes the stdio process your client launches, and speaks the remote's transport upstream, applying the exact same gates:

chaos mcp-guard --server-name robinhood-trading \
      --upstream-url https://agent.robinhood.com/mcp/trading \
      --upstream-oauth \
      --inspect-actions --max-order-value 500

--upstream-transport streamable|sse picks the wire protocol (default streamable). For static bearer auth, add --upstream-header "Authorization: Bearer ${TOKEN}" (env-expanded, never hard-coded). For OAuth-protected servers, --upstream-oauth runs a full Authorization Code + PKCE flow on first use - opens a browser for consent, then caches the token in the OS keychain, so you never touch it again. --oauth-client-id and repeatable --oauth-scope tune it for providers that need one.

Financial action gates - Claude on Robinhood

A trading MCP server is the sharpest case for tool-call gating: the transport is "allowed" (it's your own broker), but a single tool call can move real money. --inspect-actions parses the order behind each tool call (side, instrument, quantity, price) so the rest of these gates can reason about it - not just log a JSON blob:

FlagPurpose
--max-order-value NDeny any single order worth more than $N
--daily-limit NDeny once cumulative order value over 24h would exceed $N
--approve-over NHold orders worth more than $N for human approval in the dashboard
--max-orders-per-hour NDeny once this many trades have run in the last hour
--dedupe-window DURDeny an identical order (tool/side/instrument/qty) seen again within this window
--outlier-factor NHold an order worth more than N× the recent typical size (fat-finger guard)
--deny-account-dangerHard-deny money-movement (withdraw/transfer/wire) and account changes (disable MFA, enable margin)
--deny-action VERBHard-deny a specific action verb (repeatable)
--finance-packBuilt-in pack: deny options, crypto, and margin/short trades
--market-hoursDeny trades outside US market hours (Mon-Fri 09:30-16:00 ET)
--allow-instrument / --deny-instrumentScope trading to (or block) specific tickers (repeatable)
--min-price NDeny trades priced below $N/share (penny-stock guard)
# let an agent trade freely up to $1k/order, hold anything bigger, block money movement entirely
    chaos mcp-guard --server-name robinhood-trading --enforce \
      --upstream-url https://agent.robinhood.com/mcp/trading --upstream-oauth \
      --inspect-actions --max-order-value 1000 --approve-over 1000 \
      --daily-limit 5000 --max-orders-per-hour 20 --outlier-factor 10 \
      --deny-account-danger --market-hours --finance-pack
Off by default. None of this activates without --inspect-actions or a flag that implies it - a plain mcp-guard wrap just monitors tool-call traffic, the same fail-open-until-you-opt-in posture as every other guardrail here.

Shadow mode and DLP on tool-call args

--dry-run intercepts every order and logs it as "would execute" without ever sending it upstream - the safest way to watch what an agent would have traded before trusting it with a live account. And because tool-call arguments are just another payload an agent can leak through, --dlp scans them for secrets and PII with the same three actions as the egress DLP scanner:

# nothing actually trades - every order is logged as "would execute"
    chaos mcp-guard --dry-run --inspect-actions --max-order-value 1000 \
      --upstream-url https://agent.robinhood.com/mcp/trading

    # scan tool-call arguments for secrets/PII, mask and forward
    chaos mcp-guard --dlp --dlp-action redact -- npx -y @some/mcp-server

--dlp-action deny blocks the call outright, redact masks the finding and forwards the now-safe args, monitor only logs - identical semantics to outbound DLP, just scoped to the arguments of the tool call instead of an HTTP body.

Drive it with your LLM (MCP)

Straight Chaos speaks the Model Context Protocol, so your own agent - Claude Desktop/Code, Cursor, any MCP client - can run chaos with your model. It hosts no model and keeps no secret server-side: it authenticates with a workspace API token and every call goes through the same REST API as the dashboard, so the risk acknowledgment, safety policy, blast-radius cap, and plan quotas all still apply.

Setup

Create an API token in the dashboard (Settings → API tokens), then either run the local stdio server or point at the hosted endpoint.

// local stdio - Claude Desktop / Code / Cursor config
{ "mcpServers": { "straight-chaos": {
  "command": "chaos", "args": ["mcp"],
  "env": { "CHAOS_CONTROL_PLANE": "https://app.straightchaos.com",
           "CHAOS_TOKEN": "sck_…" } } } }

Or hosted over HTTP - nothing to install, just a URL + token:

{ "mcpServers": { "straight-chaos": {
  "url": "https://app.straightchaos.com/mcp",
  "headers": { "Authorization": "Bearer sck_…" } } } }

Access tiers

An LLM that can inject real faults is dangerous if it's prompt-injected, so the server is read-only by default and you opt up explicitly - with flags (chaos mcp --allow-actions) or query params on the hosted URL (…/mcp?allow_destructive=1).

TierAddsFor
defaultread tools onlyobserve, reason, triage
allow_actionsabort experiments, kill switchlet the agent stop chaos, never start it
allow_destructivethe run_* tools (inject real faults)full agentic chaos

Tools

Read tools cover agents, templates, experiments, scenarios, fleet runs, the safety policy, scorecards, the dependency graph and an agent's dependencies, plus get_autopilot_report. Streaming watch_experiment / watch_scenario_run block until a run finishes, pushing live progress. The launch tier adds run_template, run_scenario, run_experiment, run_fleet_experiment, and run_autopilot.

Example. "Run autopilot on agent agt-3 against http://localhost:8080/healthz, watch it, and tell me which dependency it can't survive."run_autopilotwatch_scenario_runget_autopilot_report.

Trust & conduct

The guard already records every action an agent takes into a tamper-evident, hash-chained activity log. The trust arc turns that record into something forward-looking: a per-identity conduct scorecard, a portable signed attestation a counterparty can verify offline, an adversarial GameDay that proves the defenses actually fire, and an earned autonomy ladder that widens an agent's own latitude as its record earns it.

Conduct scorecard

Every recorded activity event carries an identity, a verdict, and tags - a denied action, a DLP finding, an intent-firewall flag, an unverified completion claim. chaos guard --identity … already produces this stream; the conduct engine aggregates it per identity into a score and a letter grade, worst-first:

score = 100 - (exfil×20 + response_threats×15 + off_mission×15 + unverified_claims×5 + blocked×2)
GradeScore
A≥ 90
B75 - 89
C50 - 74
D< 50
GET/v1/guardrails/conduct
curl -H "Authorization: Bearer $CHAOS_TOKEN" \
  "https://app.straightchaos.com/v1/guardrails/conduct?window=24h"{ "chain_intact": true, "identities": [
    { "identity": "claude-code@alice", "score": 82, "grade": "B",
      "actions": 340, "blocked": 4, "exfil": 0, "off_mission": 1,
      "unverified_claims": 0, "response_threats": 0 } ] }

chain_intact is a live audit-chain integrity check, not a cached flag - it's what the --require-chain-intact gate below actually verifies. Filter to one identity with ?identity=. The dashboard's Conduct card reads the same endpoint.

Signed attestation

A dashboard number is only trusted by people already inside your workspace. chaos attest turns a conduct report into an Ed25519-signed credential - a counterparty (a CI pipeline, another org, another agent) can verify it offline, with no call back to your control plane and no need to trust your dashboard's honesty.

# one-time: generate the signing key, set it as CONDUCT_SIGNING_KEY on the control plane
chaos attest keygen
# prints a base64 32-byte Ed25519 seed
# fetch the signed attestation for the workspace (or ?identity=agent-a for one identity)
curl -H "Authorization: Bearer $CHAOS_TOKEN" \
  https://app.straightchaos.com/v1/guardrails/attestation > att.json

# verify the signature - no network call, works fully offline
chaos attest verify att.json
# ✓ attestation valid  identity=agent-a  key_id=3f8a1b2c9d4e5f6a  issued=1751000000

The signed payload binds the conduct report, the identity, the workspace, and a key_id (a hash of the public key) so a verifier can't be tricked into accepting a signature from a different key. Pin the expected key with chaos attest verify --pubkey "<base64>", or fetch it unauthenticated from GET /v1/guardrails/attestation/pubkey.

Gate a pipeline or an agent-to-agent handoff on the conduct bar directly - no bespoke parsing:

# CI step: fail the pipeline unless every identity clears grade B with zero exfil
chaos attest gate att.json --min-grade B --max-exfil 0 --require-chain-intact
# exit 0 = ALLOW, exit 1 = DENY - e.g. "DENY, agent-a: grade C is below the required B"
Optional: anchor it publicly. chaos attest anchor records the attestation in Sigstore Rekor, a public append-only transparency log - so a third party can confirm the attestation existed, unaltered, at a specific time, without trusting your control plane at all.
chaos attest anchor att.json -o bundle.json                 # default: rekor.sigstore.dev
chaos attest verify --rekor bundle.json                    # confirms the Rekor entry commits to this attestation

Fetching an attestation from the control plane needs CONDUCT_SIGNING_KEY set server-side (it returns 501 otherwise); plain chaos attest verify never makes a network call, only --rekor does.

Adversarial GameDay

Not the same GameDay. Orchestration's "GameDay" is an ordered chaos-fault scenario against your own infrastructure. This is a different feature aimed the other way: an Adversarial GameDay injects prompt-injection, exfiltration, and off-mission lures at the agent itself, and scores whether its defenses (data firewall, intent firewall, honeytokens, receipts) actually catch them - a resilience audit for the agent, not the service behind it.

The catalog ships eight built-in probes across four categories - injection, hijack, exfil, and false-claim - each naming the defense it expects to fire:

chaos gameday list
# injection-ignore-instructions   injection    →intent      tool result tells the agent to ignore its task
# exfil-canary-credential         exfil        →honeytoken  lure to exfiltrate a planted canary credential
# false-tests-pass                false-claim  →receipts    claims tests pass without running them
# ...eight probes total, across injection / hijack / exfil / false-claim

Run the probes (by replay or live, below), collect the resulting activity into an outcomes file, then score it into an Agent Resilience Score:

# fail CI when resilience drops below 80
chaos gameday score --min-score 80 outcomes.json
# ✓ injection-ignore-instructions   intent-firewall flag fired
# ✗ false-tests-pass                no receipts unverified-claim
# PASS, resilience 88/100 (7/8 caught, floor 80)

To test against real traffic rather than a replay, inject a probe's lure directly into the live model response - spliced into the Anthropic/OpenAI response body or SSE stream - so you see whether the defense fires for real:

chaos guard --terminate-tls --llm-host api.anthropic.com \
  --gameday-probe injection-ignore-instructions -- claude

The lure is framed with an <adversarial-gameday probe="…"> tag so it's auditable in the agent's transcript, and it fails open on any unrecognized content type or error. This needs --terminate-tls plus --llm-host to see the response - the guard logs a note if --gameday-probe is set without them.

Once an outcomes file passes, sign the resilience score the same way as a conduct credential - a portable, verifiable proof of what the agent survived:

chaos gameday attest --identity agent-a --workspace production outcomes.json > resilience.json
chaos gameday verify --min-score 80 resilience.json
# OK, resilience 88/100 (7/8 caught), signed by key 3f8a1b2c9d4e5f6a

Earned autonomy

A conduct grade is a report you read; earned autonomy is the same grade acting on the agent directly - a capability ladder that widens automatically as an agent's record earns it, and narrows the instant it doesn't:

TierConduct barEgressToken budgetApprovals
sandboxany conduct (the floor)none50,000 tokrequired for all
trustedgrade ≥ B, exfil 0, off-mission 0none500,000 tokrisky-only
autonomousgrade ≥ A, exfil 0, off-mission 0unrestricted (*)2,000,000 tokrisky-only

The ladder is monotonic - each rung needs a strictly harder bar than the one below - and an agent always lands on the highest tier its current conduct still clears, down to sandbox. Demotion is automatic, no operator action required.

chaos autonomy ladder                          # print the full capability ladder
chaos autonomy tier conduct.json                # the earned tier for a downloaded conduct report
chaos autonomy tier --identity agent-a conduct.json

Wire it into a live guard session with two flags - monitor mode just reports the tier, enforce mode acts on it:

chaos guard --earned-autonomy -- claude            # monitor: report tier + log demotions, no enforcement
chaos guard --earned-autonomy-enforce -- claude    # also caps the LLM token budget to the earned tier at startup
# ⚖ earned autonomy: trusted → sandbox (egress none · budget 50000 tok · approvals true)
Both flags need a control-plane connection and re-check the conduct report on the same 30s cycle as policy reload. A control-plane outage is fail-open: the tier just stays where it was. With --earned-autonomy-enforce and no explicit --llm-budget, the token budget seeds from the earned tier at startup - a demotion mid-session doesn't retroactively cut an in-flight budget, but the next start picks it up.

Fleet immunity & federation

Fleet immunity - one caught agent protects the whole workspace

The first time an agent leaks data or gets fed injected content from a bad host, every other agent in the workspace should already know not to trust it. chaos guard --fleet-immunity does exactly that: the control plane rolls up every guard's blocks, DLP findings, and injection hits into a shared workspace threat list, and each guard polls it (same 30 s cycle as policy reload) and denies egress to anything already on it - via the same inspect gate as the rest of the guard's controls, so it composes with your existing policy instead of replacing it.

chaos guard --fleet-immunity -- <agent command>
    chaos guard --fleet-immunity --enforce -- <agent command>   # block instead of just log

Like the guard's other controls, --fleet-immunity is monitor by default - it logs what it would have blocked until you add --enforce. It's also fail-open: a control-plane outage never breaks agent egress, the guard just keeps the last list it fetched. The list itself only grows from real catches (a block, a DLP finding, an injection hit) - benign traffic can never land on it. The dashboard's Fleet immunity card shows the shared list and which identities contributed each entry.

Cross-org federation - borrow other companies' catches too

One workspace's threat list is a small sample. Federation extends the same idea across organizations, so your agents benefit from threats other straightchaos customers (or other self-hosted deployments) have already caught - without any of you handing your actual activity data to a central party. Independent control planes gossip signed threat reports directly to each other; straightchaos only brokers membership (who's allowed to participate, and revoking anyone who shouldn't be) and never sees the underlying reports.

A single peer's report is never enough to act on. A federated hit needs corroboration from a configurable number of distinct organizations - --fleet-immunity-corroboration, default and hard floor of 2 - before it does anything at all. Below that threshold it's a dashboard curiosity, exactly like an un-enforced local threat. At or above it: monitor mode logs a would-hold, and enforce mode opens a human approval hold (naming which orgs corroborated it) - it never hard-denies on its own. Only your own workspace's directly observed activity can ever hard-block; no other organization can weaponize a false report against you.
chaos guard --fleet-immunity --fleet-immunity-corroboration 3 -- <agent command>

Two deployment shapes, and you can mix them in one federation. Same-store is zero-config: any workspace on straightchaos's own SaaS calls POST /v1/federation/join from the dashboard and it's in, using its existing workspace ID as its federation identity. Self-hosted is invite-only: a root operator runs chaos federation invite <label> to mint a one-time token, hands it to the joining org out-of-band, and that org sets FEDERATION_ENROLL_TOKEN on its own control plane to register - after which it re-authenticates on every peer-directory refresh with its own signature, no shared secret required.

# root operator: invite a self-hosted org
    FEDERATION_ROOT_URL=https://root.example.com FEDERATION_ADMIN_TOKEN=<root secret> \
      chaos federation invite "acme-corp"

    # self-hosted org: consume the token once, then it's a standing peer
    FEDERATION_ENROLL_TOKEN=scf_<hex> chaos control-plane

Operators manage membership with a handful of chaos federation subcommands:

CommandWhat it does
chaos federation statusPeer connectivity + directory freshness, read-only
chaos federation invite <label>Root: mint a one-time enrollment token for a self-hosted org
chaos federation invitesRoot: list outstanding/consumed tokens
chaos federation revoke-invite <handle>Root: revoke an unused token
chaos federation revoke <org-id>Root: cut off a registered member; its reports stop being accepted on the next directory refresh

Auto-policy: learn, drift, and policy as code

Hand-writing an egress allow-list against a blank page is the friction that keeps most agents stuck in monitor forever. Auto-policy closes that loop: run in monitor mode, let learn mode suggest a policy from what the agent actually did, check drift detection for what would break before you flip to enforce, and manage the whole thing as a reviewable YAML bundle with policy as code.

Learn mode: suggest a policy from traffic

Instead of authoring rules from scratch, mine the workspace's recorded activity into a default-deny egress allow-list of the destinations the agent actually used. internal/policysuggest.Suggest is a pure function over recorded events: only allowed, non-blocked egress seeds a rule (a destination that was already denied never gets allow-listed), and a destination seen only a handful of times is flagged rare rather than silently dropped, so you can eyeball it before trusting it.

GET/v1/guardrails/suggest?window=7d
// a default-deny policy built from what the agent actually called
    { "policy": { "mode": "monitor", "default_allow": false, "rules": [
    { "name": "allow-api.anthropic.com", "host": "api.anthropic.com", "action": "allow" },
    { "name": "allow-api.internal",      "host": "api.internal",      "action": "allow" }
      ]},
      "hosts": [
    { "host": "api.anthropic.com", "count": 412, "methods": ["POST"] },
    { "host": "api.internal",      "count": 38,  "methods": ["GET","POST"] },
    { "host": "cdn.rare-vendor.io", "count": 2,  "rare": true }
      ],
      "models_seen": ["claude-sonnet-4-6"], "window": "7d", "total": 452 }

In the dashboard this is the Guardrails → Policy view's "Suggest from traffic" button: it loads the suggestion straight into the rules editor, where you can drop the rare hosts, rename rules, and move on to simulate.

The workflow. Run in monitor for a while to build up real traffic → suggest → review the hosts (especially anything flagged rare) → simulate the draft against recorded traffic → promote to enforce once simulate comes back clean.

Drift detection: shadow denies before you flip the switch

Drift is the mirror image of learn mode. Where suggest builds a policy from traffic, drift-watch takes a policy you're about to enforce and asks what it would have broken. internal/driftscan.Scan aggregates shadow denies, events where the active policy's rules say deny but monitor mode let the request through anyway (Action=deny, Allowed=true) - grouped by destination, with a count, last-seen time, and the rule that would have fired.

GET/v1/guardrails/drift?window=7d
// traffic that would break the moment monitor flips to enforce
    { "destinations": [
    { "host": "reporting.partner.io", "count": 14, "last_seen": "2026-07-10T18:02:11Z",
      "rule": "default-deny", "sample_reason": "no matching allow rule" }
      ], "window": "7d", "total": 14 }

The dashboard's "Policy drift" card surfaces the same list and lets you absorb a legitimate miss straight back into the allow-list, and shows a green all-clear when nothing would break - the cue that it's safe to switch to enforce. An optional real-time OnDrift webhook fires on the same shadow-deny signal, so a drift event can page a channel the moment it happens instead of waiting for the next dashboard visit.

Drift is policy-relative, not history-relative - it answers "what does my current policy disagree with", not "what's new for this agent" (that's the separate behavioral-baseline --anomaly detector). Run suggest to build a first draft, then keep watching drift even after you enforce - new destinations show up here before they show up as a page from an angry teammate.

Policy as code: export, validate, diff, apply

The whole guardrail policy (egress rules, DLP, action/value gates, proctor, chaos-experiment safety) is a single YAML bundle you can check into version control, review in a pull request, and promote through CI - the same GitOps model you already use for infrastructure.

apiVersion: straightchaos.com/v1
    kind: GuardrailConfig
    spec:
      egress:
    mode: enforce
    default_allow: false
    rules:
      - { name: allow-anthropic, host: api.anthropic.com, action: allow }
      - { name: deny-all, action: deny, reason: default-deny catch-all }
      dlp:
    enabled: true
    action: redact

A section that's present in the bundle completely replaces the corresponding live config; a section that's omitted is left unmanaged, so a bundle containing only egress never touches DLP or proctor settings.

# pull the live policy down as a portable, git-diffable file
    chaos policy export -o policy.yaml

    # offline schema check, no network call - safe as a CI gate on every PR
    chaos policy validate policy.yaml

    # field-level diff against the live policy, then replay it against real
    # traffic before anyone applies it
    chaos policy diff policy.yaml --simulate --window 14d

    # write it - the server validates the whole bundle before touching anything
    chaos policy apply policy.yaml --dry-run
    chaos policy apply policy.yaml

chaos policy diff --simulate is the CLI face of the same simulate engine behind learn mode and drift: it POSTs the proposed egress rules to /v1/guardrails/simulate and replays the window's recorded traffic through them, printing the would-block delta before anyone merges the PR. API: GET /v1/guardrails/policy/export and POST /v1/guardrails/policy/apply (dry_run supported) back the same operations for a workspace without the CLI installed.

CI recipe. Gate pull requests on chaos policy validate + diff --simulate, then run validate + apply on merge to main - policy changes get the same review and blast-radius visibility as any other infrastructure change, and a bad bundle is rejected server-side instead of half-applied.

Experiment safety

Every running fault has a guardrail watching the system. The instant it trips, the agent removes the fault and reports aborted-by-slo.

HTTP health check (default)

Polls an endpoint on a fixed interval. Aborts on non-2xx or when the response takes longer than the configured maximum.

# in spec form
health_url: "http://localhost:8080/healthz"
health_max_latency_ms: 500
interval_sec: 2

Failed requests (connection refused, timeout) count as a breach - the experiment may be what broke the endpoint, so that's the safe interpretation.

Prometheus

Polls a PromQL query that returns a scalar or instant vector. Aborts on abort_above / abort_below threshold cross.

prometheus_url: "http://prom:9090"
query: 'histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[1m]))'
abort_above: 0.5
interval_sec: 5

Transient query errors are tolerated (a flaky scrape isn't a breach), but a sustained threshold violation aborts.

Kernel signal

Watches a host kernel signal's per-second rate and aborts when it exceeds abort_above - read in-kernel via eBPF where available, else from /proc. The first signal is TCP retransmits (tcp_retransmits): a fast, app-agnostic proxy for network pain that needs no health endpoint or Prometheus.

kernel_signal: "tcp_retransmits"
abort_above: 50      # retransmits/sec

Deadman

Always on. When a fault is applied, the agent forks a detached process that sleeps for duration + grace seconds, then removes the fault unconditionally. Survives agent crashes, kernel panics on the agent's own process, anything short of a full reboot - and a reboot wipes the qdisc anyway.

Server-side caps

The control plane rejects experiments that exceed these limits before they reach an agent:

Workspace kill switch

POST /v1/kill aborts every running experiment in the workspace. Use it when something's gone wrong and you don't have time to find the right experiment ID.

Orchestration

Beyond one-off experiments, the control plane composes faults into repeatable, fleet-scale workflows.

Templates

A saved fault type + spec, named and reusable. The building block for scenarios, scheduled runs, and fleet fan-outs.

Scenarios

An ordered sequence of steps (a GameDay). Each step runs to a terminal state before the next begins, with per-step steady-state gating and an optional continue-on-failure. Watch progress live on the Scenarios view.

Fleet targeting

Run one fault across every agent matching a label selector (--label env=prod), capped by a blast-radius percentage and an optional hard agent cap - a random sample, each its own experiment under one fleet run.

Schedules

Template-bound cron with cadence presets, pause/resume, and maintenance-window change-freezes that suppress scheduled chaos during set hours.

Dependency discovery, the resilience autopilot, and the fleet dependency graph (with per-dependency blast radius) build on these. Drive any of it from your own LLM via the MCP server.

Agent CLI

The chaos binary is both the CLI for one-off runs and the long-running daemon.

chaos agent

Run the agent connected to a control plane.

FlagDescription
--control-plane URLControl plane URL.
--token TOKENEnrollment token (one-time) or agent token (already enrolled).
--heartbeat DURHeartbeat interval. Default 5s.
--simulateRun the full loop without touching the kernel. Safe for any host.

chaos run latency

Run a one-off experiment locally - no control plane required.

FlagDescription
--dev IFACEInterface, default eth0.
--target HOSTDestination host[:port] or CIDR. Omit for whole-device.
--delay DURAdded latency, e.g. 200ms.
--jitter DURVariance, e.g. 30ms.
--duration DURHow long to hold the fault.
--grace DURDeadman grace window (default 10s).
--health-url URLHTTP guardrail; aborts on non-2xx.
--health-max-latency DURAborts if a health probe takes longer than this.
--prom URLPrometheus base URL.
--query QPromQL query.
--abort-above N / --abort-below NThreshold(s) for the query.
--interval DURProbe interval, default 5s.
--simulateWalk through the pipeline without touching the kernel.
--dry-runPrint the tc plan and exit without applying.
-y / --yesSkip confirmation prompt.

Exit codes: 0 success, 7 aborted-by-slo (a SLO violation auto-aborted the experiment - useful as a CI signal that the system isn't resilient to this fault).

The same chaos run <fault> shape works for every fault type - loss, partition, cpu, memory, dns, flowdrop, process, syscall, disk, l7, cloud - each with its own flags (run chaos run <fault> --help). The guardrail, dry-run, and deadman flags are shared. chaos ci-run wraps a control-plane experiment for pipelines, and chaos mcp starts the MCP server.

chaos abort / chaos status

Inspect and forcibly tear down local experiments started with chaos run:

chaos status          # show local state file
chaos abort           # remove any active qdisc and clear state

Autopilot

Autopilot turns an agent's discovered dependencies into a resilience audit automatically - no templates or scenarios to author. Point it at an agent and it builds a fault gauntlet (latency, then a full partition, scoped to each of the busiest dependencies), runs the whole thing as one scenario, and produces a resilience report.

Dependency discovery

Every connected agent passively maps its host's outbound dependencies from the kernel's TCP connection table - no interception, no proxy. Each dependency is a host:port it talks to, with an activity count. Autopilot targets the most active ones; the dashboard also surfaces them per agent (Agents → Dependencies) and as a fleet dependency graph, where a shared dependency's blast radius is the number of services that fail with it.

Running it

From the dashboard: open Agents, expand an agent's Dependencies, and click Run autopilot. You'll be prompted for an optional health URL (strongly recommended - see below). Or via the API:

POST/v1/autopilot
// generate + run a gauntlet over the agent's busiest dependencies
{ "agent_id": "agt-1", "health_url": "http://app:8080/healthz", "max_dependencies": 5 }
→ a scenario run (status running): one latency + one partition step per dependency
FieldTypeDescription
agent_idstringThe agent to audit. Required.
health_urlstringOptional steady-state check. With it, each step gets a real verdict; without it the gauntlet only exercises each dependency.
max_dependenciesintTop-N busiest dependencies to target (default 5, max 10).

The resilience report

When the run finishes (follow it on the Scenarios view, or watch it via the API/MCP), open Resilience report. It classifies every tested dependency - ranked most-actionable first, then by blast radius:

ClassificationMeaning
at-riskA fault breached steady state - the service can't tolerate losing this dependency.
toleratedHeld steady state under every fault, with a health gate watching.
exercisedFaults ran, but no health gate - so there's no pass/fail signal.
inconclusiveA step errored or was aborted before a verdict.
pendingA step hasn't finished yet.
GET/v1/scenario-runs/{id}/report
{ "headline": "1 of 5 dependencies at risk: db.internal (partition)",
  "summary": { "at_risk": 1, "tolerated": 4, "exercised": 0, "pending": 0 },
  "dependencies": [ { "target": "db.internal", "port": 5432, "blast_radius": 3,
                      "classification": "at-risk",
                      "faults": [ { "fault_type":"latency",   "verdict":"passed" },
                                  { "fault_type":"partition", "verdict":"failed" } ] } ] }
Set a health URL. Without one a fault has nothing to fail, so every dependency comes back exercised. With one, autopilot tells you which dependency actually takes your service down.

API reference

Every endpoint takes a Bearer token in Authorization. Three token types, scoped differently:

TypePrefixIssued byUse
Session tokenscs_signup/loginDashboard and admin API.
Enrollment tokensce_POST /v1/enrollment-tokensOne-time, agent uses it to register. Revocable.
Agent tokensca_returned from registerPer-agent credential for heartbeat / poll / events.
API tokensck_POST /v1/api-tokensProgrammatic workspace access for CI/CD and the MCP server. Hashed at rest, shown once, revocable.

Auth

POST/v1/auth/signup
{ "email": "you@co.com", "password": "...", "workspace": "acme-prod" }
→ { "session_token": "scs_...", "workspace_id": "ws-1", "email": "you@co.com" }
POST/v1/auth/login
{ "email": "you@co.com", "password": "..." }
→ { "session_token": "scs_...", "workspace_id": "ws-1", "email": "you@co.com" }

Enrollment tokens

GET/v1/enrollment-tokens
POST/v1/enrollment-tokens  { "label": "prod-east" }
POST/v1/enrollment-tokens/{token}/revoke

Agents

POST/v1/agents/register  (uses enrollment token)
{ "hostname": "...", "kernel": "...", "instance_id": "...", "device": "eth0", "version": "..." }
→ { "agent_id": "agt-1", "agent_token": "sca_..." }
GET/v1/agents
DELETE/v1/agents/{id}
POST/v1/agents/{id}/heartbeat  (agent token)
GET/v1/agents/{id}/next  long-poll, 25s

Experiments

POST/v1/experiments
{ "agent_id": "agt-1", "spec": { "dev":"eth0", "target":"10.0.3.21:5432",
  "delay_ms":200, "jitter_ms":30, "duration_sec":30, "grace_sec":10,
  "interval_sec":2, "health_url":"http://app:8080/healthz", "health_max_latency_ms":500 } }
→ { "id":"exp-1", "status":"queued", ... }
GET/v1/experiments
GET/v1/experiments/{id}
POST/v1/experiments/{id}/abort
POST/v1/experiments/{id}/events  (agent token)
POST/v1/kill  workspace-wide

Faults

Latency

Adds delay (with optional jitter) to outbound traffic. Implemented with tc queueing disciplines and the netem module.

Spec

FieldTypeDescription
devstringNetwork interface, e.g. eth0.
targetstringDestination host, host:port, or CIDR. Empty = whole-device blast radius.
delay_msintAdded latency in milliseconds. Capped at 60,000.
jitter_msintVariance around delay_ms (normal distribution). Capped at 10,000.
duration_secintHow long the fault is active. Capped at 3,600.
grace_secintExtra time before the deadman fires (default 10s).

Scoped (one dependency)

tc qdisc add dev eth0 root handle 1: prio
tc qdisc add dev eth0 parent 1:3 handle 30: netem delay 200ms 30ms distribution normal
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \
   match ip dst 10.0.3.21/32 match ip dport 5432 0xffff flowid 1:3

Only packets to that host:port are delayed; everything else flows unaffected.

Whole device (all egress)

tc qdisc add dev eth0 root handle 1: netem delay 200ms 30ms distribution normal

Loss

Drops a configurable fraction of outbound packets, optionally with correlation so drops cluster into bursts rather than uniformly random. Implemented with tc/netem.

Spec

FieldTypeDescription
devstringNetwork interface, e.g. eth0.
targetstringDestination host, host:port, or CIDR. Empty = whole-device blast radius.
loss_percentfloatPercentage of packets to drop, (0, 100]. Above ~50% effectively partitions the path.
loss_correlationfloat0–100. 0 = uniform random loss. Higher values make consecutive drops more likely (burstier).
duration_secintHow long the fault is active. Capped at 3,600.
grace_secintDeadman grace beyond duration (default 10s).

Scoped (one dependency)

tc qdisc add dev eth0 root handle 1: prio
tc qdisc add dev eth0 parent 1:3 handle 30: netem loss 5% 25%
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \
   match ip dst 10.0.3.21/32 match ip dport 5432 0xffff flowid 1:3

Whole device (all egress)

tc qdisc add dev eth0 root handle 1: netem loss 5%

From the CLI

chaos run loss --dev eth0 --target 10.0.3.21:5432 \
  --percent 5 --correlation 25 --duration 30s \
  --health-url http://localhost:8080/healthz
What this exposes. Latency tests timeout/retry/circuit-breaker behavior. Loss exposes idempotency: are retries safe? Do you have at-least-once semantics that survive duplicate delivery? Even small loss percentages (1–5%) on a high-RPS path will surface re-entrancy bugs that never show under latency.

Partition

Fully blocks egress traffic to a target (or the whole device) by dropping 100% of matching packets at the kernel queueing layer. Tests how a service behaves when a dependency becomes completely unreachable - the failover, retry, and circuit-breaker story under absence rather than degradation.

Spec

FieldTypeDescription
devstringNetwork interface, e.g. eth0.
targetstringDestination host, host:port, or CIDR. Empty = whole-device blast radius (the host is effectively offline from this NIC).
duration_secintHow long the partition holds. Capped at 3,600.
grace_secintDeadman grace beyond duration (default 10s).

Scoped (cut off one dependency)

tc qdisc add dev eth0 root handle 1: prio
tc qdisc add dev eth0 parent 1:3 handle 30: netem loss 100%
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 \
   match ip dst 10.0.3.21/32 match ip dport 5432 0xffff flowid 1:3

Traffic to that host:port disappears into the void; everything else flows unaffected.

Whole device (full network isolation)

tc qdisc add dev eth0 root handle 1: netem loss 100%

From the CLI

chaos run partition --dev eth0 --target 10.0.3.21:5432 \
  --duration 30s --health-url http://localhost:8080/healthz
What this exposes. Latency tests timeout settings. Loss tests retry safety. Partition tests failover - whether the service has a clear, exercised path to abandon a failed dependency. Services that "work" because they've never lost a backing service often fail spectacularly when one actually goes away. The default behavior of many HTTP clients under TCP-level black-holing is to hang for the full connect/read timeout, which is often minutes - a partition experiment surfaces that hang before production does.

CPU burn

Saturates CPU cores with tight spin loops, testing how a service behaves under compute starvation. Exposes autoscaling response time, GC pressure under contention, and whether request latency degrades gracefully or collapses.

Spec

FieldTypeDescription
coresintNumber of cores to saturate. 0 = all available cores (the default). Capped at 128.
duration_secintHow long to burn. Capped at 3,600.

Mechanism

The agent runs one busy-spin worker per core, each pinned to a single CPU. On teardown all workers exit immediately. No kernel objects persist - if the agent process dies, the burn stops instantly.

From the CLI

chaos run cpu --cores 4 --duration 60s \
  --health-url http://localhost:8080/healthz
What this exposes. CPU burn reveals whether a service has enough headroom to absorb a noisy-neighbor spike. Services running at 80%+ utilization in steady state often hit cascading failures under a CPU burn because the remaining 20% is consumed by GC, connection handling, and health checks. If the health probe trips, the service doesn't have enough margin.

Memory pressure

Allocates and pins a fixed amount of memory, testing OOM-killer behavior, swap thrash, GC pauses under heap contention, and whether memory-limited containers handle pressure gracefully.

Spec

FieldTypeDescription
megabytesintMB to allocate and pin. Capped at 32,768 (32 GB).
duration_secintHow long to hold the allocation. Capped at 3,600.

Mechanism

The agent allocates a single byte slice of the requested size and touches every page (4 KB stride) to defeat lazy allocation and overcommit. This forces the OS to commit physical memory. On teardown, the slice is nil'd and runtime.GC() is called as a release hint. No kernel objects persist.

From the CLI

chaos run memory --megabytes 512 --duration 60s \
  --health-url http://localhost:8080/healthz
What this exposes. Memory pressure reveals whether containers are properly limited (resources.limits.memory in K8s), whether the OOM killer targets the right process, and whether the service restarts cleanly or enters a crash loop. It also tests GC behavior - a Go service under heap pressure may see stop-the-world pauses that trip latency-based health checks even though the process isn't OOM'd.

DNS block

Drops outbound DNS queries for specific domain names, simulating a DNS outage for individual dependencies. Tests how a service behaves when it can't resolve a dependency's hostname - failover to cached records, circuit-breaker activation, graceful degradation vs hard crash.

Spec

FieldTypeDescription
domains[]stringDomain names to block. Up to 32 entries. E.g. ["api.stripe.com", "db.internal"].
devstringOptional: restrict to a specific interface (-o <dev>). Empty = all interfaces.
duration_secintHow long to block. Capped at 3,600.

Mechanism

The agent adds iptables rules that match outbound UDP port-53 packets containing the wire-encoded domain name (using -m string --algo bm --hex-string). Matching packets are dropped (-j DROP), causing the application's DNS resolver to time out. On teardown, the rules are removed with iptables -D.

Where the kernel supports it, an eBPF (TC-BPF) implementation does the same matching in-kernel at line rate; the fault behaves identically either way.

From the CLI

chaos run dns --domains api.stripe.com,db.internal \
  --duration 60s --health-url http://localhost:8080/healthz
What this exposes. DNS failures are one of the most common real-world outage triggers, yet they're rarely tested because tc/netem can't selectively target specific domains. A service that works fine when Postgres is slow might crash hard when it can't resolve the Postgres hostname. This fault surfaces stale DNS caches, missing TTL respect, and services that retry resolution in a tight loop (amplifying the failure).

Flow-drop (per-IP blackhole)

Blackholes all traffic to a single IPv4 destination in the kernel - a surgical partition of one peer, independent of port, implemented on the eBPF fast path (TCX/tc-BPF) where available. Use it to cut off exactly one backend instance while its siblings keep serving.

FieldTypeDescription
targetstringIPv4 destination to blackhole. Required.
devstringInterface (optional - defaults to the host's primary route).
duration_sec / grace_secintHold time and deadman grace.

Process freeze / kill

Acts on a single process by PID: freeze pauses it with SIGSTOP and resumes with SIGCONT on teardown (fully reversible); kill sends one terminating signal. Tests supervisor/restart behavior, stuck-process detection, and how peers handle an unresponsive (but not gone) instance.

FieldTypeDescription
target_pidintThe process to act on. Required; must be > 1 (pid 1 is refused).
actionstringfreeze (reversible pause) or kill.
signalstringFor kill: SIGTERM (default) or SIGKILL.
duration_sec / grace_secintFor freeze, how long to hold the pause.

Syscall errno injection

Makes a chosen syscall fail for one target process, returning a real errno - via eBPF bpf_override_return on a kernel built with CONFIG_BPF_KPROBE_OVERRIDE. Reproduces "the disk returned EIO", "connect got ECONNREFUSED", or "out of file descriptors (EMFILE)" deterministically, scoped to one process so the rest of the host is untouched.

FieldTypeDescription
syscallstringe.g. openat, connect, read.
errnostringSymbolic, from a curated allow-list (EIO, ECONNREFUSED, ENOSPC, EMFILE, …).
target_pidintOnly this process (tgid). Required.
modestringevery (optionally sampled by probability_pct), once, or nth (the n-th matching call).
duration_sec / grace_secintHold time and deadman grace.

Disk fill (ENOSPC)

Consumes free space on a target filesystem with a single balloon file until it reaches a target fill level, then deletes it on teardown (reversible). Exercises the "disk full" path - failing log writes, stalled DB checkpoints, wedged WAL growth - that little else reproduces safely. A true 100% fill is refused so the host stays recoverable.

FieldTypeDescription
pathstringA directory on the target filesystem. Required.
fill_pctfloatRaise filesystem usage to this percent. (0, 99].
duration_sec / grace_secintHold time and deadman grace.

Layer-7 HTTP

Transparent, per-dependency HTTP faults - fail 30% of POSTs to the payments API with a 503 while every other call flows untouched - with no service mesh and no app changes. The agent installs an iptables REDIRECT for the target's IPs into a local proxy, recovers the original destination, and content-sniffs each connection. Cleartext HTTP gets the full action set; TLS is matched by SNI without decryption (limited to delay/reset) unless you opt into termination with an agent CA (chaos l7-ca).

FieldTypeDescription
targetstringDependency hostname or IP to intercept. Required.
portintDependency TCP port (default 80).
actionstringstatus (inject an HTTP code), delay, or reset.
status / delay_msintThe code to inject (default 503) / added latency for delay.
methods, path_prefix, host_match-Optional AND-ed selectors (empty = all requests to target).
probability_pctfloatFault this fraction of matching requests.
tls_terminateboolDecrypt HTTPS at the proxy (needs the agent CA installed) so status/selectors work on TLS.

Cloud-layer

Reaches the host's cloud control plane to fault infrastructure, not packets: stop the instance, detach a volume, or blackhole a security group - using the instance's ambient identity (IMDS / metadata), no static keys. Only reversible actions are modeled; destructive ones (e.g. terminate) are intentionally excluded.

FieldTypeDescription
providerstringaws, gcp, or azure.
actionstringinstance_stop, volume_detach, sg_blackhole, or a managed trigger (fis_experiment / chaos_experiment).
resourcestringTarget resource id (instance, volume, security group…).
paramsmapAction-specific parameters.
duration_sec / grace_secintHow long the action holds before it's reversed.

Operations

Persistence

The control plane keeps its data in memory by default - great for dev and chaos run, with nothing to set up - and uses a Postgres backend for production, which is what the hosted service runs on. On the managed service your data is durable; for local use no database is required.

TLS and hosting

The managed control plane runs at app.straightchaos.com with TLS, and the dashboard ships with it - sign up and connect agents with an enrollment token, nothing to host. Self-hosting the control plane is also supported: it speaks plain HTTP, so front it with a TLS-terminating reverse proxy (Caddy, nginx, an ALB) and point your agents at the public URL.

Permissions

The agent calls tc, which requires CAP_NET_ADMIN. On a bare host, run as root or grant the capability:

setcap cap_net_admin+ep /usr/local/bin/chaos

In a container, use --cap-add=NET_ADMIN (or --privileged). In Kubernetes, set the capability in the pod's securityContext.

Companion surfaces

The CLI and chaos guard are the primary way to run all of this; three companion surfaces cover the cases where a terminal isn't the natural fit.

macOS menu-bar app. A native menu-bar app that turns setup into one click: "Set up protection" enrolls the Mac via a chaos://enroll deep link and installs chaos guard as a persistent LaunchAgent, so protection survives a reboot without a terminal open. It embeds the dashboard for status at a glance. It's a companion to the CLI, not a replacement - under the hood it's still running the same guard.

Browser extension (Chromium). Not every AI usage goes through a CLI agent - a lot of it is ChatGPT, Claude.ai, Gemini, or Copilot open in a browser tab. The extension captures that usage into the same per-employee oversight rollup, with DLP masking done client-side: the raw text never leaves the browser.

chaos console TUI. A terminal UI (built on Bubble Tea) for the activity stream, approval queue, and policy views, for when you'd rather stay in a terminal than open the dashboard.

Troubleshooting

Agent registers but no experiments dispatch

The agent long-polls /v1/agents/{id}/next for 25s at a time. Make sure your reverse proxy / load balancer doesn't terminate long-lived requests sooner than that.

RTNETLINK answers: Operation not supported

The sch_netem kernel module isn't available. Verify with tc qdisc add dev lo root netem delay 10ms. On a slim container kernel, install linux-modules-extra-$(uname -r) or use a different base image.

Experiment ends instantly with aborted-by-slo

The guardrail tripped on the first sample. For HTTP health probes, check that the URL is reachable from the agent and that the endpoint actually returns 2xx when the system is healthy. For Prometheus, check that the query already returns a value below your threshold before injecting.

Agent disappears as "offline" while still running

The agent token may have been revoked (workspace policy). The agent's register call will succeed on the next retry only if you give it a fresh enrollment token.

FAQ

Does this work on macOS?

The agent's real fault injection is Linux-only - macOS doesn't have tc/netem. The control plane and dashboard run natively on macOS; the agent compiles and runs on macOS only in --simulate mode. To validate real injection from a Mac, run the agent inside a Linux container (Docker Desktop / OrbStack / Lima) with --privileged or --cap-add=NET_ADMIN.

How is this different from Chaos Mesh / Litmus / Gremlin?

Straight Chaos is built for service teams, not platform teams. It's a single binary - no Helm chart, no operator, no Kubernetes prerequisite - with an HTTP health probe as the default guardrail, so you don't need Prometheus to get started. It injects real kernel-level faults (network, syscall, DNS, CPU, disk) with the safety layer built in: blast-radius limits, automatic rollback via the deadman, live scoring, and an audit trail. And chaos run works standalone as a CI resilience gate, no control plane required.

What happens if the agent crashes during an experiment?

The deadman process is detached (setsid + own process group) and survives. It sleeps for duration + grace, then runs tc qdisc del regardless of agent state. The fault is removed even if the agent is gone.

Is my data isolated between workspaces?

Yes - every API call is workspace-scoped at the auth layer. An agent token grants access to one workspace. A session token is bound to one workspace. Cross-workspace reads / writes return 404, and this isolation is covered by our test suite.

Can I use this without the control plane?

Yes - chaos run <fault> is fully usable on its own for any fault type. It runs the same plan, the same guardrails, the same deadman, locally. The exit code (0 / 7) makes it a useful resilience gate in CI.