@hammadulhaq/iris
v1.0.1
Published
Local control plane for Claude Code: inspect context, remove unused tool schemas, gate risky actions, and audit tool execution.
Maintainers
Readme
Iris for Claude Code
See what Claude carries. Kill what you don't need. Control what it can do.
Website · Documentation · GitHub
Iris is a local control plane for Claude Code. Inspect what each request carries, find unused tool schemas consuming context, control risky tool execution with Guard, and review what actually ran.
Run Iris
npx @hammadulhaq/irisThen, from your Claude Code project:
npx @hammadulhaq/iris initRestart Claude Code and open http://127.0.0.1:8787. Full requirements and details are in Install below.
Iris is a local proxy and policy layer for Claude Code. It binds to 127.0.0.1 and sits between Claude Code and Anthropic.
For each request, Iris shows what Claude Code sent: the system prompt, tool schemas, conversation history, estimated token usage, and list-price cost. It also evaluates tool calls before execution and records the decision.
Iris makes no model calls of its own. Guard decisions are deterministic, and the runtime uses Node's standard library.
Claude Code → Iris :8787 → api.anthropic.com
│
├── Context inspect what each request carries
├── Optimize find tool schemas consuming unused context
├── Spend measured cost, per project and across all
├── Guard allow, ask, or deny before execution
└── Recorder record actions and policy decisionsContext cost
Claude Code sends tool definitions with each request so the model has the schemas available when deciding which tools to call.
That means an enabled tool can consume context even when the conversation never uses it.
On one setup, measured with Claude Code's /context command:
BEFORE AFTER
System prompt 2.4k System prompt 2.4k
System tools 25.6k System tools 3.2k
──────────────────── ────────────────────
Baseline 28.0k Baseline 5.6k
↓ 22.4k tokens per turnBoth captures used the same project and the same eight-message conversation. The available tool schemas were the only change.
The 22.4k reduction came from that environment. Your numbers depend on the tools and MCP servers you have enabled and how many of them are useful to your workflow.
Iris measures the request you are actually sending so you can make that decision from your own data.

The band at the top represents a live turn to scale. The system prompt and tool schemas make up the fixed prefix that Claude Code sends again with each message.
Install
Requires Node 18+, Claude Code, and an Anthropic account that Claude Code can already use.
npx @hammadulhaq/iris # terminal 1: keep Iris running
npx @hammadulhaq/iris init # terminal 2: run from a project with .claude/Restart Claude Code, then open:
http://127.0.0.1:8787iris init configures the project to send Anthropic traffic through the local Iris instance by setting ANTHROPIC_BASE_URL.
Keep Iris running while using Claude Code in that project.
One Iris instance serves one project. Starting Iris from another project selects an available port and updates that project's configuration.
How it works
Iris uses two integration points already provided by Claude Code.
Request proxy
Every POST /v1/messages reaches Iris before being forwarded upstream.
Iris separates the request into system blocks, tools[], and messages[] using analyzer.mjs. Tool definitions are measured in schemas.mjs, and stored captures pass through redact.mjs before being written to disk.
The request is forwarded through proxy.mjs.
Initial token estimates use chars/4. Once Anthropic returns measured input usage, calibration.mjs uses that value to correct the estimate. Cost calculations then follow measured usage more closely.
Tool hooks
iris init installs iris hook as Claude Code PreToolUse and PostToolUse hooks.
PreToolUse runs before execution and returns the permission decision.
Claude wants to run:
Bash("aws s3 rm s3://acme-prod-assets --recursive")
│
▼
normalizeEffect()
│
│ {
│ effect: "delete",
│ service: "aws",
│ environment: "production",
│ reversible: false
│ }
▼
evaluate()
│
▼
{
permissionDecision: "deny",
reason: "hard-deny.production"
}The result is computed from the tool call, the project root, and the authority envelope stored on disk.
If the envelope is missing or unreadable, Guard falls back to ASK.
Optimize
Tool schemas are often the largest part of the request that can be reduced directly.
Optimize lists the definitions currently being sent along with their token weight and observed call count. Changes are staged for review before they are published.

