# IronCurtain compiles an English constitution into a tool-call policy

> IronCurtain is a research-prototype agent runtime that assumes the model is compromised and enforces anyway: you write what the agent may not do in plain English, an LLM pipeline compiles it into a deterministic policy, generated test scenarios validate it, and every tool call is allowed, denied or escalated at the boundary.

**provos/ironcurtain** — A secure* runtime for autonomous AI agents. Policy from plain-English constitutions. (*https://ironcurtain.dev)

- Repository: https://github.com/provos/ironcurtain
- Website: https://ironcurtain.dev
- Stars: 614 · Forks: 80
- Language: TypeScript
- License: Apache-2.0
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/provos-ironcurtain

## The agent is untrusted, so the model is not the control

The problem statement is about privilege, not about model behaviour. Today's agent frameworks give the agent the same privileges as the user, with full access to the filesystem, credentials and network, and security researchers call that ambient authority. Under that arrangement a single prompt injection, or drift over several turns, is enough to delete files, exfiltrate data or push malicious code.

The two usual answers both cost something. Confine the agent to a narrow sandbox and you have limited its usefulness; ask the user to approve every action and you have limited its autonomy.

IronCurtain's stance is that security should not depend on the model being good. The agent is treated as untrusted on the assumption that the LLM may already be compromised by injection or drift, and four ideas follow from that. English in, enforcement out: you write intent such as no destructive git operations without approval, and the system compiles it. Semantic interposition replaces raw system access with MCP servers for things like the filesystem and git. Defence in depth means the agent code runs in a V8 isolate with no direct host access, so the only way out is a semantically meaningful tool call, and each one is checked.

## A constitution, a compiler, and generated test scenarios

The input is a constitution, a short document describing what your agent is and is not allowed to do. The pipeline around it has three stages rather than one.

First the constitution is compiled into a deterministic security policy using an LLM pipeline. Deterministic is the important word: after compilation, the rules that run at request time no longer involve a model. The project states this as English in, enforcement out, with the LLM used once at compile time instead of on every call.

Second, the compiled rules are validated against generated test scenarios before anyone relies on them. Third, the policy is enforced at runtime on every tool call, where the policy engine can allow, deny or escalate to the user for approval.

The repository ships the artefacts of those stages as files rather than as internal state. src/config/constitution.md and constitution-user-base.md are the constitutions, while src/config/generated/ holds compiled-policy.json, tool-annotations.json and test-scenarios.json. The package scripts make the stages individually runnable: compile-policy, compile-policy:readonly and annotate-tools are all separate commands.

## Two session modes, one trust model

IronCurtain supports two ways of running, and the difference is who writes the agent code.

In the builtin agent, called Code Mode, IronCurtain's own LLM agent writes TypeScript snippets that execute in a V8 sandbox. IronCurtain controls the agent, the sandbox and the policy engine, so every tool call leaves the sandbox as a structured MCP request, passes the policy engine, and only then reaches the real MCP server.

In Docker Agent Mode, an external agent such as Claude Code, Goose or Codex runs inside a container with no direct public-network access. IronCurtain still mediates the effects. LLM API calls go through a TLS-terminating MITM proxy with a host allowlist and a fake-to-real key swap, MCP tool calls pass through the same policy engine, and supported package installations go through validating package proxies.

The design intent is stated for both modes: the agent is untrusted, and security is enforced at the boundary rather than assumed from the model following instructions. SANDBOXING.md holds the full architecture with diagrams, a layer-by-layer trust analysis and macOS platform notes.

## Node 25 is excluded on purpose

The supported runtime lines are specific. IronCurtain tests Node.js 24 and 26, the even-numbered major lines, because both install prebuilt isolated-vm binaries. Node 25 is described as unsupported and untested, and the package manifest enforces the same split with engines set to ^24.0.0 || ^26.0.0.

The reason is the sandbox. The builtin agent runs its TypeScript snippets inside a V8 isolate, and isolated-vm supplies that isolate. When a release line has no prebuilt binary for it, installing the runtime means compiling one, which is exactly the kind of surprise a security tool should not have on its install path.

Installation is otherwise ordinary. End users install the CLI globally:

```bash
npm install -g @provos/ironcurtain
```

Development installs from a clone with npm install. The package publishes under the @provos scope, is Apache-2.0, exposes a single binary named ironcurtain pointing at dist/cli.js, and works as an npm workspace so that packages/* are built alongside it.

## Nested Docker keeps the host socket out of reach

There is an opt-in nested Docker mode, and its purpose is narrow: developer sessions get a private, ephemeral Docker daemon without the host Docker socket being exposed.

That distinction matters more than it sounds. Granting a sandboxed agent access to the host socket is the usual way a container boundary is quietly undone, and this mode avoids needing the socket at all. Its offline, images and packages modes separate three different things: using local images, mediating public image pulls, and builds of supported packages.

Coverage is uneven by design. Implemented profiles cover Apple Container and Docker Desktop on macOS, and WSL2 with Docker Desktop on amd64. Native Linux Engine is not admitted for nesting, and the configuration reference is where the prerequisites and limitations live.

Docker itself is optional for running the runtime. It is not required, but it is strongly recommended for Docker Agent Mode, which the project describes as providing the strongest isolation. On macOS 26+ with Apple silicon, Apple container is an alternative backend, giving a VM per container, used automatically when its services are running and selected through the containerRuntime setting in ironcurtain config.

## Mutations escalate, and the auto-approver can clear them

The default policy is tuned for developer work rather than for maximum autonomy. Read-only operations are allowed outright, while mutations, meaning writes, pushes and pull request creation, escalate for human approval. You can use the runtime immediately after setup without writing a constitution first.

The demo shows what escalation looks like when it works in your favour. The agent is asked to clone a repository and push changes. Both git_clone and git_push are escalated by the policy engine, and the auto-approver approves them automatically, because the user's trusted input from command mode with Ctrl-A had already provided clear intent, so no manual /approve was needed.

That is the design in miniature: the boundary still sees every call, and the escalation is still recorded, but intent captured at the keyboard is allowed to satisfy it. The alternative reading is that a human who just typed the instruction does not need to type it again, which is the argument the project is making against approving every action.

## Keys come from three places with a fixed precedence

An API key for at least one LLM provider is required, with Anthropic, Google and OpenAI named. Where the key lives is flexible, and the precedence is stated rather than implied: environment variables take precedence over config file values.

The three sources are environment variables, a .env file in the project root which is loaded automatically through dotenv, and ~/.ironcurtain/config.json, edited through the ironcurtain config command. The recognised variable names are ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY and OPENAI_API_KEY.

The first-start wizard, ironcurtain setup, walks through GitHub token setup, a web search provider, model selection and other settings, and creates ~/.ironcurtain/config.json with your choices. It has to be run explicitly before the recommended mux path, and it also runs by itself on the first non-mux ironcurtain start.

Two more variables appear in the example environment file and are worth knowing before the first run: AUDIT_LOG_PATH points at ./audit.jsonl, and ALLOWED_DIRECTORY is set to /tmp/ironcurtain-sandbox.

## ironcurtain mux is the recommended way to drive an external agent

The recommended way to use the runtime is the multiplexer. It gives you the full power of your agent's interactive TUI, whether that is Claude Code or Goose, while IronCurtain mediates every tool call through its policy engine, all inside a single terminal.

The full agent TUI capability is where the security story has to hold up, because an interactive agent wants a terminal, and a terminal is exactly what you do not want to hand to an untrusted process. The arrangement described is that the agent runs in a PTY inside a Docker container with no network access, so the model can drive an interactive session while its effects still leave only through checked tool calls.

That also explains the emphasis on Docker earlier in the setup. The multiplexer is the path that needs the strongest isolation, and the runtime's own recommendation is to have Docker available even though it is not a hard requirement.

## Conclusion

IronCurtain fits a team that has an agent doing real work with real credentials and cannot accept either a narrow sandbox or a confirmation prompt on every action. It does not fit production software, because the project labels itself a research prototype and warns that APIs, configuration formats and architecture may change. Before you rely on it, read the section on what secure means on the project site rather than taking the word at face value, test your own constitution against the generated scenarios, and confirm your Node line is 24 or 26, since 25 is unsupported and the sandbox depends on prebuilt isolated-vm binaries.

## FAQ

### What does IronCurtain actually do?

It is a runtime for autonomous AI agents where security policy comes from a plain-English constitution. That constitution is compiled into a deterministic policy, validated against generated test scenarios, and then enforced on every tool call, which the policy engine can allow, deny or escalate for approval.

### Which Node versions does IronCurtain support?

Node.js 24 or 26, the even-numbered major lines it tests, both of which install prebuilt isolated-vm binaries. Node 25 is unsupported and untested, and the package manifest sets engines to ^24.0.0 or ^26.0.0.

### Do I need Docker to run IronCurtain?

It is not required, but it is strongly recommended for Docker Agent Mode, which the project describes as providing the strongest isolation. On macOS 26+ with Apple silicon, Apple container works as an alternative backend that gives a VM per container.

### How does IronCurtain handle an external agent such as Claude Code?

In Docker Agent Mode the external agent runs inside a container without direct public-network access. LLM API calls pass through a TLS-terminating MITM proxy with a host allowlist and a fake-to-real key swap, MCP tool calls pass through the same policy engine, and supported package installs go through validating package proxies.

### What is the difference between the builtin agent and Docker Agent Mode?

The builtin agent, called Code Mode, is IronCurtain's own and writes TypeScript snippets that execute in a V8 sandbox IronCurtain controls. Docker Agent Mode instead runs an external agent such as Claude Code, Goose or Codex inside a container while IronCurtain mediates its effects.

### Where does IronCurtain keep its API keys?

In environment variables, in a .env file in the project root loaded automatically through dotenv, or in ~/.ironcurtain/config.json through the ironcurtain config command, with environment variables taking precedence. The recognised names are ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY and OPENAI_API_KEY.

## Sources

- [License: Apache-2.0](https://github.com/provos/ironcurtain/blob/master/LICENSE)
- [Project website](https://ironcurtain.dev)
- [provos/ironcurtain on GitHub](https://github.com/provos/ironcurtain)
- [README](https://github.com/provos/ironcurtain/blob/master/README.md)
- [Releases](https://github.com/provos/ironcurtain/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/provos-ironcurtain
