{
  "markdown": "<div align=\"center\">\n\n<img src=\"docs/assets/logo.svg\" alt=\"IronClaw\" width=\"380\">\n\n### Self-hosted AI agents you do not have to trust.\n\nEach one runs sealed in a sandbox that provably cannot phone home, read your host, or rewrite its own rules.\n\n<!-- Security & supply-chain trust cluster — lead with what makes this project different -->\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/IronSecCo/ironclaw/badge)](https://scorecard.dev/viewer/?uri=github.com/IronSecCo/ironclaw)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13348/badge)](https://www.bestpractices.dev/projects/13348)\n[![CodeQL](https://github.com/IronSecCo/ironclaw/actions/workflows/codeql.yml/badge.svg)](https://github.com/IronSecCo/ironclaw/actions/workflows/codeql.yml)\n[![Signed releases (cosign)](https://img.shields.io/badge/releases-cosign%20signed-0a7bbb.svg)](#verifying-a-release)\n[![SBOM: SPDX + CycloneDX](https://img.shields.io/badge/SBOM-SPDX%20%2B%20CycloneDX-44883e.svg)](#verifying-a-release)\n[![SLSA provenance](https://img.shields.io/badge/SLSA-build%20provenance-44883e.svg)](#verifying-a-release)\n[![GitHub Marketplace: IronClaw sandbox scan](https://img.shields.io/badge/GitHub%20Marketplace-IronClaw%20sandbox%20scan-0a7bbb.svg?logo=github&logoColor=white)](https://github.com/marketplace/actions/ironclaw-sandbox-scan)\n[![Sandbox scan: PR scorecard](https://img.shields.io/badge/sandbox%20scan-PR%20scorecard-0a7bbb.svg)](https://ironsecco.github.io/ironclaw/scan-action/)\n[![Sandbox Isolation Score](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/IronSecCo/ironclaw/main/.ironclaw/sandbox-isolation.json)](https://ironsecco.github.io/ironclaw/scan/#sandbox-isolation-score-badge)\n\n[![Documentation](https://img.shields.io/badge/docs-ironsecco.github.io-0a7bbb.svg)](https://ironsecco.github.io/ironclaw/)\n[![Status: alpha](https://img.shields.io/badge/status-alpha-orange.svg)](#project-status)\n[![Latest release](https://img.shields.io/github/v/release/IronSecCo/ironclaw?sort=semver)](https://github.com/IronSecCo/ironclaw/releases/latest)\n[![Go Reference](https://pkg.go.dev/badge/github.com/IronSecCo/ironclaw.svg)](https://pkg.go.dev/github.com/IronSecCo/ironclaw)\n[![License: AGPLv3 + Commercial](https://img.shields.io/badge/License-AGPLv3%20%2B%20Commercial-blue.svg)](LICENSING.md)\n[![GitHub Discussions](https://img.shields.io/github/discussions/IronSecCo/ironclaw?logo=github&label=discussions)](https://github.com/IronSecCo/ironclaw/discussions)\n[![Good first issues](https://img.shields.io/github/issues/IronSecCo/ironclaw/good%20first%20issue?label=good%20first%20issues)](https://github.com/IronSecCo/ironclaw/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)\n[![GitHub stars](https://img.shields.io/github/stars/IronSecCo/ironclaw?style=social)](https://github.com/IronSecCo/ironclaw/stargazers)\n\n</div>\n\n**IronClaw runs autonomous AI agents on infrastructure you control**, reached through the chat apps\nyou already use. Each agent can read, write, schedule, and reply like any assistant, but it lives\ninside a sealed sandbox with `network=none`: it reaches the model only through a host proxy, and it\n**cannot change its own configuration.** It is for anyone who wants what agents can do without\nhanding an autonomous program the keys to their machine.\n\n> **Now on the GitHub Marketplace.** The `ironctl scan` containment grader ships as a\n> [GitHub Action](https://github.com/marketplace/actions/ironclaw-sandbox-scan): drop one\n> line into a workflow and every pull request gets a 0 to 100 sandbox isolation scorecard as\n> a sticky comment. Local, read-only, credential-free.\n>\n> ```yaml\n> # .github/workflows/scan.yml\n> - uses: IronSecCo/ironclaw@v1\n>   with:\n>     target: my-container   # a container, compose service, or k8s manifest\n> ```\n>\n> Report-only by default; set `min-score: 90` to gate merges. See [scan in CI](https://ironsecco.github.io/ironclaw/scan-action/).\n\n<div align=\"center\">\n\n<!--\n  Motion sells: a REAL recording of examples/live-containment (docs/assets/live-containment.cast,\n  regenerable via the \"Re-record the clip\" note in that example's README). The static final-frame\n  SVG is the reduced-motion / no-animation fallback. No autoplay-with-sound (GIF has no audio).\n-->\n<picture>\n  <source media=\"(prefers-reduced-motion: reduce)\" srcset=\"docs/assets/containment.svg\">\n  <img src=\"docs/assets/live-containment.gif\" width=\"820\" alt=\"live-containment demo: one command engages a real per-session sandbox, then a fully-jailbroken agent tries three escapes from inside the box and each is BLOCKED. Exfiltrating to the attacker is denied because network=none leaves only the loopback interface and DNS fails; reading the operator's host filesystem is denied because the host root is outside the sandbox mount namespace; seizing the host via the Docker Engine socket is denied because the socket is never mounted in and there is no docker client. It ends with a containment summary that 3 of 3 escape attempts were denied and the box held.\">\n</picture>\n\n<sub><b>Watch it catch a real escape.</b> A fully-jailbroken agent inside a real sandbox tries to phone home, read the host filesystem, and seize the host through the Docker socket. Each attempt is <b>denied</b> at the isolation boundary, then a containment summary prints. One command, zero credentials, reduced-motion friendly. <a href=\"examples/live-containment/\"><code>examples/live-containment/run.sh</code></a></sub>\n\n</div>\n\n**⭐ Like the idea of agents you do not have to trust? [Star the repo](https://github.com/IronSecCo/ironclaw)** so\nit is one click to follow along and easier for the next person to find. Then run the exact demo above\nin 30 seconds, no signup and no API key.\n\n### Try it in 30 seconds (zero credentials)\n\nMake sure the **Docker daemon is running** (start Docker Desktop, or `sudo systemctl start docker`\non Linux), then paste one block:\n\n```sh\ngit clone https://github.com/IronSecCo/ironclaw.git && cd ironclaw\nexamples/live-containment/run.sh   # builds the sandbox once, engages a real sandbox, proves it holds\n```\n\nThat single command runs the whole secured path on your laptop: it starts the offline mock-agent\ncontrol-plane (**no API key**), engages a **real per-session sandbox**, lets a jailbroken agent try\nto break out, and prints the containment summary you saw above. Want to chat with an agent in a\nbrowser first? Run [`hello-ironclaw`](examples/hello-ironclaw/) or the\n[zero-credential quickstart](docs/quickstart.md). Production seals each sandbox with gVisor and\n`network=none`.\n\n> [!WARNING]\n> **Alpha software, work in progress. Please read before relying on it.**\n>\n> - **It's an alpha.** Flags, the on-disk format, and the HTTP/contract surfaces can still change without notice or a migration path. Don't point it at anything you can't afford to lose.\n> - **Not every feature is tested end-to-end.** The control-plane, gateway, and encrypted-queue core have real coverage (800+ Go tests plus a black-box parity suite); channel adapters, some tools, multi-provider routing, and a live sandbox launch are exercised more lightly. Treat anything outside the [tested core](#project-status) as experimental.\n>\n> macOS gets a weaker sandbox boundary than Linux+gVisor, and **native Windows can't run the agent sandbox at all** (use WSL2). See [Platform support](#platform-support).\n\n> **The security model, in one line:** each sandboxed agent runs with `network=none`, reaches the\n> model only through a host proxy, and **cannot change its own configuration.** Every capability\n> change is held at a gateway for a human decision. The full design is in the\n> [architecture overview](docs/architecture.md) and the [threat model](docs/threat-model.md).\n\n### See the whole journey, end to end\n\n<div align=\"center\">\n\n<img src=\"docs/assets/demo.svg\" width=\"800\" alt=\"Zero-credential chat demo terminal session: one command (docker compose -f docker-compose.demo.yml up -d) starts the offline mock-agent control-plane with no API key; a chat message engages the agent, which launches a real per-session sandbox container (ic-sbx-…); the reply flows back through the encrypted per-session queue.\">\n\n<sub><b>Zero credentials, one command.</b> The offline <code>mock-agent</code> runs the full chat to per-session sandbox to reply path with no API key. Production seals each sandbox with gVisor and <code>network=none</code>. <a href=\"docs/quickstart.md\">Quickstart</a></sub>\n\n<br><br>\n\n<img src=\"docs/assets/walkthrough.svg\" width=\"820\" height=\"595\" alt=\"End-to-end IronClaw walkthrough terminal session in three acts. Act 1: one command starts the offline mock-agent and it replies with no API key. Act 2: connect a real provider by exporting a host-side, redacted ANTHROPIC_API_KEY and starting the real control-plane (each session sealed with gVisor and network=none). Act 3: the agent submits a persona change that is HELD at the human-approval gateway, a human approves it, and the submit-approve-apply trail lands on the append-only audit log.\">\n\n<sub><b>Zero-cred demo, connect a real provider, first approved task.</b> The one credential step keeps the key host-side; every agent change is held at the gateway for a human, then written to the append-only audit log. Animation freezes on the final frame under <code>prefers-reduced-motion</code>. <a href=\"docs/quickstart.md\">Quickstart</a></sub>\n\n</div>\n\n## Get running in under two minutes\n\nOne command installs the two host binaries (`ironctl` + `ironclaw-controlplane`); in dev mode the\ncontrol-plane serves its API at **`http://127.0.0.1:8787`**. From a cold machine, you'll have a\ncapability change waiting at the security gateway in **under two minutes**:\n\n```sh\n# 1. Install — detects your OS/arch and verifies the SHA-256 checksum before installing\ncurl -fsSL https://raw.githubusercontent.com/IronSecCo/ironclaw/main/scripts/install.sh | sh\n\n# 2. Start the control-plane in dev mode — API base URL: http://127.0.0.1:8787\nexport IRONCLAW_API_TOKEN=$(openssl rand -hex 32)\nironclaw-controlplane --dev --api-addr 127.0.0.1:8787 &\n\n# 3. Your first command — submit a change; it is HELD at the gateway for a human decision\nironctl change submit --kind persona --group dev-agent --by you\nironctl change pending                       # see it waiting\nironctl change approve <change-id> --by you   # apply it\n```\n\nOn Windows, `irm https://raw.githubusercontent.com/IronSecCo/ironclaw/main/scripts/install.ps1 | iex`\ninstalls the host binaries (`ironclaw-controlplane.exe` + `ironctl.exe`) and `--dev` runs, but the\n**agent sandbox needs WSL2 or Linux** — see [Windows via WSL2](#windows-via-wsl2).\nVersion pinning, system-wide installs, and building from source are all in [Installation](#installation).\n\n### One-click cloud deploy\n\nRun the **hardened control-plane** on a PaaS in ~2 minutes with zero local tooling — the\napproval gateway, encrypted per-session queues, host-side credential custody, and the web\nconsole:\n\n[![Deploy to Fly.io](https://img.shields.io/badge/Deploy-Fly.io-8B5CF6?logo=flydotio&logoColor=white)](deploy/fly/)\n[![Deploy to Render](https://img.shields.io/badge/Deploy-Render-46E3B7?logo=render&logoColor=000)](https://render.com/deploy?repo=https://github.com/IronSecCo/ironclaw&path=deploy/render/render.yaml)\n[![Deploy on Railway](https://img.shields.io/badge/Deploy-Railway-0B0D0E?logo=railway&logoColor=white)](deploy/railway/)\n\n> These PaaS paths run the **control-plane only** — a single container has no gVisor and\n> no Docker socket, so **agent sandboxes don't launch there** (same boundary as the\n> [hardened Compose path](deploy/docker-compose.prod.yml)). For full agent isolation use\n> a gVisor host or k8s node. Details + env in the\n> [deployment guide](docs/deployment.md) (**Path D**).\n\n## CLI-first and API-first\n\nThis is a feature, not a missing dashboard. Every capability is a documented HTTP endpoint **and** an\n`ironctl` subcommand, so IronClaw is scriptable, auditable, and CI-friendly from the first command —\nwith **no public web surface to phish, misconfigure, or leave exposed.** (There is now a private,\nmesh-only web console at `/ui/` — but it's **additive, never the only way in**, and rides the same\nTailscale-bound API, so it adds no public port.)\n\n---\n\n<details>\n<summary><b>Table of contents</b></summary>\n\n- [Get running in under two minutes](#get-running-in-under-two-minutes)\n- [CLI-first and API-first](#cli-first-and-api-first)\n- [Why it's different](#why-its-different)\n- [How it works](#how-it-works)\n- [Platform support](#platform-support)\n- [Project status](#project-status)\n- [Prerequisites](#prerequisites)\n- [Installation](#installation)\n- [Quickstart](#quickstart)\n- [Audit your own sandbox](#audit-your-own-sandbox-in-10-seconds)\n- [Examples](#examples)\n- [Usage](#usage)\n- [Model providers](#model-providers)\n- [Configuration](#configuration)\n- [Development](#development)\n- [Repository layout](#repository-layout)\n- [Security](#security)\n- [Roadmap](#roadmap)\n- [Community](#community)\n- [Contributing](#contributing)\n- [License](#license)\n\n</details>\n\n## Why it's different\n\n| Pillar | What it is | Attack surface it removes |\n|--------|------------|----------------------------|\n| **Sealed runtime** | The agent ships as a compiled Go binary | Agent self-modification — there's no source inside the box to rewrite |\n| **Approved by humans** | Every change to the harness clears a deterministic gateway | Silent setting changes — nothing changes without a human seeing and approving it |\n| **Encrypted queues** | Per-session encrypted message queues; read-only inbound | Data theft at rest, and cross-session reads |\n| **Sealed sandbox** | gVisor container, no network, host-proxied model calls | Data exfiltration and sandbox escape |\n| **Private control panel** | Admin access over a private mesh (Tailscale) only | Remote attacks on the controls |\n\nThe throughline: **treat the agent as untrusted, and make the security boundary something you can\nverify — not something you take on faith.**\n\n> ⚖️ **Weighing your options?** See [**Why IronClaw / vs. the alternatives**](https://ironsecco.github.io/ironclaw/comparison/)\n> for an honest comparison against hosted agent platforms, raw container + LLM glue, and other\n> self-hosted agent runtimes.\n\n## Audit your own sandbox in 10 seconds\n\nDo not take our word for any of that. `ironctl scan` grades the containment posture of\n**any** running container, docker-compose service, or Kubernetes pod on a 0 to 100 scale.\nIt works on your own setups, not just IronClaw's, so you can measure how much isolation you\nactually have before you hand a sandbox to untrusted code. It is fail-closed: any boundary\nit cannot observe is scored insecure, never waved through.\n\n```bash\nironctl scan my-container\n```\n\nIt also grades a **Dockerfile** statically, at authoring or CI time, with no daemon and no\nimage pull, so you catch a leaked credential, an unpinned base, or a root default in review\ninstead of in production:\n\n```bash\nironctl scan --dockerfile Dockerfile --min-score 80\n```\n\nGrade Dockerfiles automatically on every commit with the\n[pre-commit](https://pre-commit.com) hook, which builds `ironctl` from source, so\nthere is nothing to install first:\n\n```yaml\n# .pre-commit-config.yaml\nrepos:\n  - repo: https://github.com/IronSecCo/ironclaw\n    rev: v0.1.x\n    hooks:\n      - id: ironclaw-scan-dockerfile\n        args: [--min-score=80]   # fail the commit below grade B\n```\n\nWalkthrough:\n[How to scan a Dockerfile for security issues](https://ironsecco.github.io/ironclaw/blog/scan-a-dockerfile-for-security-issues/)\ntakes a deliberately bad Dockerfile from **5/100 (F)** to **100/100 (A)**, one fix at a time.\n\nA container started the usual way (root user, default caps, bridge network, `docker.sock`\nmounted in) grades **23/100, F**. An IronClaw `ic-sbx-*` session sandbox grades a clean\n**100/100, A**:\n\n| Target | Score | Grade | Posture |\n|---|---|---|---|\n| Typical `docker run` container | 23/100 | F | runs as root, `docker.sock` mounted, writable rootfs, bridge egress |\n| IronClaw session sandbox | 100/100 | A | non-root, all caps dropped, seccomp on, `network=none`, read-only rootfs, gVisor |\n\nEvery failing line names the specific hole and why it matters. Drop the grade into your own\nREADME with `ironctl scan --badge scan.svg`, or gate CI with `ironctl scan --min-score 90`.\n\nSee the [scan reference](https://ironsecco.github.io/ironclaw/scan/) for all seven dimensions\nand every flag. Or browse the\n[Container Isolation Scores directory](https://ironsecco.github.io/ironclaw/scores/): the\ndefault-config grade for 150+ of the most-pulled public images, so you can see how the\ncontainers you already run stack up. Rankings live on the\n[Container Isolation Leaderboard](https://ironsecco.github.io/ironclaw/scores/leaderboard/)\n(Hall of Fame vs worst offenders), and the interactive\n[scores explorer](https://nivardsec.com/scores) lets you filter and grab a badge for your repo.\n\nHead-to-head reads backed by the same scan data:\n[Alpine vs Debian vs Ubuntu](https://ironsecco.github.io/ironclaw/blog/alpine-vs-debian-vs-ubuntu-container-isolation/)\n(does the base image change isolation?),\n[Docker default vs hardened](https://ironsecco.github.io/ironclaw/blog/docker-default-vs-hardened-container-isolation/)\n(the 48-point gap, flag by flag), and\n[gVisor vs runc](https://ironsecco.github.io/ironclaw/blog/gvisor-vs-runc-container-isolation-compared/)\n(when a shared host kernel is the weak link).\n\nPer-image hardening walkthroughs, the default grade, the dimensions that fail, and the exact\n`ironctl scan --fix` flags that close the gap. Start at the\n[**hardening guides hub**](https://ironsecco.github.io/ironclaw/blog/hardening-guides/), or jump to one:\n[Postgres](https://ironsecco.github.io/ironclaw/blog/harden-postgres-container-isolation/),\n[MySQL](https://ironsecco.github.io/ironclaw/blog/harden-mysql-container-isolation/),\n[MariaDB](https://ironsecco.github.io/ironclaw/blog/harden-mariadb-container-isolation/),\n[MongoDB](https://ironsecco.github.io/ironclaw/blog/harden-mongodb-container-isolation/),\n[Cassandra](https://ironsecco.github.io/ironclaw/blog/harden-cassandra-container-isolation/),\n[ClickHouse](https://ironsecco.github.io/ironclaw/blog/harden-clickhouse-container-isolation/),\n[Redis](https://ironsecco.github.io/ironclaw/blog/harden-redis-container-isolation/),\n[Memcached](https://ironsecco.github.io/ironclaw/blog/harden-memcached-container-isolation/),\n[Elasticsearch](https://ironsecco.github.io/ironclaw/blog/harden-elasticsearch-container-isolation/),\n[Kafka](https://ironsecco.github.io/ironclaw/blog/harden-kafka-container-isolation/),\n[RabbitMQ](https://ironsecco.github.io/ironclaw/blog/harden-rabbitmq-container-isolation/),\n[Vault](https://ironsecco.github.io/ironclaw/blog/harden-vault-container-isolation/),\n[Consul](https://ironsecco.github.io/ironclaw/blog/harden-consul-container-isolation/),\n[MinIO](https://ironsecco.github.io/ironclaw/blog/harden-minio-container-isolation/),\n[nginx](https://ironsecco.github.io/ironclaw/blog/harden-nginx-container-isolation/),\n[Grafana](https://ironsecco.github.io/ironclaw/blog/harden-grafana-container-isolation/),\n[Prometheus](https://ironsecco.github.io/ironclaw/blog/harden-prometheus-container-isolation/),\n[Traefik](https://ironsecco.github.io/ironclaw/blog/harden-traefik-container-isolation/),\n[InfluxDB](https://ironsecco.github.io/ironclaw/blog/harden-influxdb-container-isolation/),\n[CockroachDB](https://ironsecco.github.io/ironclaw/blog/harden-cockroachdb-container-isolation/),\n[TimescaleDB](https://ironsecco.github.io/ironclaw/blog/harden-timescaledb-container-isolation/),\n[Valkey](https://ironsecco.github.io/ironclaw/blog/harden-valkey-container-isolation/),\n[HAProxy](https://ironsecco.github.io/ironclaw/blog/harden-haproxy-container-isolation/),\n[ZooKeeper](https://ironsecco.github.io/ironclaw/blog/harden-zookeeper-container-isolation/) and\n[untrusted Node.js](https://ironsecco.github.io/ironclaw/blog/run-untrusted-nodejs-code-safely/).\n\n### Grade every pull request\n\nThe same grader is published on the GitHub Marketplace as\n[**IronClaw sandbox scan**](https://github.com/marketplace/actions/ironclaw-sandbox-scan). One\nline in a workflow and every pull request gets a containment scorecard as a sticky comment that\nupdates in place:\n\n```yaml\n# .github/workflows/scan.yml\n- uses: IronSecCo/ironclaw@v1\n  with:\n    target: docker-compose.yml\n    mode: compose\n    min-score: 90        # omit / 0 = report-only, never blocks the check\n```\n\nIt runs on a stock `ubuntu-latest` runner with no credentials and no control-plane. `mode: k8s`\nadds `policy-check: true` to fail the check on any rule `--emit-policy` would generate, and\n`upload-sarif: true` sends failed dimensions to the Security tab. Full inputs and outputs:\n[scan in CI](https://ironsecco.github.io/ironclaw/scan-action/).\n\n### Show your score\n\nPut your containment grade in your README, the same way a coverage or build badge does.\nGenerate a shields.io endpoint file, commit it (no server, so a badge hit never triggers a\nremote scan), and embed it:\n\n```bash\nironctl scan my-container --badge-json .ironclaw/sandbox-isolation.json\ngit add .ironclaw/sandbox-isolation.json && git commit -m \"chore: add sandbox isolation badge\"\n```\n\n```markdown\n[![Sandbox Isolation Score](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/main/.ironclaw/sandbox-isolation.json)](https://ironsecco.github.io/ironclaw/scan/)\n```\n\nThe badge at the top of this README is IronClaw's own, graded 100/100 A from\n[`.ironclaw/sandbox-posture.yml`](.ironclaw/sandbox-posture.yml). Full walkthrough:\n[Add a live Sandbox Isolation Score badge to your repo](https://ironsecco.github.io/ironclaw/blog/add-a-sandbox-isolation-score-badge-to-your-repo/).\n\n## How it works\n\nTwo compiled Go programs that never share memory and talk only through a pair of encrypted SQLite\nfiles per conversation:\n\n```mermaid\nflowchart TB\n  CHAT[\"Chat platforms<br/>12 channel adapters\"]\n  CLI[\"ironctl CLI\"]\n  WEB[\"Web console\"]\n\n  subgraph host[\"Trusted host — control-plane (cmd/controlplane)\"]\n    API[\"HTTP API<br/>Tailscale mesh-only + bearer\"]\n    GW[\"Gateway<br/>deterministic verifiers · human approval\"]\n    CORE[\"Router · delivery · sweep · key custodian\"]\n    CHAD[\"Channel adapters\"]\n    MP[\"Model proxy<br/>holds provider keys\"]\n    ISO[\"Isolation launcher<br/>gVisor / runsc\"]\n  end\n\n  subgraph queues[\"Encrypted SQLCipher queues · per session\"]\n    INQ[(\"inbound.db<br/>read-only to agent\")]\n    OUTQ[(\"outbound.db<br/>append-only by agent\")]\n  end\n\n  subgraph box[\"Agent sandbox · gVisor · network=none\"]\n    LOOP[\"Agent loop · tools · model provider\"]\n  end\n\n  PROV[\"Model providers<br/>Anthropic · OpenAI · OpenRouter\"]\n\n  CHAT <--> CHAD\n  CLI -->|mesh only| API\n  WEB -->|mesh only| API\n  CHAD --> CORE\n  API --> GW --> CORE\n  CORE -->|write| INQ\n  OUTQ -->|read| CORE\n  ISO -->|launch| LOOP\n  INQ -->|ro bind mount| LOOP\n  LOOP -->|append| OUTQ\n  LOOP -->|unix socket| MP -->|HTTPS · key injected host-side| PROV\n\n  classDef host fill:#eaf2ff,stroke:#1d4ed8,stroke-width:1px,color:#0b1124;\n  classDef store fill:#b9d4ff,stroke:#1d4ed8,stroke-width:1px,color:#0b1124;\n  classDef box fill:#1d4ed8,stroke:#63a0ff,stroke-width:2px,color:#ffffff;\n  classDef control fill:#16224a,stroke:#63a0ff,stroke-width:2px,color:#ffffff;\n  classDef ext fill:#f4f9ff,stroke:#8fb4ff,stroke-width:1px,color:#16224a;\n\n  class API,CORE,CHAD,MP,ISO host;\n  class GW control;\n  class INQ,OUTQ store;\n  class LOOP box;\n  class CHAT,CLI,WEB,PROV ext;\n```\n\n- The **control-plane** receives chats, routes them, holds the keys, runs the approval gateway, and\n  performs every privileged action on the agent's behalf — after its own checks.\n- The **sandbox** — one per conversation, wrapped in gVisor with no network of its own — reads its\n  encrypted inbox (read-only), calls the AI model through the host proxy, and writes its encrypted\n  outbox. It can *request* a capability change but can never apply one.\n- The **frozen contract** (`internal/contract`) is the only package both sides import: typed IDs,\n  row shapes, the embedded SQL schema, pinned cipher params, and the gateway protocol.\n\nA single message rides a clean loop; anything that would change what the agent *can do* takes the\nseparate dashed path through the human-approval **gateway**:\n\n```mermaid\nflowchart LR\n  SENDER[\"External sender<br/>Slack · email · …\"]\n  ADAPTER[\"Channel adapter\"]\n  ROUTER[\"Router<br/>authorize + fan-out\"]\n  INQ[(\"inbound.db\")]\n  LOOP[\"Agent loop\"]\n  MODEL[\"Model provider\"]\n  OUTQ[(\"outbound.db\")]\n  DELIVERY[\"Delivery\"]\n  GW{\"Gateway<br/>human approval\"}\n  APPLY[\"Control-plane<br/>applies change\"]\n\n  SENDER -->|message| ADAPTER --> ROUTER\n  ROUTER -->|write · encrypted| INQ\n  INQ -->|ro| LOOP\n  LOOP <-->|model call via host proxy| MODEL\n  LOOP -->|reply · append| OUTQ\n  OUTQ --> DELIVERY --> ADAPTER\n  ADAPTER -->|reply| SENDER\n  LOOP -.->|capability-change request| GW\n  GW -.->|approved| APPLY\n\n  classDef host fill:#eaf2ff,stroke:#1d4ed8,stroke-width:1px,color:#0b1124;\n  classDef store fill:#b9d4ff,stroke:#1d4ed8,stroke-width:1px,color:#0b1124;\n  classDef box fill:#1d4ed8,stroke:#63a0ff,stroke-width:2px,color:#ffffff;\n  classDef control fill:#16224a,stroke:#63a0ff,stroke-width:2px,color:#ffffff;\n  classDef ext fill:#f4f9ff,stroke:#8fb4ff,stroke-width:1px,color:#16224a;\n\n  class ADAPTER,ROUTER,DELIVERY,APPLY host;\n  class INQ,OUTQ store;\n  class LOOP box;\n  class GW control;\n  class SENDER,MODEL ext;\n```\n\nFor the full design, see [`docs/architecture.md`](docs/architecture.md),\n[`docs/threat-model.md`](docs/threat-model.md), and the plain-language tour in\n[`docs/ironclaw-explained.md`](docs/ironclaw-explained.md).\n\n> 📚 **Full documentation site:** [**ironsecco.github.io/ironclaw**](https://ironsecco.github.io/ironclaw/)\n> — quickstart, architecture, threat model, channels, skills, the OpenAPI reference, and security,\n> all in one navigable place (built from `docs/` and published on every push to `main`).\n>\n> 🧭 **New here?** The [**hands-on tutorials**](https://ironsecco.github.io/ironclaw/tutorials/) take\n> you from `git clone` to a running agent: [your first sandboxed agent in 5 minutes](https://ironsecco.github.io/ironclaw/tutorials/first-agent/),\n> [connecting Slack](https://ironsecco.github.io/ironclaw/tutorials/connect-slack/), and\n> [writing a custom channel adapter](https://ironsecco.github.io/ironclaw/tutorials/custom-channel-adapter/).\n\n## Platform support\n\nIronClaw's security model rests on **gVisor** (`runsc`) — a user-space kernel that intercepts the\nagent's Linux syscalls and is the layer that actually *enforces* `network=none`, the seccomp\nsyscall allowlist, dropped Linux capabilities, and a read-only rootfs. **gVisor is Linux-only**, and\nthat one fact drives the whole platform story:\n\n| Capability | Linux + gVisor (production target) | macOS / Windows |\n|---|---|---|\n| Host side — control-plane, gateway, API, `ironctl`, web console | ✅ native | ✅ native (incl. native Windows) |\n| Real agent sandbox | ✅ gVisor (`runsc`) | ⚠️ `--runtime docker` only — runc in Docker Desktop's Linux VM. **macOS:** Docker Desktop. **Windows:** WSL2 (native Windows can't reach it — see below) |\n| Per-sandbox syscall interception | ✅ | ❌ not available |\n| Seccomp syscall allowlist | ✅ enforced | ❌ not applied on the Docker path |\n| `network=none` | ✅ enforced by the OCI spec | ⚠️ **not auto-enforced** — you must point `IRONCLAW_DOCKER_NETWORK` at a no-egress network |\n| Dropped capabilities · read-only rootfs | ✅ enforced by the runtime | ⚠️ only as strong as the Docker Desktop VM kernel |\n\n**On macOS** you can build, script, demo, and develop against the entire system natively, and you\ncan even run agents through Docker Desktop — but understand that the sandbox boundary then comes from\n**runc inside the Docker Desktop Linux VM, not gVisor.** There is no per-sandbox syscall\ninterception, the curated seccomp profile is not applied, and `network=none` is not enforced for you\n(the Docker isolator passes whatever network you configure straight through — set\n`IRONCLAW_DOCKER_NETWORK` to a no-egress bridge yourself). That is **weaker than the posture the\n[threat model](docs/threat-model.md) assumes.**\n\n### Windows via WSL2\n\nThe `install.ps1` PowerShell installer gives you the **host plane** natively on Windows:\n`ironclaw-controlplane.exe` and `ironctl.exe` run, the encrypted SQLCipher queue works, and `--dev`\nmode (no real sandbox) runs end-to-end. **A real agent sandbox does not run on native Windows** —\ngVisor (`runsc`) is Linux-only, and the Docker fallback talks to the Docker Engine over a **Unix**\nsocket (`/var/run/docker.sock`), which native Windows Docker Desktop does not expose (it serves a\nWindows named pipe instead). So on native Windows you get the control plane and `ironctl`, but the\nagent runtime has nowhere to launch.\n\n**To actually run agents on Windows, use WSL2:**\n\n```powershell\nwsl --install -d Ubuntu          # one-time: install WSL2 + Ubuntu, then reboot\n```\n\nThen, **inside the WSL2 Ubuntu shell**, install the Linux build and run it exactly as on Linux:\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/IronSecCo/ironclaw/main/scripts/install.sh | sh\n```\n\nInside WSL2, `/var/run/docker.sock` is present (Docker Desktop's WSL integration, or Docker installed\nin the distro), so `IRONCLAW_RUNTIME=docker` launches real Linux sandbox containers. For the full\ngVisor posture, install `runsc` inside the WSL2 distro just as you would on bare-metal Linux. Treat a\nWSL2 host the same as the Linux row above.\n\n**For anything past local development, run the sandbox host on Linux with gVisor** (bare-metal,\na VM, or WSL2). The control plane can live wherever you like — including native Windows — but it's\nthe agent sandbox that needs the Linux + gVisor substrate to give you the boundary IronClaw is built\naround.\n\n## Project status\n\n**Alpha.** The architecture is settled and the full control-plane and sandbox pipelines are\nimplemented and tested. The encrypted-queue binding is now wired:\n\n- **Encrypted-SQLite queue binding** — ✅ wired (**RFC-0001 applied**). `contract.Open*` open\n  per-session SQLCipher databases via cgo (`github.com/mutecomm/go-sqlcipher/v4`); a round-trip test\n  covers write→read, read-only-write rejection, wrong-key failure, and no-plaintext-on-disk. The\n  build now requires `CGO_ENABLED=1` (a C toolchain). `internal/host/queue` uses the live binding;\n  in-memory backends remain for `--dev` and tests.\n- **Sandbox rootfs provisioning** — ✅ wired via a pluggable provisioner: `isolation` builds the\n  hardened OCI spec, provisions the bundle rootfs (with image digest/signature verification against a\n  trust policy), and execs `runsc`. A real launch still needs `runsc` and a provisioned/signed image\n  present in the environment.\n- **Production hardening (Wave 4)** — durable/pluggable master-key custody, a Prometheus `/metrics`\n  surface, structured logging, host respawn + sandbox provider backoff, and model-proxy rate\n  caps/audit/redaction have landed **and are composed into `cmd/controlplane`**. The API-server\n  hardening knobs (optional TLS, rate-limit, body limits, `/readyz` readiness gate) exist as\n  `api.With*` options but aren't attached in the entrypoint yet (see the [roadmap](#roadmap)).\n\nSee the [roadmap](#roadmap) for what remains. You can build, test, and run the control-plane today;\na live sandbox launch needs `runsc` plus a provisioned image.\n\n## Prerequisites\n\n| Requirement | For | Notes |\n|-------------|-----|-------|\n| **Go 1.23+ and a C toolchain** | building everything | `CGO_ENABLED=1` is required — the encrypted-SQLite binding builds via cgo |\n| **containerd + gVisor (`runsc`)** | production sandboxing | runtime `io.containerd.runsc.v1`; not needed for `--dev` |\n| **Tailscale** | remote admin access | the control-plane API binds to the tailnet IP; no public port |\n| **SQLCipher (vendored)** | encrypted queues | the SQLCipher C amalgamation is vendored by the driver; no system lib needed |\n| **A model credential** | live model calls | an Anthropic / OpenAI / OpenRouter key, or a gateway like OneCLI — injected host-side into the model proxy, never into the sandbox ([Model providers](#model-providers)) |\n\nThe three external runtime dependencies (gVisor, Tailscale, the encrypted-SQLite binding) are\nintentionally **not vendored**. See [`deploy/README.md`](deploy/README.md) for host setup.\n\n## Installation\n\n### Homebrew (macOS / Linux)\n\n```sh\nbrew tap IronSecCo/ironclaw https://github.com/IronSecCo/ironclaw\nbrew install ironsecco/ironclaw/ironclaw\n```\n\nThis installs `ironctl`, `ironclaw-controlplane`, and `ironclaw-sandbox` from the release the\nformula currently pins. The formula pins each archive to the SHA-256 recorded in that release's\nsigned `SHA256SUMS`, so Homebrew verifies the download before installing. Confirm it with\n`ironctl version`.\n\nThe tap carries exactly one version at a time. An automated pull request bumps the formula after\nevery release, and it lands only once a required CI check has re-derived the formula from that\nrelease's cosign-verified `SHA256SUMS`, so the tap can briefly trail the newest release. Run\n`brew update` first, and see\n[Releases](https://github.com/IronSecCo/ironclaw/releases/latest) for the newest version. To\ninstall a specific version, including one the tap has not picked up yet, use the installer\nscript's `IRONCLAW_VERSION` ([below](#prebuilt-binaries-installer-script)).\n\n> **Use the fully-qualified name.** homebrew-core ships an *unrelated* formula also called `ironclaw`,\n> and core wins the bare name — so install `ironsecco/ironclaw/ironclaw`, not bare `ironclaw`. The\n> explicit tap URL is required too: our tap lives in this repo, not a `homebrew-ironclaw` repo.\n\n> In production the control plane usually runs as the GHCR container image (see the\n> [deployment guide](https://ironsecco.github.io/ironclaw/deployment/)); the native\n> `ironclaw-controlplane` binary is convenient for local / `--dev` runs.\n\n### Prebuilt binaries (installer script)\n\nOne command installs the latest release — `ironctl` and `ironclaw-controlplane`. The script\ndetects your OS/arch, downloads the matching archive from\n[GitHub Releases](https://github.com/IronSecCo/ironclaw/releases), and verifies its SHA-256\nchecksum before installing.\n\n**macOS / Linux**\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/IronSecCo/ironclaw/main/scripts/install.sh | sh\n```\n\n**Windows (PowerShell)**\n\n```powershell\nirm https://raw.githubusercontent.com/IronSecCo/ironclaw/main/scripts/install.ps1 | iex\n```\n\n> This installs the **host binaries** (`ironclaw-controlplane.exe` + `ironctl.exe`) and runs `--dev`\n> natively, but it **cannot run a real agent sandbox** — that needs Linux. To run agents on Windows,\n> install inside **WSL2**; see [Windows via WSL2](#windows-via-wsl2).\n\nA fresh release is published on every push to `main`, with prebuilt archives for:\n\n| OS | Architectures |\n|----|---------------|\n| macOS | Intel (`amd64`) · Apple Silicon (`arm64`) |\n| Linux | `amd64` · `arm64` |\n| Windows | `amd64` |\n\nThe installer reads a few environment variables (pass them on the `sh` side of the pipe):\n\n```sh\n# Pin a version instead of latest\ncurl -fsSL https://raw.githubusercontent.com/IronSecCo/ironclaw/main/scripts/install.sh | IRONCLAW_VERSION=v0.1.102 sh\n\n# Install system-wide (a normal user defaults to ~/.local/bin)\ncurl -fsSL https://raw.githubusercontent.com/IronSecCo/ironclaw/main/scripts/install.sh | sudo sh\n\n# Choose the install directory\ncurl -fsSL https://raw.githubusercontent.com/IronSecCo/ironclaw/main/scripts/install.sh | IRONCLAW_BINDIR=\"$HOME/bin\" sh\n```\n\nThen confirm what you installed:\n\n```sh\nironctl --version\n```\n\nPrefer to grab files by hand? Download the archive and `SHA256SUMS` for your platform from the\n[latest release](https://github.com/IronSecCo/ironclaw/releases/latest).\n\n### Version managers (mise / asdf)\n\nPin IronClaw per project with [mise](https://mise.jdx.dev) or [asdf](https://asdf-vm.com), no\naccount and no `sudo`. The quickest path uses mise's `ubi` backend to install the `ironctl` CLI\nstraight from the GitHub release (no plugin repo):\n\n```sh\nmise use -g \"ubi:IronSecCo/ironclaw[exe=ironctl]@latest\"\nironctl --version\n```\n\nFor both host binaries (`ironctl` + `ironclaw-controlplane`) and a pinned `.tool-versions`, use the\nasdf-style plugin under [`packaging/asdf-ironclaw/`](packaging/asdf-ironclaw/). It downloads the\nrelease tarball, verifies it against the published `SHA256SUMS`, and drops both binaries on the\nmanaged PATH:\n\n```text\nironclaw 0.1.217\n```\n\nThe plugin resolves as a standalone repo (asdf clones plugins by URL), so `asdf plugin add ironclaw`\nand `mise use asdf:...` become available once the plugin lands in its own `IronSecCo/asdf-ironclaw`\nrepository. Until then the scripts in `packaging/asdf-ironclaw/` are runnable directly (see that\ndirectory's `README.md`).\n\n### Verifying a release\n\nReleases are **signed and attested** — a keyless [cosign](https://docs.sigstore.dev/)\nsignature over `SHA256SUMS`, an **SBOM** (SPDX + CycloneDX), and build-provenance attestations for\nevery archive and the container image. For how releases are cut, verified, and yanked, see the\n[release runbook](docs/release-runbook.md).\n\n<details>\n<summary>Verifying a signed release</summary>\n\nEach release carries `SHA256SUMS` plus `SHA256SUMS.sig` + `SHA256SUMS.pem` (the cosign signature and\nits certificate), `*.spdx.json` / `*.cdx.json` SBOMs, and per-archive + image attestations.\n\nVerify the checksum signature (no key to manage — the identity is the release workflow):\n\n```sh\ncosign verify-blob SHA256SUMS \\\n  --signature SHA256SUMS.sig --certificate SHA256SUMS.pem \\\n  --certificate-identity-regexp '^https://github.com/IronSecCo/ironclaw/' \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com\nsha256sum -c SHA256SUMS        # then confirm your archive matches\n```\n\nVerify build provenance for an archive, an extracted binary, or the image:\n\n```sh\ngh attestation verify ironclaw_<version>_<platform>.tar.gz --repo IronSecCo/ironclaw\ngh attestation verify ./ironctl --repo IronSecCo/ironclaw   # a binary extracted from the archive\ngh attestation verify oci://ghcr.io/ironsecco/ironclaw-controlplane:latest --repo IronSecCo/ironclaw\n```\n\nThe container image also carries a signed **SBOM attestation** (CycloneDX) you can verify\nand read anonymously:\n\n```sh\ngh attestation verify oci://ghcr.io/ironsecco/ironclaw-controlplane:latest \\\n  --repo IronSecCo/ironclaw \\\n  --predicate-type https://cyclonedx.org/bom\n```\n\nEvery third-party GitHub Action is **pinned to a commit SHA**, builds use a **pinned\ntoolchain + `-trimpath`** and are checked for **bit-for-bit reproducibility** by a\ndouble-build CI job (`ironctl` and `sandbox` are verified byte-identical; the larger\ncontrol-plane binary is reproducible under newer Go and tracked for the pinned toolchain),\nand the project's supply-chain posture is scored continuously by\n[OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/IronSecCo/ironclaw) (see the badge above).\n\n</details>\n\n### From source\n\nRequires Go 1.23+ and a C toolchain (`CGO_ENABLED=1` — the encrypted-SQLite binding builds via cgo).\n\n```sh\n# Clone\ngit clone https://github.com/IronSecCo/ironclaw.git\ncd ironclaw\n\n# Build all binaries\nmake build            # == go build ./...\n\n# Or install the two host binaries onto your PATH\ngo build -o /usr/local/bin/ironclaw-controlplane ./cmd/controlplane\ngo build -o /usr/local/bin/ironctl               ./cmd/ironctl\n```\n\nFor a full system install — build and install the binaries, provision `/etc/ironclaw`\nand `/var/lib/ironclaw`, and enable the service (systemd on Linux, launchd on macOS) —\nrun [`sudo deploy/install.sh`](deploy/install.sh). It needs root to write under `/etc`\nand `/var/lib`. The external runtime dependencies it relies on (containerd + gVisor and\nTailscale) are set up separately — see [`deploy/README.md`](deploy/README.md).\n\n### With Docker (`docker compose`)\n\nSelf-host the control-plane in one command. From a clone:\n\n```sh\ncp .env.example .env          # fill in ANTHROPIC_API_KEY (optional to boot)\ndocker compose up -d          # builds locally on first run, or pulls the GHCR image\ndocker compose logs -f controlplane   # CLAIM the admin token printed once on first run\n```\n\nThe admin/API token is **minted on first run and printed once** in the logs (there is\nno recovery) unless you set `IRONCLAW_API_TOKEN` yourself. The admin API is published\non `127.0.0.1:8787` only — front it with Tailscale for remote access.\n\nPrefer the published image? It is pushed to GitHub Container Registry on every release:\n\n```sh\ndocker pull ghcr.io/ironsecco/ironclaw-controlplane:latest\n# or pin a release: docker pull ghcr.io/ironsecco/ironclaw-controlplane:v0.1.102\n```\n\nSet `IRONCLAW_IMAGE` in `.env` to pin that tag for `docker compose`. Every variable the\ncontrol-plane reads is documented in [`.env.example`](.env.example). The agent sandboxes\nthemselves are **not** compose services — the control-plane launches them as gVisor\n(`runsc`) children with `network=none`; running real sandboxes needs a runsc-capable\nhost (see [`deploy/README.md`](deploy/README.md)).\n\nGoing to production? The **[deployment guide](https://ironsecco.github.io/ironclaw/deployment/)**\ncovers the hardened, durable posture: locked-down [`deploy/docker-compose.prod.yml`](deploy/docker-compose.prod.yml)\n(read-only rootfs, dropped caps, resource limits) behind a TLS reverse proxy\n([`deploy/Caddyfile`](deploy/Caddyfile)), secrets via an env-file, encrypted-state\nbackup/restore, pinned-digest upgrades, and Prometheus `/metrics`.\n\n## Quickstart\n\nA fuller local walkthrough — run the control-plane **from source** in dev mode (no gVisor, binds to\nloopback) and drive it with the admin CLI:\n\n```sh\n# Terminal 1 — start the control-plane in dev mode\nexport ANTHROPIC_API_KEY=sk-ant-...        # held host-side; never enters the sandbox\nexport IRONCLAW_API_TOKEN=$(openssl rand -hex 32)\ngo run ./cmd/controlplane --dev --api-addr 127.0.0.1:8787\n\n# Terminal 2 — talk to the gateway with ironctl\nexport IRONCLAW_API_TOKEN=<same token as above>\n\n# Submit a capability change — it is HELD pending a human decision (the gateway choke point)\nironctl change submit --kind persona --group dev-agent --by alice\n\n# See what's waiting for approval, then approve or reject by id\nironctl change pending\nironctl change approve <change-id> --by alice\n\n# Inspect the append-only audit log\nironctl audit --limit 20\n```\n\nEvery mutation — persona, enabled tools, packages, wiring, permissions, mounts — flows through this\nsame gateway. There is no file-edit path that bypasses it.\n\n## Examples\n\nTwo of them run **end to end with zero credentials** — no model key, no channel tokens,\njust Docker. Copy one line and watch it work:\n\n<div align=\"center\">\n\n<img src=\"docs/assets/hello.svg\" width=\"760\" alt=\"hello-ironclaw terminal demo: one command (examples/hello-ironclaw/run.sh) builds the sandbox image, starts the offline mock-agent control-plane with no credentials, sends a chat through the real engage → per-session sandbox → encrypted queue → reply path, and prints PASS when the agent's reply comes back.\">\n\n<sub><b><a href=\"examples/hello-ironclaw/\">hello-ironclaw</a> — the canonical \"it works.\"</b> One command sends a chat through the <b>real</b> secured path (engage → per-session sandbox → encrypted queue → reply) and asserts the reply returns. Zero credentials; doubles as the CI smoke test. Animation freezes on the final frame under <code>prefers-reduced-motion</code>.</sub>\n\n<br><br>\n\n<img src=\"docs/assets/containment.svg\" width=\"760\" alt=\"live-containment terminal demo (final frame): a fully-jailbroken agent tries three escapes from inside the sandbox and each is BLOCKED — network exfil denied by network=none (only loopback), host filesystem read denied by the mount namespace, host takeover via the Docker Engine socket denied because the socket is never mounted in — ending with a containment summary that 3 of 3 escape attempts were denied and the box held.\">\n\n<sub><b><a href=\"examples/live-containment/\">live-containment</a> — watch it catch a real escape.</b> The 60-second security aha: one command engages a real sandbox, a fully-jailbroken agent <b>tries to break out</b> (network exfil, host-filesystem breakout, host takeover via the Docker socket), and your terminal shows each attempt <b>denied</b> plus a containment summary. The curated cut of <code>red-team-escape</code>. Zero credentials.</sub>\n\n<br><br>\n\n<img src=\"docs/assets/redteam.svg\" width=\"760\" alt=\"red-team-escape terminal demo: assuming a fully jailbroken agent, the harness runs an escape battery from inside the sandbox and prints a PASS table — network egress blocked (interfaces: lo), Docker socket absent, no sibling orchestration, host root not mounted, self-modification held at the gateway, host master and sibling keys unreachable — then reports every core containment assertion held.\">\n\n<sub><b><a href=\"examples/red-team-escape/\">red-team-escape</a> — isolation you can prove.</b> The full six-assertion battery behind <code>live-containment</code>: adds sibling-breakout and cross-session key-custody probes and emits a <b>signed, versioned containment report</b>; runs as the CI containment gate on every push. Zero credentials.</sub>\n\n</div>\n\nRunnable recipes live in [`examples/`](examples/) — each is a directory with a `README.md` and a `setup.sh`.\nThree of them ship a `run-mock.sh` that drives the **whole** inbound → agent → reply pipeline on the\noffline `mock` provider, so a fresh clone runs them with **no model key and no channel tokens**:\n\n```sh\ndocker compose -f docker-compose.demo.yml up -d --build   # seeds the offline mock-agent\n./examples/scheduled-report/run-mock.sh                   # cron-style self-scheduling summary\n./examples/webhook-responder/run-mock.sh                  # inbound webhook → agent reply\n./examples/slack-triage/run-mock.sh                       # classify/label every message\n```\n\n- [`scheduled-report/`](examples/scheduled-report/) — wakes itself on a schedule (`schedule_task`), summarizes, posts to a channel. *(credential-free demo)*\n- [`webhook-responder/`](examples/webhook-responder/) — routes an inbound HTTP webhook to an agent that replies. *(credential-free demo)*\n- [`slack-triage/`](examples/slack-triage/) — classifies/labels every incoming Slack message. *(credential-free demo)*\n- [`personal-assistant/`](examples/personal-assistant/) — a private 1:1 assistant on Telegram, plus a walk-through of the mandatory change-approval flow.\n- [`channel-triage/`](examples/channel-triage/) — a Slack triage bot that engages only on `@mention`, only for known senders.\n- [`multi-agent-team/`](examples/multi-agent-team/) — two agents sharing one channel, separated by engage mode and priority.\n\n## Usage\n\n### `ironclaw-controlplane` — the host daemon\n\n```sh\nironclaw-controlplane \\\n  --api-addr \"$(tailscale ip -4):8787\" \\            # bind to the tailnet IP (no public port)\n  --model-proxy-socket /run/ironclaw/modelproxy.sock \\\n  --runtime runsc \\                                 # container runtime for sandboxes\n  --state-dir /var/lib/ironclaw \\\n  --sweep-interval 60s\n```\n\n| Flag | Default | Purpose |\n|------|---------|---------|\n| `--api-addr` | `127.0.0.1:8787` | control-plane API address; set to the tailnet IP in production |\n| `--model-proxy-socket` | `/run/ironclaw/modelproxy.sock` | unix socket bound into each sandbox for model egress |\n| `--state-dir` | OS-specific | gateway change store, audit log, keystore |\n| `--runtime` | `runsc` | OCI runtime for sandboxes |\n| `--bundle-root` | `<state-dir>/bundles` | per-session OCI bundles |\n| `--sweep-interval` | `60s` | stale-sandbox / due-message sweep cadence |\n| `--egress-socket` | `\"\"` (sealed) | opt-in: host unix socket for the egress broker, bound into each sandbox so an agent can reach **approved** external hosts (deny-by-default, audited) |\n| `--egress-allow` | `\"\"` | comma-separated hostnames the egress broker permits (only with `--egress-socket`) |\n| `--search-backend` | `\"\"` (off) | give each sandbox the `web_search` tool: `duckduckgo` (keyless) or `brave[:cred]` (keyed via the vault). Requires `--egress-socket`; the backend's host is auto-added to the allowlist |\n| `--mcp-catalog` | `\"\"` (off) | opt-in: enable MCP servers — a per-session host broker, the `mcp_access` change kind, and the **MCP** console tab. The 0600 JSON catalog of configured servers |\n| `--mcp-isolation` | `container` | how **local** (stdio) MCP servers run: `container` (hardened, `network=none` — production) or `none` (bare host process — dev only) |\n| `--mcp-runtime` / `--mcp-image` | `\"\"` | OCI runtime (e.g. `runsc` for gVisor) and default image for isolated local MCP servers |\n| `--dev` | `false` | loopback bind, no gVisor — local development only; also opens a **DuckDuckGo-only** egress path so `web_search` works out of the box, and enables MCP with `--mcp-isolation=none` |\n\n#### MCP servers\n\nExtend an agent with the tools of a **Model Context Protocol** server — local (a stdio\nsubprocess) or remote (an HTTPS endpoint) — without weakening the sandbox. MCP runs\n**host-side only**: a local server is isolated in a hardened `network=none` container, a\nremote one is dialed over TLS, and the sandbox reaches neither directly — it talks to a\n**per-session broker socket** where every call is checked against a **per-tool,\nhuman-approved grant** and audited. This closes the \"blind MCP approval\" gap the\nreference design had. Enable it with `--mcp-catalog`, add servers + grant agents on the\nconsole's **MCP** tab, and try it end to end with the bundled `cmd/mcp-sample` server.\nFull guide: [docs/mcp.md](docs/mcp.md).\nTo expose IronClaw's `sandbox_exec` tool to Claude Desktop, Cursor, or Windsurf\nas an MCP server, see [docs/mcp-server/](docs/mcp-server/index.md).\n\nEnvironment: `ANTHROPIC_API_KEY` (model proxy credential, host-only) and `IRONCLAW_API_TOKEN`\n(bearer token required on every API call when set).\n\n#### Web search\n\nThe sandbox is `network=none`; it can only reach hosts through the host-mediated, audited\negress broker. The `web_search` tool rides that broker, so it is **off by default** and turns\non only with both `--egress-socket` and `--search-backend`:\n\n- `duckduckgo` — keyless, no secret. The quickest way to a working search, but DuckDuckGo's\n  keyless API returns instant answers / related topics rather than a full ranked web index, so\n  specific lookups (e.g. a person's name) can come back thin.\n- `brave[:cred]` — Brave Search reached **by name** through the credential vault\n  (`vault://<cred>/…`), so the API key stays host-side in the injector and never enters the\n  sandbox. Requires `--vault-endpoint` with a matching credential.\n\n`--dev` enables the DuckDuckGo backend automatically (placing the egress socket next to the\nmodel-proxy socket so it rides the same sandbox mount). Under the Docker isolator, make sure the\ndirectory holding those sockets is in `IRONCLAW_DOCKER_BINDS` so the sandbox can reach it.\n\n### `ironctl` — the admin CLI\n\nA thin client of the control-plane API. `--addr` defaults to `http://127.0.0.1:8787`; the bearer\ntoken comes from `IRONCLAW_API_TOKEN` or `--token`.\n\n```sh\nironctl change submit  --kind <k> --group <g> --by <user>   # k: persona|enabled_tools|packages|wiring|permissions|mounts\nironctl change pending                                       # list changes awaiting a decision\nironctl change history                                       # all changes and their outcomes\nironctl change approve <id> --by <user>\nironctl change reject  <id> --by <user>\nironctl audit [--limit N]                                    # append-only gateway audit log\n```\n\n#### Define an agent the easy way\n\nYou don't have to know tool names or hand-write JSON. Pick a starter template, tweak it, and go —\nin one step, from the CLI or the web console's **Agents → Create** builder:\n\n```sh\nironctl tools                                  # browse every built-in tool, grouped, with descriptions\nironctl agent templates                        # list starter presets (assistant, researcher, …)\n\n# Guided wizard (run in a terminal with no flags): name → template → persona → tools → confirm\nironctl agent create\n\n# Or one-shot/scriptable — template + a couple extra tools, persona override, default model:\nironctl agent create --name \"Research Bot\" --template researcher --tool schedule_task\nironctl agent create --name \"Helper\" --template assistant --all-tools --yes\n\nironctl agent list                             # all agents, with model + live session/channel counts\nironctl agent show research-bot                # persona, model, enabled tools, installed skills\n```\n\n**Persona as separate documents.** Rather than one opaque prompt, an agent's persona is split by\nconcern — **IDENTITY.md** (who it is), **SOUL.md** (personality/voice), and **AGENTS.md** (how it\nworks) — which compose into the system prompt. Set them inline, or point at a directory of those\nfiles (the builder shows the same three fields):\n\n```sh\nironctl agent create --name \"Atlas\" \\\n  --identity \"You are Atlas, a research assistant for the data team.\" \\\n  --soul     \"Curious and precise. You cite sources and admit uncertainty.\" \\\n  --instructions \"Search before answering; prefer primary sources; summarize with links.\"\n\nironctl agent create --name \"Atlas\" --persona-dir ./atlas/   # loads IDENTITY.md / SOUL.md / AGENTS.md\n```\n\nThis defines the agent (name + persona docs + model + tools) in a single operator-direct write.\nEnabling a web/API tool only makes it *visible* to the agent — actual egress still requires an\napproved host through the gateway, so the network posture is unchanged.\n\n### `sandbox` — the in-sandbox agent\n\nLaunched by the control-plane's isolator, not by hand. It receives its session key and queue paths\nand runs the reasoning loop. Key flags (`cmd/sandbox`): `--inbound`, `--outbound`, `--key`,\n`--workspace`, `--heartbeat`, `--model-socket`, `--model-host`, `--model`.\n\n### Control-plane HTTP API\n\n| Method & path | Purpose |\n|---------------|---------|\n| `GET  /healthz` | liveness (unauthenticated) |\n| `POST /v1/changes` | submit a `ChangeRequest` |\n| `GET  /v1/changes/pending` | list pending changes |\n| `GET  /v1/changes/history` | list all changes |\n| `POST /v1/changes/{id}/decision` | record an approve/reject decision |\n| `GET  /v1/audit` | read the audit log |\n\n## Model providers\n\nBy default every agent talks to **Anthropic** (Claude). You can point an agent at **OpenAI** or\n**OpenRouter** instead, or — without IronClaw holding any model key at all — route through an\noperator-run **credential gateway** such as **OneCLI**, which injects the real credential at request\ntime. In every case the **sandbox stays `network=none` and credential-free**: it reaches the model\nonly through the host model-proxy unix socket, and the host proxy authenticates the call and enforces\nthe egress allowlist. The backend is chosen **per agent group, host-side** — a sandbox can never pick\nor change its own provider.\n\n### Direct provider keys\n\nSet one or more keys host-side (daemon env, or `.env` for `docker compose`). A provider's upstream\nhost is allowlisted only when its key is present:\n\n```sh\nexport ANTHROPIC_API_KEY=sk-ant-...     # default / primary\nexport OPENAI_API_KEY=sk-...            # optional\nexport OPENROUTER_API_KEY=sk-or-...     # optional\n```\n\n### Via a credential gateway like OneCLI (ChatGPT/Codex — no key inside IronClaw)\n\nA credential gateway is a host-local HTTP `CONNECT` proxy that holds the real credential and injects\nit per request, so **neither the control-plane nor the sandbox ever sees a model key.** This is how\nyou power an agent with a **ChatGPT/Codex** account via **OneCLI**: IronClaw's `codex` provider\nspeaks the ChatGPT Codex Responses API (`chatgpt.com`) and OneCLI attaches the OAuth credential.\n\nRun OneCLI on the host (its default address is `127.0.0.1:10255`), then point the model-proxy at it\nand allowlist the host it serves:\n\n```sh\n# The gateway URL carries your per-agent OneCLI token as Basic userinfo — Go's HTTP\n# client sends it as Proxy-Authorization on CONNECT. The gateway terminates TLS with\n# its own CA, so upstream TLS verification is skipped (intended for a loopback gateway).\nexport IRONCLAW_MODEL_GATEWAY_URL=\"http://x:aoc_<your-onecli-agent-token>@127.0.0.1:10255\"\nexport IRONCLAW_MODEL_GATEWAY_HOSTS=\"chatgpt.com\"\n\n# No ANTHROPIC_API_KEY needed — the gateway is the only credential path. Make the\n# default backend Codex so every agent uses it out of the box:\nexport IRONCLAW_DEV_PROVIDER=codex\nexport IRONCLAW_DEV_MODEL=gpt-5.5\n\nironclaw-controlplane --api-addr 127.0.0.1:8787   # (+ your other flags)\n```\n\nWhen a gateway is set, don't also set a key for the host it serves — the gateway is the credential\npath, and the control-plane injects nothing for the gateway's hosts. Under `docker compose` the\ngateway must be reachable *from the container*, so use `host.docker.internal:10255` (Docker Desktop)\nor put OneCLI and the control-plane on a shared Docker network instead of `127.0.0.1`.\n\n### Run a 100% local model (Ollama, LM Studio, vLLM) — no cloud key\n\nPoint IronClaw at a self-hosted **OpenAI-compatible** endpoint and the whole stack runs on your own\nbox with **zero cloud credentials** — nothing leaves the machine. Ollama, LM Studio, vLLM, and\nllama.cpp all expose the OpenAI `/v1` API (Ollama at `http://localhost:11434/v1`).\n\n```sh\nollama pull llama3.2                                # 1. run a model locally\nexport IRONCLAW_LOCAL_MODEL_URL=http://localhost:11434/v1\nexport IRONCLAW_LOCAL_MODEL=llama3.2                # 2. point IronClaw at it\nironclaw-controlplane --dev --api-addr 127.0.0.1:8787   # 3. chat — no API key\n```\n\nThis allowlists the local host, forwards to it over plain HTTP (these servers serve no TLS), and\nmakes it the deployment-default model, so every agent group without a pinned provider runs local. No\nkey is required; set `IRONCLAW_LOCAL_MODEL_KEY` only for the rare local server (e.g. a guarded vLLM)\nthat requires one. Under `docker compose` the server must be reachable *from the control-plane\ncontainer*, so use `http://host.docker.internal:11434/v1` (Docker Desktop) instead of `localhost`.\nFull walkthrough: **[Run IronClaw with a 100% local model (Ollama)](docs/tutorials/local-model-ollama.md)**.\n\n### Choosing the provider per agent\n\n`IRONCLAW_DEV_PROVIDER` / `IRONCLAW_DEV_MODEL` set the deployment-wide default for any agent group\nthat doesn't pin one (the env names keep their `DEV_` prefix but apply deployment-wide). To choose\nper agent instead — a gateway-approved change, like any other config:\n\n```sh\nironctl agent create --name \"Codex Bot\" --provider codex  --model gpt-5.5\nironctl agent create --name \"GPT Bot\"   --provider openai --model gpt-4o\nironctl agent create --name \"Local Bot\" --provider local  --model llama3.2   # uses IRONCLAW_LOCAL_MODEL_URL\n```\n\nValid `--provider` values: `anthropic` (default), `openai`, `openrouter`, `codex`, `gemini`,\n`vertex`, `local` (a self-hosted OpenAI-compatible endpoint — Ollama/LM Studio/vLLM/llama.cpp), and\n`mock` (a deterministic, offline backend for demos and tests). Each maps to a model-proxy-allowlisted\nupstream; `codex` targets `chatgpt.com` and defaults to the `gpt-5.5` model, and `local` inherits the\nloopback host from `IRONCLAW_LOCAL_MODEL_URL`.\n\n## Configuration\n\n- **State** lives under `--state-dir`: the durable gateway change store (survives restart), the\n  append-only JSONL audit log, and the host keystore.\n- **Secrets** are host-only. The model credential (an Anthropic / OpenAI / OpenRouter key, or a\n  credential gateway like OneCLI — see [Model providers](#model-providers)) is applied to outbound\n  model calls by the host `modelproxy`; the sandbox never sees it and has `network=none`. Per-session 256-bit keys are\n  gene",
  "bytes": 60000,
  "sha": "2d3417bb4b3a0a7b9c3ea124c9636a902788276fdf7b20340cb178f698fb55a2",
  "repo_slug": "ironsecco/ironclaw",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_ironsecco_ironclaw_5238935d/readme"
}