Publishing adds a bare tool name to Claude Code's permissions.deny through permissions.mjs.
| Deny rule | Removes schema from context | Blocks execution |
| --------------------------------- | --------------------------- | ---------------- |
| "NotebookEdit" | yes | yes |
| { "tool": "Bash", "path": "…" } | no | yes |
Claude Code removes a tool schema from the request when the deny entry uses the bare tool name. Optimize therefore publishes bare names when trimming context.
There are two timing details to keep in mind.
Claude Code applies the updated deny list when a new session starts. An existing cached prefix can also continue affecting billing until its cache entry expires.
Core tools require an explicit unlock before Optimize can disable them. Removing tools such as Read, Edit, or Bash can prevent an agent from doing normal project work. Use Guard when the goal is to restrict what those tools may do.
Billing modes
Claude Code meters the same tokens four different ways. Which one applies decides whether a dollar figure on the dashboard is your bill, a ceiling, or the wrong rate card entirely.
Iris classifies the mode from the shape of the credential on the wire — never the credential itself. No token, and no fragment of one, is read, stored, or exposed.
| Mode | Detected from | Cache TTL | A $ figure is |
| ---------------------------------------- | ------------------------- | --------- | ---------------------------------------- |
| Subscription seat (Pro/Max/Team/Ent.) | Authorization: Bearer | 1 hour | not a bill — the API-rate equivalent |
| Usage credits | not on the wire — you set it | 5 min | literal, per token at list rates |
| Console API key | x-api-key | 5 min | literal, before contracted discounts |
| Cloud provider (Bedrock/Vertex/Foundry) | upstream host | 5 min | withheld — partner rates are not carried |
On a subscription seat nobody is billed per token, so Optimize leads with tokens per turn and the share of your metered usage, and brackets the dollars as what the traffic would cost once you pass your allowance. On an API key the dollars lead. On a cloud provider the money columns are dropped rather than filled from a rate card that does not apply.
Nothing on the wire distinguishes a seat inside its allowance from one already drawing on usage credits, so that stays a user choice in the header. Selecting it also shortens the cache lifetime Iris assumes.
Iris cannot see your remaining allowance — seat quotas are not published as token counts. Run
/usage in Claude Code for that. Every plan-relative figure in Iris is a share of your own measured
usage, which needs no quota.
Guard
Guard converts each tool call into a structured effect using effects.mjs, then evaluates that effect against an authority envelope stored at:
~/.iris/projects/<id>/sessions/authority.jsonThe classifier looks at the operation represented by the call instead of treating individual words as policy signals.
For example, the word production appearing inside harmless text does not automatically turn the call into a production operation.
The model may propose an authority envelope. Changes that widen an existing envelope are rejected.

