<p align="center">
<img src="assets/ironshell_banner4.jpg" alt="IronShell" width="100%">
</p>
<p align="center">
<a href="https://github.com/nervosys/IronShell/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/nervosys/IronShell/ci.yml?style=flat-square&logo=github&label=CI" alt="CI"></a>
<a href="https://crates.io/crates/iron-shell"><img src="https://img.shields.io/crates/v/iron-shell.svg?style=flat-square&logo=rust&color=orange" alt="Crates.io"></a>
<a href="https://crates.io/crates/iron-shell"><img src="https://img.shields.io/crates/d/iron-shell?style=flat-square&color=orange&label=downloads" alt="Downloads"></a>
<a href="https://marketplace.visualstudio.com/items?itemName=admercs.ironshell"><img src="https://img.shields.io/visual-studio-marketplace/v/admercs.ironshell?style=flat-square&logo=visual-studio-code&label=VS%20Code" alt="VS Code"></a>
<a href="https://github.com/nervosys/IronShell/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg?style=flat-square" alt="License"></a>
<a href="https://github.com/nervosys/IronShell/stargazers"><img src="https://img.shields.io/github/stars/nervosys/IronShell?style=flat-square&color=yellow" alt="Stars"></a>
</p>
<h3 align="center">The shell for AI agents. Typed pipelines. Multi-modal. Protocol-native.</h3>
<p align="center">
<a href="https://nervosys.github.io/IronShell/book/">Documentation</a> •
<a href="#getting-started">Getting Started</a> •
<a href="#syntax">Syntax</a> •
<a href="#agentic-benchmark">Benchmark</a> •
<a href="#ai-agents">AI Agents</a> •
<a href="#reliable-file-editing-for-llms">File Editing</a> •
<a href="#shell-migration-transpilers">Migration</a> •
<a href="#protocols">Protocols</a> •
<a href="#external-integrations">External Integrations</a> •
<a href="#ai-context--discoverability">AI Context</a> •
<a href="docs/TUI_GUIDE.md">TUI Guide</a>
</p>
---
## Agentic Benchmark
*Measured, not asserted.* The standalone [`agentic-eval`](crates/agentic-eval) crate
scores any program for agentic use across the four axes that determine an agent's cost
and trust — **token efficiency, determinism, reliability, safety** — and rolls them
into a composite on a **0–10 scale**. Real GPT-4 cl100k tokenizer; reproduce with
`cargo run --example shell_agentic_eval --features real-tokens`.
| **IronShell** | 10.0 | 10.0 | 10.0 | 7.0 | 10.0 | 10.0 | 10.0 | **9.6** |
| Nushell | 7.1 | 6.4 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | **2.3** |
| PowerShell | 5.9 | 6.4 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | **2.2** |
| Bash / Zsh / Fish | 3.6 | 2.6 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | **1.4** |
Sub-metrics: **tok** total-token efficiency · **scal** output per-item scaling · **det**
determinism · **rel** pass/actionable · **err** error actionability · **saf** blast-radius
gated · **rev** reversibility. Each axis is the mean of its sub-metrics; the composite is
the mean of the four axes. `tok`/`scal`/`saf` are measured for every shell; `det`/`rel`/
`err`/`rev` are measured on IronShell's engine and a structural capability for the rest
(traditional shells have no byte-stable output, branchable errors, or rollback).
IronShell leads on token cost (**~2.8× fewer than POSIX shells**, **2.4–3× vs
PowerShell's default JSON**) and is the only shell with deterministic typed output,
machine-branchable errors, effect-gated safety, and transactional rollback. Full
methodology, per-task token tables, and capability matrices are in
[Benchmarks](#benchmarks-vs-bash--zsh--fish--nushell--powershell) below.
---
## Getting Started
### Install
```bash
cargo install iron-shell # from crates.io
```
<details>
<summary>Other ways</summary>
```bash
# Homebrew
# (the formula lives in this repository, so tap it by URL)
brew tap nervosys/ironshell https://github.com/nervosys/IronShell
brew install nervosys/ironshell/ironshell
# From source
git clone https://github.com/nervosys/IronShell && cd IronShell
cargo install --path . --bin ish
# Prebuilt binaries for Linux, macOS and Windows
# https://github.com/nervosys/IronShell/releases
```
Editor support: `code --install-extension admercs.ironshell`, or
`cursor --install-extension admercs.ironshell` for Cursor, VSCodium and other
forks.
</details>
### Run it
```bash
ish # REPL
ish script.ish # run a script
ish -c 'expr' # evaluate one expression
ish tui # full-screen interactive mode
```
IronShell was called AetherShell before 14.0.0, and this repository was
`nervosys/AetherShell` (old links redirect). The crate is `iron-shell`, the Python
SDK `ironshell`. The `ae` command is still installed as an alias of `ish`. Scripts
must be renamed from `.ae` to `.ish`, and variables from `AETHER_*` to
`IRONSHELL_*`; the old names are no longer read. What else changed is in
[CHANGELOG.md](CHANGELOG.md).
Three flags matter more than the rest, and they are what make this shell
different from the one you are using now:
```bash
ish --agent # default-deny: destructive effects need approval
ish --workspace ./sbx # confine writes and deletes to one directory
ish --deterministic # byte-stable canonical JSON, for diffs and snapshots
```
### Your first five minutes
```ironshell
1 + 2 # 3
ls() | len # 199
pwd() # "/home/you"
```
Two things to know straight away.
**A builtin used as a value needs its parentheses.** `ls()` returns an array of
records; bare `ls` returns null.
```ironshell
```
**Pipelines carry typed values, not text.** There is nothing to parse, so there
is nothing to parse wrongly:
```ironshell
### What it is for
<table>
<tr><td width="33%">
**Agents that touch a real machine**
An effect gate, a workspace jail and a keyed audit chain, so an agent can be
given a directory rather than a machine.
</td><td width="33%">
**Data work without text parsing**
`ls`, `http_get` and `from_json` return records and arrays. No `awk`, no
column-counting, no quoting bugs.
</td><td width="33%">
**Scripts that behave the same everywhere**
One language on Linux, macOS and Windows, with transpilers for the Bash, Zsh and
PowerShell you already have.
</td></tr>
</table>
### Examples
**Find the five largest files under `src/`**
```ironshell
**Fetch JSON and pull one field out**
```ironshell
http_get("https://api.github.com/repos/nervosys/IronShell")
| from_json
| keys
```
`http_get` returns `{body, headers, status, url}` — the status is a number you
can branch on, not a line you have to grep.
**Write a file and read it back**
```ironshell
file_write("notes.txt", "hello")
# {bytes: 5, lines: 1, path: "notes.txt", success: true}
cat("notes.txt") # "hello"
```
**Run an agent against your own shell, safely**
```bash
export IRONSHELL_AI=ollama # or openai, compat, irongate
export AGENT_ALLOW_CMDS=ls,cat,grep # default-deny until you say otherwise
ish --agent --workspace ./sandbox
```
```ironshell
agent("Find every TODO under src/", ["ls", "grep", "cat"])
```
**Orchestrate several steps**
```ironshell
let t = workflow_pipeline("audit", [
{id: "scan", run: "grep", args: ["TODO", "src"], output: "$.hits"},
{id: "count", run: "len", input: "$.hits"}
])
workflow_execute(workflow_create(t, {input: null}))
```
**Keep a tamper-evident record of what ran**
```bash
export IRONSHELL_AUDIT_LOG=./audit.log
export IRONSHELL_AUDIT_KEY_FILE=./audit.key
ish --agent --workspace ./sandbox script.ish
```
Every operation is appended to an HMAC-SHA256 chain. A rewritten entry fails
verification rather than passing quietly. Agent mode refuses writes to the log
and the key file, but not reads: it reads anywhere the account running it can.
Anyone holding the key can sign a forged chain, so for a chain an agent cannot
forge, make the key file unreadable by the account the agent runs as.
---
## Syntax
Every snippet below is executable as written.
### Values
```ironshell
42 # Int
3.14 # Float
"text" # String
true # Bool
[1, 2, 3] # Array
{name: "ae", version: 12} # Record
```
### Bindings
```ironshell
let x = 42 # immutable
let mut n = 1 # mutable
n = n + 1 # 2
let name = "world"
"hello ${name}" # "hello world"
```
### Operators
```ironshell
1 + 2 1 - 2 2 * 3 10 / 4 # 2.5 — division is exact
10 % 3 # 1
1 == 1 1 != 2 1 < 2 2 >= 2
true && false # false
```
`&&`, `||` and `!` are the boolean operators. There is no `and` / `or`.
### Functions
```ironshell
let double = fn(x) => x * 2
double(21) # 42
let add = fn(a, b) => a + b
add(2, 3) # 5
```
### Pipelines
The pipe passes a value, so each stage receives whatever the last one produced.
```ironshell
[1, 2, 3, 4] | reduce(fn(a, b) => a + b, 0) # 10
[3, 1, 2] | sort # [1, 2, 3]
[1, 2, 3] | sum # 6
[1, 2, 3] | len # 3
[1, 2, 3] | reverse # [3, 2, 1]
[1, 2, 2, 3] | unique # [1, 2, 3]
[1, 2, 3, 4] | take(3) # [1, 2, 3]
[1, 2, 3] | slice(1, 3) # [2, 3]
[[1, 2], [3]] | flatten # [1, 2, 3]
[1, 2, 3] | any(fn(x) => x > 2) # true
[1, 2, 3] | all(fn(x) => x > 0) # true
range(1, 5) # [1, 2, 3, 4]
```
### Strings
```ironshell
"hello" | contains("ell") # true
"a-b" | replace("-", "+") # "a+b"
"a,b,c" | split(",") # ["a", "b", "c"]
["a", "b"] | join(", ") # "a, b"
```
### Records
```ironshell
let p = {name: "ada", age: 36}
p.name # "ada"
### Control flow
```ironshell
if 1 > 0 { "yes" } else { "no" } # "yes"
match n {
0 => "zero",
n if n > 0 => "positive",
_ => "negative"
}
let result = try { cat("missing.txt") } catch { "fallback" }
```
### Modules
Related builtins are also reachable by a dotted name:
```ironshell
file.exists("config.toml") # {exists: true, is_file: true, is_dir: false, path: ...}
file.read("config.toml")
sys.hostname() # "heimdall"
crypto.uuid() # "62375508-c80a-4490-9824-6b6a883acc88"
str.upper("ab") # "AB"
math.sqrt(9.0) # 3.0
```
### The system
```ironshell
pwd() # "/home/you"
hostname() # "heimdall"
platform() # "linux"
sys_info() # {arch: "x86_64", family: "unix", os: "linux", ...}
exists("/etc/hostname") # {exists: true, is_file: true, ...}
```
### JSON
```ironshell
```
`help()` lists every builtin. `ontology_manifest()` gives an agent the same
thing as a compact, categorised index.
---
## 🤖 AI Coding Assistants
AI coding tools like **Claude Code**, **ChatGPT**, **Cursor**, **Windsurf**, and **VS Code Copilot** can leverage IronShell for **reliable, cross-platform OS operations**.
### The Problem
When AI assistants need to perform system operations, they face platform fragmentation:
```bash
# Different commands per platform
ls -la # Linux/macOS
dir # Windows cmd
Get-ChildItem # PowerShell
# Different escaping rules, encoding issues, error handling...
```
This forces AI tools to detect the OS, generate platform-specific commands, and handle edge cases—leading to errors and inconsistent behavior.
### The Solution: IronShell as Universal Runtime
```ish
# Same command works everywhere: Windows, macOS, Linux
ls("./src") # => [{name, size, modified, ...}]
file.read("config.json") # => String content
file.write("output.txt", data) # => {success: true, bytes: 42}
sys.hostname() # => "my-machine"
### Benefits for AI Coding Tools
| **Cross-platform** | Generate 3+ variants | Single command |
| **File editing** | Escape hell (`sed`, heredocs) | `file.replace()`, `file.patch()` |
| **Structured output** | Parse text with regex | Native records/arrays |
| **Error handling** | Exit codes only | `{success, error, details}` |
| **Safe execution** | Shell injection risks | Typed parameters |
| **Batch operations** | Script multiple commands | Atomic operations |
### Integration Example
An AI assistant can execute IronShell commands directly:
```ish
# AI discovers system state
sys.cpu_info() # => {cores: 8, model: "Apple M2"}
sys.mem_info() # => {total: 16384, used: 8192}
net.interfaces() # => [{name: "eth0", ip: "192.168.1.5", ...}]
# AI modifies files reliably (no escaping issues)
file.replace("src/config.rs",
'const DEBUG: bool = false',
'const DEBUG: bool = true')
# AI performs batch operations atomically
file.patch("Cargo.toml", [
{find: 'version = "0.2.0"', replace: 'version = "0.3.0"'},
{find: 'edition = "2018"', replace: 'edition = "2021"'}
])
# => {success: true, patches_applied: 2}
# AI creates complex pipelines
ls("./src")
# => [{file: "main.rs", lines: 142}, ...]
```
### Tool Discovery
AI assistants can discover available operations:
```ish
mcp.tools() # List all 130+ MCP-compatible tools
help() # list every builtin (takes no arguments)
file # => {read, write, exists, copy, move, patch, ...}
```
This enables AI tools to understand what operations are available and use them correctly—without hardcoding platform-specific knowledge.
---
## AI Agents
```ish
# Simple query
ai("Explain recursion in one sentence")
# With context: put the file text into the prompt
readme = file.read("README.md")
ai("Summarize this file: ${readme}")
# Multi-modal (images, audio, video)
ai("What's in this image?", {images: ["photo.jpg"]})
ai("Transcribe this", {audio: ["meeting.mp3"]})
# Autonomous agent with tool access
agent("Find all TODOs in the codebase", ["file.read", "grep", "ls"])
# Agent with config
agent({
goal: "Fix code style violations",
tools: ["file.read", "file.write", "grep"],
max_steps: 20
})
# Multi-agent swarm
swarm({
coordinator: "Perform security audit",
agents: [
{role: "scanner", goal: "Find vulnerable deps"},
{role: "reviewer", goal: "Check for injections"},
{role: "reporter", goal: "Generate report"}
],
tools: ["file.read", "grep", "http.get"]
})
```
---
## Agentic-First: token economy, safety, transactions, self-healing
IronShell is optimized end-to-end for AI agents. Four things matter to an agent
that a human shell ignores: how many **tokens** the output costs, whether an action
is **safe**, whether a mistake can be **undone**, and whether a failed call tells the
agent enough to **fix it**.
### Compact, lossless output (AECON)
Set `IRONSHELL_MODE=agent` and every tabular result renders as **AECON** — a
header-once format that emits each column name once, factors constant columns into
a single `@const` line, dictionary-encodes string columns with repeats
(`@dict`), delta-encodes large slowly-varying integers (`@delta`), factors a
shared leading prefix out of path/URI/id columns into one `@prefix` line, factors a
shared trailing run (extensions, domain tails) into `@suffix`, elides a cell
identical to the one above it in run-structured columns (`@same`), and expands
record-valued columns into dotted columns (`@nest`) so every pass above reaches the
leaves instead of stopping at a JSON blob. On
realistic tabular results that's **~2.8× fewer output tokens than POSIX shells**. Versus
PowerShell the ratio depends on which output an agent parses: ~1.4× vs its display
`Format-Table` (not reliably parseable), ~1.6× vs compact `ConvertTo-Json -Compress`,
and **~2.4–3× vs the default `ConvertTo-Json`** an agent gets without flags (measured
with the real GPT-4 cl100k tokenizer; the gap widens with row count).
```sh
IRONSHELL_MODE=agent ish -c '[{name:"a",size:1,kind:"x"},{name:"b",size:2,kind:"x"}]'
# name size
# @const kind=x
# a 1
# b 2
```
`@prefix` and `@suffix` compose on the same column — the suffix is searched in the
residue the prefix leaves, so the two runs can never overlap even where they consume
a value entirely. Which encoding a column gets is decided by **exact character cost**,
not by heuristics: raw, `@dict` and `@prefix`/`@suffix` are each costed as they will
actually be emitted (including the `@same` elision that follows), and the cheapest
wins. A table with nothing to factor emits no metadata lines at all. On a 30-row
grouped listing with paths, those additions take the result from **265 to 153
tokens — 42% fewer** (real cl100k, same rows, encoder before vs. after:
`cargo test --test aecon_factoring --features real-tokens -- --nocapture`).
`@nest` is the larger win, because a record-valued cell previously serialized as whole
JSON on *every row*, keys included — exactly the cost this format exists to remove, and
unreachable by any of the column passes. Expanding it into `meta.region` / `artifact.path`
subjects each leaf to `@const`, `@prefix` and the rest. On a 30-row API-shaped payload
that is **583 → 258 tokens, a 2.26× reduction**
(`cargo test --test aecon_nesting --features real-tokens -- --nocapture`). Only records
are expanded, so an array cell stays a single atom; a top-level key is expanded only when
nothing already occupies its dotted namespace.
It's **lossless and reversible**: `aecon_decode` reconstructs the original rows
(with on-demand `@type` tags so numeric-looking strings and integral floats
round-trip exactly). The same default applies on the HTTP Agent API and MCP server.
Companion builtins: `pick` (project fields before rendering), `budget` (token-bounded
paging), `digest` (constant-token structural summary), `canonical` (deterministic
JSON), `tokens` (cl100k estimate), and `ontology_manifest`/`ontology_describe`
(progressive discovery). `--deterministic` renders byte-stable canonical JSON for
snapshot tests, caching, and diffs.
### A real safety model
An **effect taxonomy** (Pure → ReadLocal → WriteLocal → Destructive → Process →
Network → Exec → Privileged) drives a capability → policy → approval → audit
pipeline. Agent mode (`--agent`) default-denies dangerous effect classes behind
content-bound approval tokens; a **workspace jail** (`--workspace`) confines writes
and deletes; a **hash-chained audit log** records every effecting action tamper-evidently;
**RBAC** grants bypass approval but never the jail (and can be loaded from a config
file at startup via `IRONSHELL_RBAC_CONFIG`); **every** failure surfaces as a structured,
branchable `E_*` error — not just argument errors, but anything that fails, down to a
catch-all `E_UNKNOWN` — rendered as legible prose for humans. `safety_status()` reports
the live envelope.
Effect tags are enforced at a single `guard()` chokepoint, and the tagging itself is
checked in CI rather than trusted — by two lints that disagree on purpose. The first
(`tests/effect_coverage.rs`) reads *names*: a builtin whose name advertises a side
effect may not classify as `Pure`. It found 61 mis-tagged, among them `ssh_exec`,
`terraform_destroy`, `kubectl_delete` and every package installer (`npm_install` and
friends, which fetch remote code and run its install scripts). Nine high-blast-radius
executors are now approval-gated in agent mode.
But a name-based lint is the same reasoning that produced the mistakes — four builtins
were tagged as executing purely because they were called `*_exec`, when their bodies
execute nothing. So the second lint (`tests/effect_ratchet.rs`) reads *bodies*,
following calls four levels deep: if a function constructs a process, writes a file,
opens a socket or kills a process, it cannot be `Pure` whatever it is called. It found
306 more.
Coverage is reported, not asserted: **911 of the 1,319 names in the dispatch table carry an
explicit effect** (`tests/effect_coverage.rs` prints the current figure). Three independent
checks say nothing dangerous hides in the remaining 408 — zero act
while classified `Pure`, zero read local state while classified `Pure`, and zero could
reach an effect even when every ambiguous helper name is resolved to its worst
definition. Each check has a canary proving it can still see, because a lint that has
gone blind reports the same zero as a clean one.
What is left is **unclassified, not unsafe** — and agents can now tell the difference.
`effect_of` falls through to `Pure`, so `x-effect: pure` conflated "read and found
harmless" with "nobody looked". `ontology_describe` now carries `effect_declared:
false` on exactly the builtins where the label is a default:
```console
$ ish -c 'ontology_describe("sh")' # classified
effect: "exec"
$ ish -c 'ontology_describe("upper")' # defaulted
effect: "pure", effect_declared: false
```
**Secret hygiene** is on by default in agent mode: known secret shapes (API-key
prefixes, AWS keys, JWTs, PEM blocks, URL credentials, `key=secret` forms) are
redacted from agent output *and* the audit log, and reading a secret-named env var
returns an opaque `[REDACTED:NAME]` handle — so credentials never reach the model's
context or persist to disk (opt out with `IRONSHELL_REDACT=off` / `IRONSHELL_SECRETS=allow`).
Programs a shell starts do not get those credentials either: in agent mode a child loses
every variable whose name is secret-shaped (`*TOKEN*`, `*_API_KEY`, `*SECRET*`, `*PASSWORD*`,
`*CREDENTIAL*`, and so on, including `GITHUB_TOKEN`, `AWS_SECRET_ACCESS_KEY` and
`ANTHROPIC_API_KEY`). A tool that needs one names it in `IRONSHELL_CHILD_ENV_KEEP`, as
comma-separated exact names (`IRONSHELL_CHILD_ENV_KEEP=GH_TOKEN` for `gh`). Human mode is unchanged.
**Resource governors** (opt-in: `IRONSHELL_MAX_OPS`/`IRONSHELL_MAX_FILES`/`IRONSHELL_MAX_PROCS`/
`IRONSHELL_MAX_NET`/`IRONSHELL_TIMEOUT_MS`) bound a run's blast radius at the same `guard()`
chokepoint — a runaway agent loop is stopped with `E_BUDGET_EXCEEDED` rather than
running unbounded. **Output cap** (opt-in: `IRONSHELL_MAX_OUTPUT_BYTES=<n>`) cuts the printed
result of a one-shot `ish -c` at `n` bytes, on a UTF-8 boundary, and reports the bytes omitted
on stderr; the exit status is unchanged, and unset means no cut. It does not cut a streaming
child's stdout, which goes straight to the terminal.
**What an agent cannot change.** In agent mode the workspace root is fixed at startup,
so a `cd` moves the working directory but not the jail or the audit log. Variables that
steer the programs a shell starts (`PATH`, `HOME`, `GIT_*`, `LD_*`, `DYLD_*`, `XDG_*`,
`IRONGATE_*`, `*_PROXY`, provider keys) cannot be set or unset by the agent; whoever
launches it sets them. The jail resolves symbolic links and `..` the way the operating
system does, and on Windows a path must stay inside under both of the readings Windows
can give it. Network calls ask for approval, and once approved they still refuse
internal and metadata addresses, on every redirect hop and in IPv4-mapped forms, and
connect only to the address that was checked. These came out of the 2026-11-04 audit;
[its write-up](docs/security/SECURITY_AUDIT_2026-11-04.md) lists what was fixed and the
risks that remain.
### Transactions & checkpoints (no other shell offers this)
```sh
tx_begin()
# … file writes, appends, deletes, recursive rmdir, sqlite/kv mutations …
tx_savepoint("before-risky")
# … more work …
tx_rollback_to("before-risky") # partial rollback, transaction stays open
tx_begin() # nest: a child frame, undone independently
tx_rollback() # roll back just the child, parent stays open
```
`tx_begin`/`tx_commit`/`tx_rollback` with **named savepoints** and **full nesting**
(child commit folds into the parent; nothing is durable until the outermost commit)
cover file writes & appends, deletes, **recursive directory trees**, sqlite databases,
and the key-value store. **`plan`/`apply`** give Terraform-style declarative
destructive batches (ops: `write`/`append`/`rm`/`mkdir`/`copy`/`move`) — a reviewable
typed plan plus a content-bound approval token, applied atomically with automatic
rollback on any failure.
### Self-healing: a failed call that tells you how to fix it
Structured errors only help if failures actually carry codes, suggestions are real,
and a failed attempt leaves nothing behind for the next one.
```sh
try { file.raed("notes.md") } catch e { diagnose(e) }
# { code: "E_UNKNOWN_FIELD", retryable: true, did_you_mean: ["read"], … }
try { http_get() } catch e { diagnose(e) }
# { code: "E_BAD_ARG", retryable: true, builtin: "http_get", effect: "network",
# signature: "http_get(url: String, options?: Record) -> Value",
# example: "http_get(\"https://api.example.com/data\")", … }
try_repair("file_write(\"a.txt\", \"x\")\nno_such_builtin()")
# { ok: false, restored: 1, retryable: true,
# error: { code: "E_UNKNOWN_BUILTIN", builtin: "no_such_builtin", … } }
# ↑ the partial write was rolled back — the next attempt starts from clean state
```
- **`did_you_mean`** searches the *live* builtin table and, for dotted module paths
like `file.raed`, the record's own fields. When nothing is close it returns
**nothing** — a confident wrong guess costs a retrying agent a whole round trip.
- **`diagnose(error)`** returns only what fixing *this* call needs: code, `retryable`,
the builtin's signature and effect class. It never costs more than a full
`ontology_describe` and is about half on richly-documented builtins
(`map`: 82 vs 206 tokens).
- **`try_repair(code)`** does not invent a fix — nothing in a shell can. It makes
retrying *safe*, bracketing the attempt in a savepoint so a failure is rolled back
before the error is returned.
**Measured, not asserted** (`tests/repair_rate.rs`): a deliberately model-free
strategy — substitute the first `did_you_mean` candidate, nothing else — repairs
**8/8** plausible misspellings, with **0 uncoded failures** across a mixed corpus that
includes real runtime errors. That is a floor, not a ceiling: it is what the error
surface alone is worth before any intelligence is applied. Wrong-*argument* failures
are diagnosable but not mechanically repairable — deciding what the value should have
been is a model's job — and a test pins that down so the number cannot be over-read.
---
## Benchmarks vs Bash / Zsh / Fish / Nushell / PowerShell
Three things matter to an AI agent driving a shell: how many **tokens** the
interaction costs, how **reliable** the output is to parse, and whether actions are
**safe**. Token counts below use the real GPT-4 cl100k tokenizer
(`cargo run --example shell_bench --features real-tokens`, counting each shell's
idiomatic command + output) over *representative* outputs; for figures captured
from shells that actually ran, with the answers checked against an oracle, see
[`benches/agentic/`](benches/agentic/). Reliability and safety are capability
comparisons — the traditional shells return unstructured text and have no
effect/approval model, so there is no single-number benchmark to run there.
### Result handles: the tokens you never send
Encoding a result more cheaply has a floor. Not sending it does not. On the
agent path a result over `IRONSHELL_HANDLE_BYTES` (default 2048 rendered bytes)
stays server-side and comes back as a reference:
```
@handle h1
@shape array<record{bytes:int,id:int,path:str,status:str}>
@items 2000
@omitted 1997
The agent then composes against it — `handle("h1") | where(…) | take(5)` — and
only the answer crosses back. Measured with the real cl100k tokenizer:
| 400 | 2,975 | 113 | **26×** |
| 2,000 | 15,709 | 115 | **137×** |
The ratio grows with the payload because the summary is constant-size, so no
single multiplier describes it — which is why both rows are shown. The saving is
determined entirely by how much of a result you discard; across a mixed task
corpus it ranges from **2× to 7000×** (`cargo test --test agent_task_cost --
--nocapture`). Nothing is lost: `handle(id)` returns exactly what was computed,
and the preview states how many items it omits. `IRONSHELL_HANDLE_BYTES=0` restores
whole results. Human output is untouched.
### Reversible sessions: undo instead of ask
Approval is expensive — every prompt is a round-trip. A write that can be undone
does not need to be prevented. Before any `WriteLocal` or `Destructive` call the
prior contents of the files it may touch are captured; `undo(n)` puts them back
and `journal()` shows what was recorded.
```
$ ish --agent -c 'file_write("notes.txt", "clobbered")'
$ ish --agent -c 'undo()'
{complete=true failed=0 removed=0 restored=1 skipped=0 …}
```
**`undo` reports what it could not reverse.** A network call cannot be unsent, a
directory tree is not captured, an oversized file is not held — each is recorded
as an explicit irreversible entry and `complete: false` comes back while any
remain. A partial undo that claimed success would be worse than no undo, because
it turns a recoverable situation into one you believe you have already recovered
from. Agent-mode only (`IRONSHELL_JOURNAL=on|off` forces it); handles and the
journal both survive across processes, which matters because the SDK runs
`ish -c …` per call.
### Return shapes: compose instead of discover
The agent surface describes inputs (`json_schema`) and danger (`x-effect`). It
now also describes the **result**, so an agent meeting one of ~1,300 unfamiliar
builtins does not have to run it to learn the shape and then write the pipeline
it wanted:
```
ls x-returns array<record{ext:str,is_dir:bool,modified:int,name:str,path:str,size:int}>
x-returns-examples {"ext": ".rs"}
first x-returns T (T = element type of the first argument)
```
Only **proven** shapes are advertised: each is reproduced by calling the builtin,
and must agree across probes with different input types. Combinators whose result
tracks their argument are described with `T` rather than a concrete type, and
`sum` is deliberately absent — `int` for integers, `float` for floats is a
promotion rule this notation cannot honestly express.
The examples exist because a type is not a format: `ext:str` is true, and
`where(fn(f) => f.ext == "rs")` still matches nothing, because the value is
`".rs"`. That filter does not error — it returns an empty set, which is the worst
failure available to an agent, since an empty result is a plausible answer.
### Token efficiency (real cl100k BPE over representative output)
Per-task **output** tokens — what the agent must read back, each shell's *idiomatic
display* output (reliably-parseable forms are compared in the scale table and
scoreboard below).
The tokenizer is exact; the outputs in this first table are **representative**,
written by hand to match each shell's usual format rather than captured from a
run. Executed measurements — real processes, real bytes, answers checked against
an oracle — are in [`benches/agentic/`](benches/agentic/), and the argument built
on them is [docs/TYPED_SHELL_RESPONSE.md](docs/TYPED_SHELL_RESPONSE.md). On eight
ordinary repository operations there, IronShell's agent mode costs **3.0× fewer
total tokens than bash** (502 vs 1,489) and **4.2× fewer output tokens** (339 vs
1,421); on a ten-query corpus whose every answer is a scalar, its default syntax
comes **sixth of eight** and its token-minimised syntax (`ish -a`) **second**,
behind SQLite. Both numbers are worth having, and the second is why
this table is not the whole story.
| list files (name, size) | **19** | 78 | 78 | 78 | 108 | 29 |
| processes (pid, name, cpu) | **26** | 138 | 138 | 138 | 124 | 41 |
| json field (scalar) | **2** | 2 | 2 | 2 | 2 | 2 |
| disk usage (mount, free) | **23** | 85 | 85 | 85 | 107 | 33 |
Totals (command + output) over the 4 tasks:
| **IronShell** | 48 | 70 | **118** | 1.00× |
| Bash / Zsh / Fish | 28 | 303 | 331 | 2.81× |
| Nushell | 34 | 341 | 375 | 3.18× |
| PowerShell | 55 | 105 | 160 | 1.36× |
**The honest PowerShell spread** — IronShell's edge over PowerShell depends entirely
on *which output an agent parses*, and (for JSON) grows with row count because AECON
emits each column name once while JSON repeats every key on every row. A plain
`name,size` listing, every form token-counted with real cl100k
(`cargo run --example shell_agentic_eval --features real-tokens`):
| 3 | 35 (1.00×) | 48 (1.37×) | 58 (1.66×) | 85 (2.43×) |
| 25 | 189 (1.00×) | 224 (1.19×) | 300 (1.59×) | 547 (2.89×) |
| 100 | 714 (1.00×) | 824 (1.15×) | 1125 (1.58×) | 2122 (2.97×) |
\* `Format-Table` is display-only (variable widths, truncation, culture-dependent) and
**not reliably parseable** — an agent that must parse the result uses `ConvertTo-Json`.
With richer, constant-heavy columns, AECON's `@const`/`@dict`/`@delta`/`@prefix`
factoring widens the gap further (a 50-row listing: 470 vs 1447 for default
`ConvertTo-Json` = 3.1×; a path-heavy listing is 44–69% cheaper via `@prefix` alone).
**Takeaway:** ~**2.8× fewer tokens than the POSIX shells**. Versus PowerShell the honest
range is **~1.4× (display `Format-Table`, not reliably parseable) → ~1.6× (compact
`-Compress` JSON) → ~2.4–3× (default `ConvertTo-Json`, the idiomatic form)**. Scalars
are at parity; the savings live in structured output, where agents spend most of their
tokens.
**Verified with live capture.** The tables above use representative idiomatic
output. Re-running against the *actual* shells installed on a test machine —
Bash / Zsh 5.9 / Fish 4.0.2 via WSL Debian, Nushell 0.113, PowerShell 7 — each
listing the **same** 5-file directory and token-counted with real cl100k, confirms
the pattern (and real `ls -l` is even more verbose than the representative output):
| **IronShell (AECON)** | 33 | 1.00× |
| PowerShell (`Format-Table Name,Length`) | 44 | 1.33× |
| Bash / Zsh / Fish (`ls -l`) | 128 | 3.88× |
| Nushell (boxed table) | 231 | 7.00× |
### Reliability (capability comparison)
| Typed structured output (not text to re-parse) | ✗ | ✗ | ✗ | ✓ | ~ | ✓ |
| Deterministic output (no locale/width/ANSI variance) | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Byte-stable output for diffs / caching | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (`--deterministic`) |
| Structured, branchable errors | ✗ | ✗ | ✗ | ~ | ~ | ✓ (`E_*` codes on **every** failure + `hint` + `retryable`) |
| Suggests the real name on a typo | ✗ | ~ | ~ | ~ | ~ | ✓ (`did_you_mean`, builtins *and* module paths) |
| Machine-readable repair context for a failure | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (`diagnose`) |
| Lossless, reversible compact format | ✗ | ✗ | ✗ | ✗ | ~ | ✓ (`aecon_decode`) |
| Multi-line edits without quoting/escaping hazards | ✗ | ✗ | ✗ | ~ | ~ | ✓ |
An agent parsing `ls -l` text breaks on spaces in filenames, locale date formats,
terminal width, and tool-version drift. AECON is byte-identical for identical values
across OS/locale; `canonical` gives byte-stable JSON for snapshot tests and caches.
### Safety (capability comparison)
| Effect taxonomy (pure → privileged) gating | ✗ | ✗ | ✗ | ✗ | ~ | ✓ |
| Default-deny dangerous ops behind approval | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (`--agent`) |
| Workspace jail confining writes/deletes | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (`--workspace`) |
| Tamper-evident (hash-chained) audit log | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Secret redaction (output + audit) & env handle gating | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (agent mode) |
| Resource governors (ops/files/procs/net/wall-clock → `E_BUDGET_EXCEEDED`) | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (agent mode) |
| RBAC over effect classes | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Filesystem transactions / rollback | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (`tx_*`, savepoints, nesting) |
| Plan → approve → atomic apply | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (`plan`/`apply`) |
| Failed attempt rolled back before the retry | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (`try_repair`) |
| Effect tagging enforced by a CI lint | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ (by name *and* by body evidence) |
No traditional shell offers transactional rollback or effect-gated approval — a
mistaken `rm -rf` is irreversible. In IronShell a file-effecting batch can be
planned, approved, attempted, and **rolled back atomically**.
> Token numbers reproduce with `cargo run --example shell_bench --features real-tokens`.
> Token efficiency is measured (real cl100k BPE over each shell's idiomatic output);
> reliability/safety rows are capability comparisons. `~` = partial (e.g. PowerShell
> objects are typed but its display formatting is lossy to parse).
### Four-axis scoreboard (token & safety detail)
The [composite scorecard](#agentic-benchmark) at the top rolls the four axes into one
number; here is the token-and-safety detail it builds on — each shell scored on its
**reliably-parseable** output (`cargo run --example shell_agentic_eval --features real-tokens`):
| **IronShell** | **118** | **1.00×** | **A** |
| Nushell | 166 | 1.41× | F |
| PowerShell | 199 | 1.69× | F |
| Bash / Zsh / Fish | 331 | 2.81× | F |
This scoreboard scores each shell on its **reliably-parseable** output — Nushell
`to json -r`, PowerShell `ConvertTo-Json -Compress`, raw text for the POSIX shells
(which have no structured mode), AECON for IronShell. That's why Nushell and
PowerShell land closer here than in the display-output per-task table above (their
pretty tables are compact but not reliably parseable). Against PowerShell's *default*
`ConvertTo-Json` the per-row ratio is **2.4–3×** (see the spread table above); the
≥2× threshold holds there, not against the hand-compacted `-Compress` form.
*Tokens* = command + output, summed over the 4 tasks (real cl100k BPE via
`agentic_eval::evaluate_with` + IronShell's tokenizer). *Safety grade* is
`agentic_eval::assess_safety` of a read/write/delete/exec task: IronShell's agent
mode gates the dangerous classes (blast radius bounded → **A**); a traditional shell
applies no agent policy, so everything runs ungated (→ **F**).
The same harness verifies the other two axes directly on IronShell's engine:
**determinism** — `assess_determinism` over the canonical renderer returns
*deterministic (1 distinct / 8 runs)*; **reliability** — `assess_reliability` over
representative programs returns *pass 60% / actionable 80%* (a wrong-typed arg is a
structured, catchable `E_BAD_ARG`, not a dead end). Traditional shells lack both by
construction (locale/width/ANSI-variant text; unstructured errors).
#### Composite scorecard & context metrics
The **0–10 composite scorecard is at the [top of this README](#agentic-benchmark)**.
Rubric: each axis is the mean of its sub-metrics and the composite the mean of the four
axes; `tok`/`scal`/`saf` are measured for every shell, `det`/`rel`/`err`/`rev` on
IronShell's engine and a structural capability for the rest. IronShell's `rel` is
**7.0, not 10** — the corpus includes intentional-failure programs, so it's held honest
rather than rounded up.
Two further v0.6 metrics are reported as context (not folded into the composite):
**exfiltration risk** (0.60 for a read+network task — *shell-invariant*; only IronShell
can bound it via gating + the `IRONSHELL_NET_ALLOW` egress allowlist) and **prompt-cache**
headroom. That second figure used to be given as *a 90%-stable prefix is ~4.1× cheaper
over 20 turns*, which was modelled rather than measured. Measured
(`cargo test --test prefix_stability --features real-tokens -- --nocapture`), a realistic
20-turn poll — 20 rows, one field changing per turn — holds **61.8%** mean prefix
stability and comes out **2.12× cheaper** at a 0.1 cached-token rate. The gap is
structural, not a rounding error: a row that changes *early* in the table truncates
everything after it, so stability depends on where the change lands, not just how much
changed. Deterministic output remains the precondition — the same test shows a reordered
result forfeits most of the prefix.
---
## Reliable File Editing for LLMs
Traditional shells (Bash, PowerShell) make multi-line text operations error-prone for LLMs due to escaping, quoting, and command injection issues. IronShell provides **structured file editing** that LLMs can use reliably:
### The Problem with Traditional Shells
```bash
# Bash: Fragile multi-line insertion - escaping nightmare
sed -i '10a\
line1\
line2' file.txt # Fails with quotes, backslashes, $vars
# PowerShell: Complex and error-prone
$content = Get-Content file.txt # Race conditions, encoding issues
```
### IronShell: Structured, Reliable Operations
```ish
# Simple string replacement (handles any content)
file.replace("config.rs",
"const DEBUG: bool = false;",
"const DEBUG: bool = true;")
# Multi-line insertion at specific position
file.insert("main.rs", {after: "use std::io;"}, "use std::fs;
use std::path::Path;
use std::collections::HashMap;")
# Insert at line number
file.insert("script.py", 10, "# This comment spans
# multiple lines without
# any escaping needed")
# Batch patches (atomic, all-or-nothing)
file.patch("config.toml", [
{find: "debug = false", replace: "debug = true"},
{find: 'log_level = "info"', replace: 'log_level = "debug"'},
{find: "timeout = 30", replace: "timeout = 60"}
])
# => {success: true, patches_applied: 3, patches_failed: 0}
# Replace with multi-line content
file.replace("template.html",
"<body></body>",
"<body>
<header>Welcome</header>
<main id=\"content\">
Loading...
</main>
</body>")
```
### Why This Matters for AI Agents
| Multi-line insert | ❌ Escape hell | ✅ Native strings |
| Special chars (`$`, `"`, `\`) | ❌ Breaks commands | ✅ Just works |
| Atomic batch edits | ❌ Manual rollback | ✅ Built-in |
| Structured results | ❌ Exit codes only | ✅ `{success, applied, failed}` |
| Unicode/encoding | ❌ Platform-dependent | ✅ UTF-8 always |
```ish
# AI agent can safely edit any file
agent({
goal: "Add error handling to all functions",
tools: ["file.read", "file.patch", "file.insert", "grep"]
})
```
---
## Shell Migration (Transpilers)
IronShell includes built-in transpilers for **Bash**, **Zsh**, and **PowerShell** — making adoption seamless. Run existing scripts directly or pipe shell code through `ish`:
```bash
# Run legacy scripts directly (auto-detected by extension)
ish deploy.sh # Bash → IronShell
ish setup.zsh # Zsh → IronShell
ish provision.ps1 # PowerShell → IronShell
# Explicit mode flags
ish --pwsh -c 'Get-ChildItem *.rs | Measure-Object'
# Pipe shell code via stdin
echo 'for f in *.log; do wc -l $f; done' | ish --bash
```
### What Gets Transpiled
| `ls -la` | `ls("-la")`: does not run, as native `ls` takes a path, not flags (`ish -b` runs `ls -a` in-process) |
| `grep "pattern" file` | `grep("pattern", "file")` |
| `cat file.txt` | `file.read("file.txt")` |
| `X=42` | `let X = 42;` |
| `echo \$HOME` | `print(HOME);` |
| `cmd1 \| cmd2` | `cmd1() \| cmd2()` |
| Multi-line `if/for/while/case` | Single `sh([...])` block |
| `setopt`, `autoload`, `compdef` (Zsh) | `# (zsh-only: ...)` |
| `typeset -A arr` (Zsh) | `let arr = {};` |
| `Get-ChildItem` (PS) | `ls()` |
| `Write-Host` (PS) | `print()` |
The transpilers map **100+ commands** per shell to native IronShell builtins, with block accumulation for multi-line constructs and fallback to `sh()` for anything unsupported.
---
## Protocols
IronShell implements four agentic protocols:
### MCP (Model Context Protocol)
```ish
mcp.tools() # List 130+ tools
mcp.call("git", {command: "status"}) # Execute tool
mcp.connect("http://localhost:3001") # Connect to server
```
IronShell is also an **MCP _server_**: every builtin is exposed as an MCP tool
(annotated with its `x-effect` class) over HTTP, or as a strict **JSON-RPC 2.0 stdio**
server — `ish --agent mcp stdio` — with every `tools/call` routed through the same
policy / jail / approval / audit, so any MCP client gets the full typed surface and
the safety model with zero bespoke integration.
**The HTTP servers require a bearer token.** Both `ish mcp serve` and the agent API
mint one at startup and print it; every route except `/health` needs it. Both run in
agent mode (the effect gate and workspace jail apply) unless `IRONSHELL_MODE` is set:
```console
$ ish mcp serve
MCP bearer token: _4hQtKJGyPyP8bl6tp2iHzoottkROCbqpPAxFIIMa3w
$ curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:3001/mcp/v1/tools
```
Set `IRONSHELL_API_TOKEN` to supply your own. This is not ceremony: the server
executes builtins, so an unauthenticated one lets any web page the user visits
drive it from their browser — loopback is no defence, since the browser is on
the same machine. For the same reason `--cors` is off by default, and binding a
non-loopback address is **refused** unless `IRONSHELL_ALLOW_REMOTE_BIND=true`:
```console
$ ish mcp serve --host 0.0.0.0
Error: refusing to bind 0.0.0.0 - this server executes builtins over plaintext
HTTP ... prefer an SSH tunnel or a reverse proxy terminating TLS.
```
### A2A (Agent-to-Agent)
```ish
a2a.send("analyzer", {task: "review", files: ls("./src")})
a2a.receive("analyzer")
```
### A2UI (Agent-to-User Interface)
```ish
a2ui.notify("Task complete", "success")
a2ui.progress("Processing", 0.75)
a2ui.confirm("Deploy to production?")
```
### NANDA (Consensus)
```ish
nanda.propose("deployment", {version: "2.0", threshold: 0.7})
nanda.vote("proposal_id", true)
```
---
## External Integrations
Connect IronShell to external LLM providers and MCP tool servers.
### External LLMs
```ish
# Auto-detect best available backend
model = ai.detect() # => "ollama:llama3.2:3b"
ai.backends() # List all available providers
# The provider is chosen by IRONSHELL_AI (ai() takes no model argument).
# OpenAI: export IRONSHELL_AI=openai OPENAI_API_KEY=...
ai("Explain quantum computing")
# Anthropic: export IRONSHELL_AI=anthropic ANTHROPIC_API_KEY=...
ai("Write detailed analysis")
# Local Ollama (free, private): export IRONSHELL_AI=ollama
# Start: ollama serve && ollama pull llama3.2:3b
ai("Hello!")
# Any OpenAI-compatible server: export IRONSHELL_AI=compat IRONSHELL_COMPAT_BASE=...
ai("Process this request")
```
### External MCP Tools (e.g., SiliconMonitor)
```ish
# List available MCP servers
mcp.servers()
# Connect to external MCP server (e.g., SiliconMonitor for hardware metrics)
# Start server first: silicon-monitor --mcp --port 3006
monitor = mcp.connect("http://localhost:3006")
print(monitor.available) # => true
print(monitor.tools) # => ["cpu_usage", "memory_info", ...]
# Create agent with external tool access
# The second argument is the tool list, not a model. Pick the model with
# IRONSHELL_AGENT_MODEL_URI; a model string here would be read as a tool name.
agent(
"Monitor system health and alert on high CPU usage",
monitor.tools, # Give agent access to metrics
5 # Max reasoning steps
)
# Connect multiple MCP servers
fs_server = mcp.connect("http://localhost:3001") # Filesystem
git_server = mcp.connect("http://localhost:3002") # Git operations
monitor = mcp.connect("http://localhost:3006") # Hardware metrics
# Combine tools for powerful agents
all_tools = fs_server.tools + git_server.tools + monitor.tools
agent(
"Analyze codebase performance impact on system resources",
all_tools,
10
)
# Agent with MCP endpoint
agent.with_mcp("Check system health", monitor.tools, "http://localhost:3006")
```
### Environment Variables
| `OPENAI_API_KEY` | OpenAI API key |
| `ANTHROPIC_API_KEY` | Anthropic Claude API key |
| `IRONSHELL_AI` | Default AI provider (`openai`, `ollama`) |
| `OLLAMA_HOST` | Ollama server URL (default: localhost) |
| `VLLM_API_BASE` | vLLM server endpoint |
| `COMPAT_API_BASE` | Custom OpenAI-compatible endpoint |
| `AGENT_ALLOW_CMDS` | Whitelist of allowed shell commands |
### Advanced AI Features
```ish
# Cost-based routing — automatically pick cheapest provider
ai_add_route({condition: "cost_under", max_cost_per_1k: 0.01, provider: "ollama"})
# Load balancing across providers (5 strategies)
ai_set_load_balancing("round_robin") # Or: least_latency, weighted, adaptive, least_requests
ai_load_balancing() # Show current strategy
# Local inference — no API keys needed (compile with --features candle or --features onnx)
ai_local_backends() # List available backends
result = ai_local_load("models/llama.gguf") # Load model into memory
ai_local_generate(result.handle, "Hello") # Native Rust inference via Candle
ai_local_embed(result.handle, ["text"]) # Embeddings
ai_local_unload(result.handle) # Free memory
# Usage & cost tracking
ai_usage() # Token usage across providers
ai_cost() # Cost breakdown by provider
ai_registry_stats() # Provider latency, success rate, p95
```
---
## Enterprise
```ish
# Login. Prompts for the password when it is omitted (terminal only -- it will
# not read one off a pipe), stores it as Argon2id, and sets the acting
# principal only after verifying it.
rbac_register("alice") # prompts twice
rbac_login("alice") # prompts once -> {user, session, expires_at}
rbac_session() # who am I, until when
rbac_logout()
# RBAC. Note: the `rbac.*` module is a standalone policy registry for your own
# resources -- it answers `rbac.check` and nothing more. What the safety guard
# consults is a different store: the flat `rbac_principal` / `rbac_grant`
# builtins, keyed by `effect:<class>` and `builtin:<name>`. The two do not
# share entries.
rbac.create("admin", [{resource: "*", actions: ["read", "write", "delete"]}])
rbac.grant("alice", "admin") # assign the role
rbac.check("alice", "config.toml", "write")
# Logging in and granting permissions are `Privileged`, so they are *denied* in
# agent mode: an agent that can acquire authority on its own has none. Its
# principal comes from IRONSHELL_PRINCIPAL or .ironshell/rbac.toml, chosen by whoever
# launched it.
# Audit logging
audit.log("file_modified", "config.toml", {user: "alice"})
audit.query({action: "file_modified", since: "2024-01-01"})
# SSO
sso.init("okta", {client_id: "...", issuer: "https://..."})
sso.auth(callback_data) # refused with E_UNIMPLEMENTED: the callback is not verified
```
---
## ML Built-ins
```ish
# Neural networks
net = nn.create("policy", [8, 16, 4])
output = nn.forward(net, [0.1, 0.2, ...])
# Evolution
pop = evo.population(100, "nn", {layers: [4, 8, 2]})
pop = evo.evolve(pop, fitness_fn, 50)
best = evo.best(pop)
# Reinforcement learning
agent = rl.agent("q-learner", 16, 4, {epsilon: 0.1})
action = rl.action(agent, state)
agent = rl.update(agent, state, action, reward, next_state)
```
---
## Development
```bash
# Build
cargo build --release --bins
# Test
cargo test
# TUI
ish tui
# VS Code extension
code --install-extension admercs.ironshell
```
### Project Structure
```ish
src/
main.rs # Entry point
eval.rs # Expression evaluator
parser.rs # IronShell syntax parser
builtins.rs # 1,200+ builtin functions
modules.rs # Module system (file, sys, net, ...)
ai.rs # AI provider integration
agent.rs # Autonomous agent framework
tui/ # Terminal UI components
transpile/ # Shell transpilers (Bash, Zsh, PowerShell)
```
---
## AI Context & Discoverability
IronShell ships AI-readable metadata for discovery by Claude, ChatGPT, Copilot, and other AI assistants:
| [`llms.txt`](llms.txt) | [llms.txt](https://llmstxt.org) | Short AI-readable summary |
| [`llms-full.txt`](llms-full.txt) | llms.txt | Complete context (syntax, modules, API) |
| [`AGENTS.md`](AGENTS.md) | GitHub Copilot | Agent discovery with module directory |
| [`.github/copilot-instructions.md`](.github/copilot-instructions.md) | GitHub Copilot | Development instructions |
| [`.well-known/ai-plugin.json`](.well-known/ai-plugin.json) | OpenAI | ChatGPT plugin manifest |
| [`.well-known/openapi.yaml`](.well-known/openapi.yaml) | OpenAPI 3.1 | Agent API specification |
---
## License
Dual-licensed under the [GNU Affero General Public License v3.0](LICENSE) (AGPL-3.0-or-later) for open source use, with a [commercial license](https://nervosys.ai) available for proprietary and enterprise use. All contributions require a [CLA](CLA.md). See [LICENSE](LICENSE) for details.
---
<p align="center">
<strong>IronShell</strong> - The OS interface for agentic AI<br>
<a href="https://github.com/nervosys/IronShell">GitHub</a> |
<a href="https://crates.io/crates/iron-shell">Crates.io</a> |
<a href="https://nervosys.github.io/IronShell/book/">Documentation</a> |
<a href="https://github.com/nervosys/IronShell/issues">Issues</a>
</p>