---
slug: "iota-policy-pi-extension"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/gokalper/iota-policy@main/README.md"
repo: "https://github.com/gokalper/iota-policy"
source_file: "README.md"
branch: "main"
---
# iota

**One policy file for every coding agent.**

[![CI](https://github.com/gokalper/iota-policy/actions/workflows/ci.yml/badge.svg)](https://github.com/gokalper/iota-policy/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/%40iota-policy%2Fpi-extension?label=pi-extension)](https://www.npmjs.com/package/@iota-policy/pi-extension)
[![npm](https://img.shields.io/npm/v/%40iota-policy%2Fcore?label=core)](https://www.npmjs.com/package/@iota-policy/core)
[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)

Every agent harness reinvents the same decision — what may this agent read, write, run, and fetch — in its own proprietary, non-portable config. iota is a portable declaration (`agents.yaml`) plus enforcement that travels with it. We don't build another enforcer; we build the format, the engine, and the shells that plug it into each harness natively.

```yaml
# agents.yaml — lives in your repo like .gitignore
version: 0.1

fs:
  read:
    allow: ["**"]
    deny: ["**/.env*", "secrets/**"]
  write:
    allow: ["src/**", "test/**", "*.md"]
  delete: ask

exec:
  actions:
    test: "npx vitest run <paths?>"
    deps: "npm install <pkg?>"
  patterns:
    - match: "git *"
      verdict: allow
      best_effort: true
  default: ask

net:
  allow: ["registry.npmjs.org", "*.github.com"]
  default: deny
```

## Packages: which one do I install?

| Package | What it is | Who installs it |
|---|---|---|
| [`@iota-policy/pi-extension`](https://github.com/gokalper/iota-policy/tree/HEAD/packages/pi-extension) | The pi package: intercepts tool calls, bless prompts, decision log | **You**, via `pi install` — this is the only package pi users touch |
| [`@iota-policy/core`](https://github.com/gokalper/iota-policy/tree/HEAD/packages/policy-core) | The engine library: `agents.yaml` parser + verdict evaluator, pure functions, no harness hooks | Harness/tool authors, as a normal npm dependency. **Not a pi extension** — pi will report it invalid if you try to install it, and the pi-extension already pulls it in automatically |

## Use it today with pi

[pi](https://github.com/badlogic/pi-mono) ships no permission layer by design. iota is that layer — in-process, ~0.2 ms per decision, zero extra tokens:

```bash
pi install npm:@iota-policy/pi-extension
pi              # inside pi, run /iota init to scaffold agents.yaml
                # (iota nudges you if none exists; first governed call prompts a bless)
```

Prefer to scaffold before launching pi (or in CI)? `npx -p @iota-policy/pi-extension iota-pi init` writes the same starter policy from the shell.

Package docs: [@iota-policy/pi-extension](https://github.com/gokalper/iota-policy/blob/HEAD/packages/pi-extension/README.md)

## Policy controls

Every verdict is `allow`, `deny`, or `ask`. All defaults are fail-closed.

| Control | What it does |
|---|---|
| `fs.read` / `fs.write` / `fs.delete` | Glob allow/deny lists per operation, or a bare verdict. **Deny always wins**; anything matching no allow glob is denied. `*` matches within a path segment, `**` any depth, `{a,b}` alternation; dotfiles are matched (`.env` is excludable) |
| Workspace confinement | Absolute paths, `~`, and `..` escapes are denied unconditionally — no policy can grant access outside the workspace root |
| Self-protection | Writes/deletes to `agents.yaml` and harness config paths (`.claude/`, `.codex/`) are always denied, non-overridable. A policy that can authorize edits to its own enforcement is not a policy |
| `exec.actions` — named actions | Map intent to an exact program + frozen flags: `test: "npx vitest run <paths?>"`. Refuses to pattern-match, so refuses to be pattern-evaded — `sh -c`, pipes, `$(…)`, env prefixes all fall through to `default` |
| Typed argument slots | `<paths>` (workspace-relative, no flags/absolute/`..`), `<pkg>` (npm-name-shaped), `<word>` (single safe token); `?` variants allow empty. Validated by type, never by user-supplied regex |
| `exec.patterns` — escape hatch | Glob over the raw command string, **must** carry `best_effort: true` — the weakness is confessed in the file, resurfaced at bless time and in every log entry |
| `exec.default` | Catches every unmapped command (default `ask`) |
| `net` | Domain allowlist; `*.github.com` matches subdomains but never the apex; default deny. (Enforced at compile targets; in pi, egress rides `exec` — see honesty notes) |
| `ask` semantics | Interactive → human prompt. CI/headless → **deny**, always logged. No timeout-allow. No config option to change this — deliberately |
| Layering | org → user → repo, **tighten-only**, strictest wins: a repo policy can narrow your baseline, never widen it. A malicious repo's policy is structurally inert |
| Bless | Repo policies govern nothing until you approve them: hash-pinned, shows every grant + best-effort entry. Any file change re-prompts **with a diff**; rejections persist until the file changes |
| Absent policy | No `agents.yaml` = not governed. No implicit policy, ever. But a present-and-*invalid* file **fails closed** — everything blocks until it parses |
| Decision log | Every evaluation → one JSONL line: verdict, matched rule, reason, best-effort flag, what actually happened, timing |

## Sample policies

**Node/TypeScript development** — day-to-day work, tests and deps allowed by name:

```yaml
version: 0.1
fs:
  read:
    allow: ["**"]
    deny: ["**/.env*", "**/*.pem", "**/id_*", ".git/config"]
  write:
    allow: ["src/**", "test/**", "*.md", "package.json"]
  delete: ask
exec:
  actions:
    test: "npx vitest run <paths?>"
    lint: "npx eslint ."
    build: "npm run build"
    deps: "npm install <pkg?>"
  patterns:
    - match: "git *"
      verdict: allow
      best_effort: true
  default: ask
net:
  allow: ["registry.npmjs.org"]
  default: deny
```

**Docs-only contributor** — the agent edits prose, nothing else:

```yaml
version: 0.1
fs:
  read:
    allow: ["**"]
    deny: ["**/.env*", "secrets/**"]
  write:
    allow: ["docs/**", "*.md"]
  delete: deny
exec:
  default: deny
net:
  default: deny
```

**Read-only investigator** — audits and CI analysis; can look, run named checks, change nothing (`ask` auto-denies headless):

```yaml
version: 0.1
fs:
  read:
    allow: ["**"]
    deny: ["**/.env*", "**/*.pem"]
exec:
  actions:
    test: "npm test"
    typecheck: "npx tsc --noEmit"
  default: ask
net:
  default: deny
```

**Tight user baseline** — put this in `~/.config/iota/agents.yaml`; no repo policy can widen it:

```yaml
version: 0.1
fs:
  read:
    allow: ["**"]
    deny: ["**/.env*", "**/*.pem", "**/id_*"]
  write:
    allow: ["src/**", "test/**", "docs/**", "*.md", "package.json"]
net:
  allow: ["registry.npmjs.org", "*.github.com"]
  default: deny
```

## Design: one engine, three shells

One pure-TypeScript engine ([`@iota-policy/core`](https://github.com/gokalper/iota-policy/tree/HEAD/packages/policy-core)) answers `evaluate(operation) → allow | deny | ask` — synchronous, no I/O, no daemon. Three shells wrap it: a **compiler** to native harness configs (Claude Code `settings.json`, Codex approval policies — roadmap), an **embeddable library** (how the pi extension works today), and an MCP server (last resort, documented as such). When a compile target can't express a rule, compilation fails closed and emits a machine-readable gap report — never a silent weakening. A language-neutral [conformance suite](https://github.com/gokalper/iota-policy/tree/HEAD/packages/policy-core/conformance) makes "supports agents.yaml" a verifiable claim.

| Target | Status |
|---|---|
| pi | shipped — [`@iota-policy/pi-extension`](https://github.com/gokalper/iota-policy/tree/HEAD/packages/pi-extension) |
| Claude Code (compile to settings.json/hooks) | roadmap |
| OpenAI Codex (compile to approval policy + sandbox) | roadmap |
| `audit` / `test` (attack scenarios → per-harness enforcement matrix) | roadmap |
| MCP shell | roadmap (last resort) |

## What we claim, exactly

The embedded library governs **tool-call intent, not execution** — an allowed bash command's child processes aren't constrained by an in-process interceptor. The compiled OS/harness config is the enforcement layer; recommended deployment is always both. `best_effort` rules are best-effort and permanently visible. Repo policy binds agents whose harness runs iota — "enforced where installed, auditable everywhere." The full adversary analysis, including the self-escalation and lobbying-the-human attempts we've already caught live, is in [THREAT-MODEL.md](https://github.com/gokalper/iota-policy/blob/HEAD/THREAT-MODEL.md).

## Documents

[SPEC.md](https://github.com/gokalper/iota-policy/blob/HEAD/SPEC.md) — the normative format spec (the actual product) · [THREAT-MODEL.md](https://github.com/gokalper/iota-policy/blob/HEAD/THREAT-MODEL.md) · [CONTRIBUTING.md](https://github.com/gokalper/iota-policy/blob/HEAD/CONTRIBUTING.md) · [SECURITY.md](https://github.com/gokalper/iota-policy/blob/HEAD/SECURITY.md) · [CHANGELOG.md](https://github.com/gokalper/iota-policy/blob/HEAD/CHANGELOG.md)

## License

Apache-2.0. Embeddability is strategic: vendor it in.