Default-envelope examples:
| Tool call | Decision | Rule |
| --------------------------------------------- | -------- | ---------------------------------- |
| Read src/guard/policy.mjs | ALLOW | explicit-allow.in-scope |
| npm test | ALLOW | explicit-allow.in-scope |
| rm -rf ./build | ASK | high-consequence.destructive |
| npx unknown-cli --wipe | ASK | high-consequence.unknown |
| git push origin main | ASK | high-consequence.external-writes |
| Write ~/.ssh/config | DENY | scope.filesystem |
| aws s3 rm s3://acme-prod-assets --recursive | DENY | hard-deny.production |
git push resolves to ASK because the command represents an external write that requires approval.
npm test is allowed as normal in-scope execution. Treating every npm command as package installation would add approval prompts to ordinary test runs.
Path containment is resolved against the project root. Calls that cannot be classified safely resolve to ASK or a stricter decision. Every decision is written to the action history.
Execution boundary
Guard evaluates intent before a tool runs. It does not isolate the process that executes an allowed command.
Shell behavior can be difficult to classify completely from the command alone, and Guard's prose-stripping logic is a heuristic rather than a full shell parser.
For projects where Claude Code can run arbitrary commands, consider running Claude Code and Iris inside a Dev Container. This keeps execution inside a container and limits access to host files and processes outside the mounted workspace.
The repository remains writable from inside the container. Explicit mounts, credentials placed inside the environment, Docker socket access, and available network connections remain part of the security boundary.
See the Dev Containers guide for the threat model, recommended setup, persistent Claude and Iris state, credential handling, and network restrictions.
Running from a checkout
git clone https://github.com/csehammad/iris-control-plane.git
cd iris-control-plane
npm start # proxy + UI on 127.0.0.1:8787
npm test # 548 assertions across 11 suitesIris has no runtime or development package dependencies, so there is no install step after cloning.
The test suite uses a local stub upstream. It requires no API key and makes no outbound calls.
Project layout
bin/iris.mjs CLI: start, init, hook
src/runtime/ proxy, HTTP server, sessions, SSE events
src/context/ request analysis, schema sizing, calibration, exact token counting
src/guard/ effect normalization, policy ladder, authority, trajectory
src/adapters/claude/ Claude Code settings, hooks, permissions
src/billing/ usage extraction, list pricing, cache accounting
src/security/ secret redaction, credential patterns
src/forensic/ action ledger, correlation, timeline, export
ui/ dashboard, in-app guide
tests/ 10 suites, run with `node tests/run.mjs`The adapter boundary lives in contract.mjs.
Claude Code is currently the implemented host. IRIS_ADAPTER selects the adapter.
Configuration
These are the main settings. The documentation covers the rest.
| Variable | Default | Effect |
| ------------------- | --------- | ------------------------------------------------------------------------------------------ |
| PROXY_PORT | 8787 | Bind port. An explicitly configured port stays fixed on conflict |
| IRIS_HOME | ~/.iris | Storage for envelopes, decisions, and exports |
| IRIS_AUTOWIRE | on | Keeps ANTHROPIC_BASE_URL pointed at the active Iris port. Set =0 to manage it yourself; the dashboard still reports a mismatch |
| PROXY_REDACT | on | Scrubs detected secrets before captures are written to disk |
| PROXY_REDACT_WIRE | off | Redacts outbound traffic as well and rehydrates the streamed response |
| IRIS_UI_TOKEN | unset | Requires X-Iris-Token on mutating UI routes |
| IRIS_COUNT_TOKENS | on | Counts the prefix exactly via count_tokens. Free, cached per prefix. =0 to disable |
Project-local captures are stored in:
.claude/proxy-logs/
.claude/history-index.json
.claude/action-log.jsonAuthority envelopes and decisions are stored under:
~/.iris/projects/<id>/iris init adds the project-local Iris data paths to .gitignore.
Documentation
| Guide | Covers | | --------------------------------------------------------------------- | ----------------------------------------------------------------------- | | Quickstart | Installation, first session, and first trim | | Context | Request composition, token measurement, calibration, and billing modes | | Optimize | Reviewing tool schemas and publishing trims | | Guard | Authority envelopes, effect classification, and policy decisions | | Security | Threat model, redaction, credentials, and storage | | Troubleshooting | Ports, hooks, configuration, and recovering disabled tools | | The Token Tax | Method behind the 28.0k → 5.6k context measurement |
Contributing
Issues and pull requests are welcome.
Open an issue for bugs, proposals, or implementation questions.
Guard recognizers and adapters for additional agent hosts are useful areas for contributions.
Before opening a pull request:
npm testChanges to Guard behavior should include a corresponding case in tests/guard.test.mjs.
Cost figures use published list rates for models Iris recognizes. Models without a known price are left unpriced instead of being displayed as $0.00. Iris detects your billing mode, which determines whether those figures represent a bill at all, but it does not know your rate — promotional pricing, negotiated discounts and seat allowances are not reflected.
Token counts are calibrated estimates and can differ from final billing totals. Use Claude Console for invoice values.
Acknowledgements
The initial investigation into Claude Code's context and tool-schema overhead was inspired in part by Matt Pocock's How To Kill The Bloat In Claude Code's System Prompt.
Iris grew from that starting point into a tool for inspecting what Claude Code sends, removing unused tool schemas, controlling what the agent is allowed to do, and recording what actually ran.
License
MIT.
Iris is an independent open-source project with no affiliation or endorsement from Anthropic.
