Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

IronShell

Welcome to the official IronShell documentation!

IronShell is a next-generation shell that combines the power of typed functional programming with multimodal AI capabilities. It’s designed for developers who want a shell that understands structure, not just text.

Why IronShell?

Traditional shells treat everything as text, leading to fragile scripts and endless string parsing. IronShell takes a different approach:

  • 🔷 Typed Pipelines: Data flows as structured values (arrays, records, tables), not raw text
  • 🧠 AI-Native: Built-in AI agents, multi-provider support, tool calling, and reasoning
  • ⚡ Functional-First: Lambdas, pattern matching, and immutable-by-default semantics
  • 🎨 Modern TUI: Rich terminal interface with multimodal content (images, charts)
  • 🔌 Extensible: Plugin system, MCP protocol support, and Python/Node.js SDKs

Quick Example

# Type-safe pipelines
let files = ls "." 
  | where(fn(f) => f.size > 1000)
  | sort_by("modified")
  | take(5)

# AI-powered analysis  
let review = ai("Review this code for security issues:
" + cat("app.rs"))

# An agent, with the builtins it may call
agent("Find all TODO comments in src/", ["ls", "cat", "grep"])

Features at a Glance

FeatureDescription
Typed ValuesInt, Float, String, Bool, Array, Record, Table, Lambda
Pipeline Operators|, |>, ?> with full type inference
Pattern Matchingmatch expressions with guards and destructuring
AI Providers20 provider types; OpenAI, Anthropic, Google and Ollama have dedicated clients, the rest go over OpenAI-compatible endpoints
Agent FrameworkSingle agents with builtins as tools, over MCP or in-process
MCPish mcp stdio makes every builtin callable through a three-tool facade
Interactive TUIReal-time chat and multimodal file references

Getting Started

Ready to dive in? Start with Installation to set up IronShell on your system.

If you’re coming from Bash or PowerShell, check out our Quick Start guide that translates common patterns to IronShell.

Community

License

IronShell is open source under the Apache 2.0 License.

Installation

This guide covers installing IronShell on your system.

Requirements

  • Rust 1.88+ (for building from source)
  • OS: Windows, macOS, or Linux
  • Optional: API keys for AI providers (OpenAI, Anthropic, etc.)

Install with Cargo

The recommended way to install IronShell:

cargo install iron-shell

This installs two binaries:

  • ish - The main IronShell executable
  • aimodel - AI model management CLI

Install from Source

For the latest development version:

git clone https://github.com/nervosys/IronShell.git
cd IronShell
cargo build --release

# Add to PATH
cp target/release/ish ~/.local/bin/
cp target/release/aimodel ~/.local/bin/

Pre-built Binaries

Download pre-built binaries from the releases page:

macOS

# Intel Mac
curl -LO https://github.com/nervosys/IronShell/releases/latest/download/ironshell-x86_64-apple-darwin.tar.gz
tar xzf ironshell-x86_64-apple-darwin.tar.gz

# Apple Silicon
curl -LO https://github.com/nervosys/IronShell/releases/latest/download/ironshell-aarch64-apple-darwin.tar.gz
tar xzf ironshell-aarch64-apple-darwin.tar.gz

Linux

curl -LO https://github.com/nervosys/IronShell/releases/latest/download/ironshell-x86_64-unknown-linux-gnu.tar.gz
tar xzf ironshell-x86_64-unknown-linux-gnu.tar.gz

Windows

Download ironshell-x86_64-pc-windows-msvc.zip from releases and extract to a directory in your PATH.

Verify Installation

ish --version
# IronShell 0.2.0

ish --help

VS Code Extension

Install the IronShell extension for syntax highlighting and IDE features:

code --install-extension admercs.ironshell

Or search for “IronShell” in the VS Code marketplace.

Next Steps

Quick Start

Get up and running with IronShell in 5 minutes.

Launch the Shell

# Interactive REPL
ish

# TUI mode (rich terminal interface)
ish tui

# Execute a file
ish script.ish

# Evaluate an expression
ish -e '[1,2,3] | map(fn(x) => x * 2)'

Basic Syntax

Variables

# Immutable by default
let name = "Alice"
let numbers = [1, 2, 3, 4, 5]
let config = { host: "localhost", port: 8080 }

# Mutable when needed
let mut counter = 0
counter = counter + 1

Types

IronShell has a rich type system:

# Primitives
let n = 42           # Int
let f = 3.14         # Float
let s = "hello"      # String
let b = true         # Bool

# Collections
let arr = [1, 2, 3]           # Array[Int]
let rec = { x: 1, y: 2 }      # Record
let table = [[1,"a"], [2,"b"]] # Table

# Functions
let double = fn(x) => x * 2   # Lambda

Pipelines

The power of IronShell is in pipelines:

# Traditional pipeline
[1, 2, 3, 4, 5]
  | filter(fn(x) => x > 2)
  | map(fn(x) => x * 10)
  | sum()
# Result: 120

# File operations
ls "src"
  | where(fn(f) => f.extension == "rs")
  | sort_by("size")
  | take(5)
  | select("name", "size")

Pattern Matching

let describe = fn(x) => match {
    0 => "zero",
    n if n < 0 => "negative",
    n if n > 100 => "large",
    _ => "normal"
}

describe(42)  # "normal"
describe(-5)  # "negative"

AI Features

Simple Query

# Ask the AI a question
ai("What is the capital of France?")

# One prompt; the provider is set by IRONSHELL_AI
ai("Explain monads")

Run an Agent

agent takes a goal, not a persona, and returns the final answer as a string. It is not a callable object: there is no let a = agent(...) then a("...").

# A goal, with no tools
agent("Explain what this project builds")

# A goal, with the builtins it may call
agent("Find all TODO comments under src/", ["ls", "grep", "cat"])

Shell commands are default-deny; export AGENT_ALLOW_CMDS before starting ish to permit any. See Creating Agents.

Set Up AI Provider

# Set environment variable
env_set("OPENAI_API_KEY", "sk-...")

# Or use the config
# ~/.config/ironshell/config.toml

Common Commands

CommandDescription
ls [path]List directory contents as a table
cd pathChange directory
pwdPrint working directory
cat fileRead file contents
envList environment variables
http_get urlMake HTTP GET request
print valueDisplay a value
help [builtin]Get help

Example Script

Save as example.ish:

# Fetch and process data
let response = http_get("https://api.github.com/repos/nervosys/IronShell")
let repo = json_parse(response.body)

print("Repository: " + repo.full_name)
print("Stars: " + string(repo.stargazers_count))
print("Language: " + repo.language)

# Find large files in the project
let large_files = ls "."
  | where(fn(f) => f.size > 10000)
  | sort_by("size", "desc")
  | take(10)

print("\nLargest files:")
large_files | each(fn(f) => print("  " + f.name + ": " + string(f.size) + " bytes"))

Run it:

ish example.ish

Next Steps

Configuration

IronShell reads one TOML settings file and environment variables. AI models are stored in a separate directory, described under Model Storage. Some keys in the TOML file are read but have no effect; the tables below say which.

Configuration File

Where it is read from

IronShell uses the first of these that applies:

  1. The file named by IRONSHELL_CONFIG, if that variable is set. It is used even if it does not exist; then the defaults apply.
  2. config.toml in the config directory, if that file exists. The config directory is IRONSHELL_CONFIG_HOME if set. Otherwise it is an ironshell directory in the OS config directory:
    • Linux: $XDG_CONFIG_HOME/ironshell (~/.config/ironshell when XDG_CONFIG_HOME is unset)
    • macOS: ~/Library/Application Support/ironshell
    • Windows: %APPDATA%\ironshell\config.toml
  3. ~/.ironshellrc, if it exists. It is parsed as TOML if it looks like TOML; otherwise it is read as legacy key = value lines. The legacy keys it reads are color/colors, theme, vi_mode/vi-mode, auto_cd/auto-cd, banner/show_banner, history_size/history-size, editor/EDITOR, ai_model/model, and ai_provider/provider. Other lines are ignored.
  4. Otherwise the built-in defaults. IronShell does not create a file at startup.

If the file exists but cannot be read or does not parse, IronShell starts with the defaults and prints one line to stderr naming the file and the error. config_set() and config_reload() report the error.

Creating and changing it

  • config_init() writes a commented default file to the config directory. It does nothing if config.toml already exists.
  • config_set(path, value) changes one key and saves the file. It accepts only these paths: colors.enabled, colors.force, colors.true_color, colors.theme, shell.show_banner, shell.show_tips, shell.vi_mode, shell.auto_cd, ai.default_model, ai.default_provider, ai.max_tokens, ai.streaming, history.enabled, history.max_size.
    • It writes the same file that the shell reads, which is the file config_path() reports as config_file: IRONSHELL_CONFIG if that is set; otherwise config.toml in the config directory if it exists; otherwise ~/.ironshellrc if it exists; otherwise a new config.toml in the config directory. Those are the rules in the list above. A file that the shell reads from ~/.ironshellrc is written back there, as TOML.
    • After the save, the running shell reloads the file, so the new value is in effect and config_get() returns it at once. The save rewrites the whole file from the settings IronShell knows, so comments and keys it does not read are dropped. The reply says (not applied: ...) for a key the shell stores but does not act on yet.
  • Edits to the file take effect when you run config_reload() or start IronShell again. config_reload() replaces the settings in use; if the file cannot be read or does not parse, it fails and the settings in use stay.

Unknown keys are ignored. A key you leave out takes the default shown below.

Example

The values are the built-in defaults.

[shell]
show_banner = true
show_tips = true
default_directory = ""
vi_mode = false
auto_cd = false
glob_expansion = true
command_correction = true
bell_style = "none"

[colors]
enabled = true
theme = "catppuccin"
force = false
true_color = true

[colors.custom]          # used when theme = "custom"
number = "#a6e3a1"
string = "#a6e3a1"
boolean = "#cba6f7"
keyword = "#cba6f7"
punctuation = "#89b4fa"
key = "#89dceb"
uri = "#f9e2af"
error = "#f38ba8"
warning = "#fab387"
success = "#a6e3a1"
dim = "#6c7086"
comment = "#6c7086"

[prompt]
format = "{symbol}"
continuation = "… "
right = ""
show_git = true
show_time = true
time_threshold_ms = 1000
transient = false
style = "classic"
symbol = "❯"
segments = ["os", "cwd", "git", "status", "duration"]
powerline_separator = ""
abbreviate_path = true
max_path_segments = 0
two_line = false
show_user_host = false
show_git_dirty = false
autosuggestions = true

[prompt.segment_colors]  # e.g. cwd = "#1e1e2e:#89b4fa"

[prompt.abbreviations]   # e.g. gs = "git.status()"

[ai]
default_provider = "openai"
default_model = "gpt-4o-mini"
suggestions = false
max_tokens = 4096
temperature = 0.7
streaming = true
allowed_tools = []
blocked_tools = []
max_agent_steps = 10

[history]
enabled = true
max_size = 10000
ignore_duplicates = true
ignore_space = true
ignore_patterns = ["^exit$", "^quit$"]
share = false
timestamps = true

[editor]
# external defaults to $EDITOR, else notepad on Windows or vi elsewhere
tab_width = 4
syntax_highlighting = true
line_numbers = true
auto_indent = true
bracket_matching = true

[keybindings]
mode = "emacs"

[keybindings.custom]

[aliases]

[env]

Keys

“Applied” means the shell’s own code uses the value. “Not applied” means the key is read into the config and shown by config() and config_get(), and config_set() can change it, but nothing else in the shell acts on it.

[shell]

KeyTypeDefaultStatus
show_bannerBooltrueApplied: the REPL banner
show_tipsBooltrueNot applied
default_directoryString""Not applied
vi_modeBoolfalseNot applied
auto_cdBoolfalseNot applied
glob_expansionBooltrueNot applied
command_correctionBooltrueNot applied
bell_styleString"none"Not applied

[colors]

KeyTypeDefaultStatus
enabledBooltrueApplied: false turns colour off. NO_COLOR also turns it off
themeString"catppuccin"Applied. Names are those Theme::from_str accepts, such as catppuccin, catppuccin-latte, monokai, dracula, nord, gruvbox, tokyonight. An unknown name gives Catppuccin. "custom" uses [colors.custom]
forceBoolfalseNot applied. Forced colour comes from the FORCE_COLOR or CLICOLOR_FORCE environment variables
true_colorBooltrueApplied

[colors.custom] holds hex colours for number, string, boolean, keyword, punctuation, key, uri, error, warning, success, dim and comment. Its defaults are the Catppuccin Mocha colours shown in the example. It is applied when theme = "custom".

[prompt]

Applied. The prompt and the line editor use these keys.

KeyTypeDefault
formatString"{symbol}"
continuationString"… "
rightString""
show_gitBooltrue
show_timeBooltrue
time_threshold_msInteger1000
transientBoolfalse
styleString"classic": classic, fish, powerline, pure or custom
symbolString"❯"
segmentsArray of String["os", "cwd", "git", "status", "duration"]: os, user, host, user@host, cwd, git, status, duration, time, symbol; other values are shown as literal text
powerline_separatorString"" (needs a Nerd Font)
abbreviate_pathBooltrue
max_path_segmentsInteger0 (keep all)
two_lineBoolfalse
show_user_hostBoolfalse
show_git_dirtyBoolfalse (costs a git status per prompt)
autosuggestionsBooltrue
[prompt.segment_colors]Tableempty. Values are "#fg" or "#fg:#bg"
[prompt.abbreviations]Tableempty. Abbreviations expand when you press space

[ai]

Not applied to ai(), to agents or to the TUI. These keys are read and shown by config() and config_get(), and config_set() can change some of them. The provider and model come from IRONSHELL_AI and the variables in AI Provider Setup.

KeyTypeDefault
default_providerString"openai"
default_modelString"gpt-4o-mini"
suggestionsBoolfalse
max_tokensInteger4096
temperatureFloat0.7
streamingBooltrue
allowed_toolsArray of String[]
blocked_toolsArray of String[]
max_agent_stepsInteger10

[history]

KeyTypeDefaultStatus
enabledBooltrueApplied: when false, history is neither loaded nor saved
max_sizeInteger10000Not applied
ignore_duplicatesBooltrueApplied
ignore_spaceBooltrueApplied: commands that start with a space are not saved
ignore_patternsArray of String["^exit$", "^quit$"]Not applied
shareBoolfalseNot applied
timestampsBooltrueNot applied

History is saved to the history file in the data directory.

[editor]

Not applied. The external default is $EDITOR, else notepad on Windows or vi elsewhere.

KeyTypeDefault
externalString$EDITOR, else notepad or vi
tab_widthInteger4
syntax_highlightingBooltrue
line_numbersBooltrue
auto_indentBooltrue
bracket_matchingBooltrue

[keybindings]

Not applied.

KeyTypeDefault
modeString"emacs"
[keybindings.custom]Tableempty

[aliases] and [env]

Not applied. [aliases] is a map from a name to a command. [env] is a map of variable names to values. IronShell does not set these variables. The settings are parsed and kept, but nothing reads them.

Environment Variables

VariableDescriptionExample
IRONSHELL_AIDefault AI provideropenai, claude, ollama
OPENAI_API_KEYOpenAI API keysk-...
ANTHROPIC_API_KEYAnthropic API keysk-ant-...
GOOGLE_API_KEYGoogle AI API keyAIza...
OLLAMA_HOSTOllama address probed by model discovery (chat uses OLLAMA_URL)http://localhost:11434
AGENT_ALLOW_CMDSComma-separated commands an agent step may run; unset allows nonels,cat,grep
IRONSHELL_RECORDLocal evaluation records (off unless set)1, full

The tables below cover every IRONSHELL_* variable the code reads, then the other names IronShell reads to make a decision. Each row gives the code location that reads it. Mode is Agent when the effect applies only in agent mode (--agent, IRONSHELL_MODE=agent, ish agent serve, ish mcp serve), and Both otherwise. Boolean switches accept only the values listed; any other value counts as unset.

Provider and model selection

VariableAccepted values (default)EffectMode
IRONSHELL_AIA provider name, or provider:model. Unset: IronGate if it answers, else an errorChooses the backend for ai() and the TUI chat (complete_sync_router, src/ai.rs:659). Names: irongate (gate, iron), openai, anthropic (claude), google (gemini), azure, ollama, vllm, tgi, llamacpp (llama_cpp), local (lmstudio), compat (openai_compat), openrouter, deepseek, groq, together, fireworks, perplexity, mistral, cohere, xai (grok)Both
IRONSHELL_AI_ROUTERregistry (unset: off)ai() tries the provider registry first, with its rules, status and fallback (src/ai.rs:661)Both
IRONSHELL_MODEL_URIprovider:model (unset)Model for multimodal ai() calls with images, audio or video (src/ai.rs:466), and the backend backend_from_env returns for agents and RLM (src/ai.rs:2073)Both
IRONSHELL_AGENT_MODEL_URIprovider:model (unset: default)Model for agent runs that name none (src/ai.rs:2439, 2459; src/builtins.rs:22773)Both
IRONSHELL_SWARM_AGENT_MODEL_URIprovider:modelModel for each swarm member that names none (src/ai.rs:2714)Both
IRONSHELL_RLM_MODEL_URIprovider:modelSubagent model for recursive agents when the config names none (src/rlm.rs:670)Both
IRONSHELL_COMPAT_BASEURL (http://localhost:8000/v1)Base URL of an OpenAI-compatible server for compat (src/ai.rs:1035, 1151)Both
IRONSHELL_COMPAT_MODELModel name (mixtral)Model sent to the compat server (src/ai.rs:1153, 1401)Both
IRONSHELL_MODEL_DIRDirectoryExtra directory scanned for local models (src/ai_api/providers.rs:866)Both

Agent mode and safety

VariableAccepted values (default)EffectMode
IRONSHELL_MODEagent (case-insensitive). Any other value, or unset, is human modeSelects agent mode: default-deny for dangerous effects, and the workspace jail (current_mode, src/safety.rs:1458). --agent sets it (src/main.rs:330). ish agent serve and ish mcp serve set it when it is unset (src/main.rs:353)Sets the mode
IRONSHELL_AGENT1, true, yes, onAlso selects agent mode (src/safety.rs:1463)Sets the mode
IRONSHELL_POLICYpermissive (case-insensitive). Other values are ignored (strict)Makes the guard and the egress and clipboard checks let agent mode through, as human mode does (policy_permissive, src/safety.rs:1474). --policy sets it (src/main.rs:357)Agent
IRONSHELL_WORKSPACEA directory (unset: the current directory)Workspace root. Writes are confined to it. Setting it jails writes outside agent mode too (workspace_root, src/safety.rs:2673; src/security.rs:492), and module imports are held to it (src/module_loader.rs:303). --workspace sets it (src/main.rs:333)Both
IRONSHELL_FIPS1, on, true (unset: off)Refuses non-FIPS hash algorithms, MD5 and SHA-1 (fips_enabled, src/safety.rs:119, 140)Both
IRONSHELL_NET_ALLOWHosts separated by , or ;. * allows all (unset: no allowlist)Egress allowlist for network builtins. A call naming an unlisted host is refused. The check is not mode-specific, and it is skipped under IRONSHELL_POLICY=permissive (net_allowlist, src/safety.rs:4734; egress_permitted, src/safety.rs:5432)Both
IRONSHELL_ALLOW_SHtrue, 1, yes (exact, lowercase)Enables the sh() builtin, which is otherwise denied (validate_sh_allowed, src/security.rs:942)Both
IRONSHELL_REDACTUnset: on. off, 0, false, no (lowercase) turn it offRedacts secret-looking values from output (redaction_enabled, src/safety.rs:6796)Both
IRONSHELL_SECRETSallow, 1, true (lowercase; unset: denied)Permits agent mode to read secret-named environment variables in the clear (env_secret_gated, src/safety.rs:6838) and to read the clipboard (clipboard_read_gate, src/safety.rs:6851)Agent
IRONSHELL_CHILD_ENV_KEEPComma-separated variable names (unset: none)Secret-named variables a child process keeps in agent mode (child_env_keep, src/safety.rs:2060)Agent
IRONSHELL_CRYPTO_LEGACY_DECRYPT1, true, yes, onAllows decrypting legacy unauthenticated CBC ciphertext, which is otherwise refused (src/builtins.rs:43428)Both
IRONSHELL_APPROVEComma-separated approval tokensApproves the listed tokens, such as a plan token or a network call (is_token_approved, src/safety.rs:3075)Both
IRONSHELL_APPROVE_ALL1, true, yes, onApproves every token. For trusted automation only (src/safety.rs:3071)Both
IRONSHELL_PRINCIPALUser id (unset: anonymous)Acting principal for RBAC checks (init_rbac_from_env, src/safety.rs:3132)Both
IRONSHELL_RBAC_CONFIGPath to an RBAC file (unset: <workspace>/.ironshell/rbac.toml, if it exists)RBAC roles and permissions (src/safety.rs:3137)Both
IRONSHELL_PLUGINSoff (case-insensitive) or 0 disables native pluginsKill switch for loading native plugins (src/plugins.rs:351, 1939)Both
IRONSHELL_PLUGIN_ALLOWDirectories separated by ;Directories native plugins may load from. In agent mode, a plugin outside them is refused (src/plugins.rs:356, 1943)Agent

Limits and output

VariableAccepted values (default)EffectMode
IRONSHELL_MAX_OPSNon-negative integer (unset: no limit)Maximum guarded operations per run (governor_admit, src/safety.rs:3940)Agent
IRONSHELL_MAX_FILESNon-negative integer (unset: no limit)Maximum file operations per run (src/safety.rs:3952)Agent
IRONSHELL_MAX_PROCSNon-negative integer (unset: no limit)Maximum processes started per run (src/safety.rs:3964)Agent
IRONSHELL_MAX_NETNon-negative integer (unset: no limit)Maximum network requests per run (src/safety.rs:3976)Agent
IRONSHELL_TIMEOUT_MSMilliseconds (unset: no limit)Wall-clock budget, started at the first guarded operation (src/safety.rs:3928)Agent
IRONSHELL_TOKEN_BUDGETPositive integer, in estimated tokens (unset: no budget)Budget for agent-mode rendering of arrays and tables, which are paged or truncated to fit (src/agent_api.rs:738, src/repl.rs:244, src/mcp.rs:445). --budget sets it (src/main.rs:326)Agent
IRONSHELL_HANDLE_BYTESInteger bytes (default 2048). 0 disables handlesA structured result larger than this is kept as a session handle that the agent can query, not sent whole (threshold_bytes, src/handles.rs:251; src/builtins.rs:20200)Agent
IRONSHELL_MAX_OUTPUT_BYTESNon-negative integer (unset: no cap)Caps the output of ish -c. A value that is not a number is ignored, with a warning (src/repl.rs:195)Both
IRONSHELL_DETERMINISTIC1 or true (case-insensitive)Renders results as canonical, byte-stable JSON. --deterministic sets it (src/agent_api.rs:346, src/repl.rs:252, src/main.rs:360)Both
IRONSHELL_MCP_TOOLSall or full (default: compact)MCP tool list. The default is three discovery tools; all lists every builtin (tools_compact_mode, src/mcp.rs:320)Both

Audit, journal and telemetry

VariableAccepted values (default)EffectMode
IRONSHELL_AUDIT_LOGA path (agent default: <workspace>/.ironshell/audit.log)Hash-chained audit log (audit_path, src/safety.rs:3211). Human mode writes no audit log unless this is setBoth
IRONSHELL_AUDIT_SINKA pathEach audit line is also appended here, as a mirror (mirror_to_sink, src/safety.rs:3621). A sink inside the workspace is protected from writes (src/safety.rs:2709)Both
IRONSHELL_AUDIT_REQUIRED1, true, yes, onAn audit write failure fails the guarded operation, instead of only logging a warning (src/safety.rs:3705, 3713)Both
IRONSHELL_AUDIT_KEYHex that decodes to at least 32 bytes, or at least 32 raw bytesKey for the keyed audit chain. It is read once and then removed from the environment. A shorter key leaves the chain unkeyed, with an error (load_audit_key, src/safety.rs:3274, AUDIT_KEY_MIN_BYTES at src/safety.rs:3260)Both
IRONSHELL_AUDIT_KEY_FILEA path to a file with the key in the same formatKey source when IRONSHELL_AUDIT_KEY is unset (src/safety.rs:3281)Both
IRONSHELL_JOURNALon, 1, true or off, 0, false (unset: on in agent mode, off for humans)Forces the undo journal on or off (enabled, src/journal.rs:287)Both
IRONSHELL_TELEMETRY1 or true (case-insensitive; unset: off)Starting state of in-process telemetry, which is opt-in (src/builtins.rs:1725)Both

Directories, sessions and features

VariableAccepted values (default)EffectMode
IRONSHELL_CONFIGA path to a fileConfig file location. Replaces config.toml in the config directory (config_file, src/config.rs:451)Both
IRONSHELL_CONFIG_HOMEA directory (default: ironshell in the OS config directory)Config directory (config_dir, src/config.rs:408)Both
IRONSHELL_DATA_HOMEA directory (default: ironshell in the OS data directory)Data directory, including the alert store (data_dir, src/config.rs:422)Both
IRONSHELL_CACHE_HOMEA directory (default: ironshell in the OS cache directory)Cache directory (cache_dir, src/config.rs:436)Both
IRONSHELL_SESSION_DIRA directorySession directory. Handles and record.jsonl go here directly; the journal uses a journal subdirectory (src/handles.rs:44, src/journal.rs:106). On Unix the journal is private to the user (0700 directories, 0600 files), but it holds prior file contents in plain textBoth
IRONSHELL_SESSIONAny key string (unset: an MD5 of the working directory)Names the per-project cache subdirectory for handles and the journal (src/handles.rs:47, src/journal.rs:109)Both
IRONSHELL_FEATURESComma-separated feature namesFeatures that are enabled in feature conditions in cfg expressions and in the feature registry (src/eval.rs:23, src/builtins.rs:1608)Both

Network servers, registries and marketplace

VariableAccepted values (default)EffectMode
IRONSHELL_API_TOKENA stringBearer token for ish agent serve (its --token flag wins) and ish mcp serve (src/main.rs:635, 694)Both
IRONSHELL_ALLOW_REMOTE_BIND1 or trueish mcp serve refuses a non-loopback address unless this is set (src/mcp.rs:1303). The agent API only prints a warning for one (src/agent_api.rs:4869)Both
IRONSHELL_REGISTRY_TOKENA stringPackage registry token. Checked before IRONSHELL_TOKEN (src/packages.rs:362)Both
IRONSHELL_TOKENA stringFallback package registry token (src/packages.rs:363)Both
IRONSHELL_API_KEYA stringRegistry key, required to publish an agent to the marketplace (src/marketplace.rs:650)Both

Variables read from other tools

These are not IronShell settings. Each row names the decision the code makes with it, or says that the value is only reported.

VariableRead atDecision it affects
OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, AZURE_OPENAI_API_KEY, GROQ_API_KEY, MISTRAL_API_KEY, COHERE_API_KEY, DEEPSEEK_API_KEY, XAI_API_KEY, OPENROUTER_API_KEY, TOGETHER_API_KEY, FIREWORKS_API_KEY, PERPLEXITY_API_KEY, LOCAL_API_KEY, IRONGATE_API_KEYsrc/providers/mod.rs:210-229The key a provider is sent with. A provider counts as configured when its key is set (src/providers/registry.rs:86). Bedrock has no key here: AWS_SECRET_ACCESS_KEY is never read, and bedrock: is refused with E_UNIMPLEMENTED because SigV4 signing is not implemented
IRONGATE_URLsrc/model_plane.rs:55, src/providers/mod.rs:241Gateway address. Default http://localhost:7700/v1
IRONGATE_MODELsrc/model_plane.rs:73Model name asked of the gateway. Default auto
IRONGATE_TOKENsrc/model_plane.rs:82Bearer token, used when IRONGATE_API_KEY is unset
IRONVAULT_BINsrc/model_plane.rs:230Vault CLI to run. Default iv
OLLAMA_URLsrc/ai.rs:615, 1128Ollama chat endpoint. Default http://localhost:11434
OLLAMA_MODELsrc/ai.rs:616, 1129Ollama model. Default llama3 (llava in the multimodal path)
OLLAMA_HOSTsrc/ai.rs:1666Address probed when discovering local models. Default http://localhost:11434
OPENAI_MODELsrc/ai.rs:514, 1079, 1281, 1344OpenAI model name. Default gpt-4o-mini
AZURE_OPENAI_ENDPOINTsrc/ai.rs:751, 1017Azure OpenAI deployment URL
VLLM_URL, VLLM_BASE_URL, VLLM_MODEL, VLLM_API_KEYsrc/ai.rs:764, 1025, 1297, 1524, 1525, 1705; src/ai_api/providers.rs:1336vLLM endpoint, model and key. Default endpoint http://localhost:8000/v1
TGI_URL, TGI_BASE_URLsrc/ai.rs:1031, 1191, 1426, 1784TGI endpoint. Default http://localhost:8080
LLAMACPP_URL, LLAMA_CPP_URL, LLAMACPP_MODEL, LLAMACPP_API_KEYsrc/ai.rs:1302, 1553, 1554, 1745; src/ai_api/providers.rs:871, 1747llama.cpp endpoint, model and key. Default http://localhost:8080/v1
LMSTUDIO_MODELsrc/ai.rs:1306LM Studio model name. Default default
TENSORRT_LLM_API_KEY, SGLANG_API_KEYsrc/ai_api/providers.rs:1476, 1610Keys for those servers
HF_TOKEN, HUGGINGFACE_TOKENsrc/ai_api/server.rs:730; src/bin/aimodel.rs:1030, 1032HuggingFace credential. Either one marks it configured
PATH, PATHEXTsrc/providers/ontology.rs:2487, src/builtins.rs:52210Where commands are looked up
HOME, USERPROFILEsrc/builtins.rs:14015; src/ai_api/config.rs:344; src/ai_api/storage.rs:108Home directory, and the fallback for config and model paths
XDG_CONFIG_HOME, XDG_DATA_HOMEsrc/ai_api/config.rs:342; src/ai_api/storage.rs:106, 122; src/safety.rs:5961Base directories for ai-models storage, and for the user git config the remote check reads
APPDATAsrc/ai_api/config.rs:348; src/ai_api/storage.rs:112, 128; src/ai_api/providers.rs:857Windows fallback for the ai-models directories
EDITORsrc/config.rs:376Default external editor
NO_COLORsrc/config.rs:1499Any non-empty value turns colour off
SSL_CERT_FILEsrc/certificates.rs:303PEM bundle of trusted CA certificates. Size-limited
USER, USERNAMEsrc/security.rs:94-160; src/prompt.rs:418Principal name for audit records and the prompt
GIT_DIR, GIT_CONFIG, GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM, GIT_CONFIG_PARAMETERS, GIT_CONFIG_COUNTsrc/safety.rs:5791Any of them set makes the remote check for git fetch, pull and push refuse, rather than read .git/config
GIT_CONFIG_NOSYSTEMsrc/safety.rs:5948Any value other than 0, false, no, off skips the system git config files in that check
DOCKER_HOST, DOCKER_CONTEXTsrc/safety.rs:5169Set means a remote daemon, so the registry-mirror check refuses rather than read the local config
CONTAINER_HOST, CONTAINER_CONNECTIONsrc/safety.rs:5177The same refusal for podman
CONTAINERS_REGISTRIES_CONFsrc/safety.rs:5148, 5180Set means a registries file the check does not read, so the call is refused
containersrc/builtins.rs:52089Read by the container-detection builtin, with /.dockerenv
VIRTUAL_ENVsrc/builtins.rs:52097Reported by the virtualenv query
DISPLAY, WAYLAND_DISPLAYsrc/builtins.rs:53626Reports has_gui on Linux
ProgramDatasrc/plugins.rs:830; src/safety.rs:5950Windows system-wide directory for plugins and the git system config
SUDO_USER, SUDO_UIDsrc/accounts/macos.rs:706, 707Finds the invoking user for macOS account operations
SystemRoot, SystemDrive, COMSPEC, COMPUTERNAME, USERDOMAIN, PSModulePath, PROCESSOR_ARCHITECTURE, WindowsSdkDir, WindowsSDKVersion, VSINSTALLDIR, JAVA_HOME, ANDROID_HOME, ANDROID_SDK_ROOT, CUDNN_PATH, SHELL, TERM, LANG, LC_ALL, TZ, CARGO_HOME, RUSTUP_HOME, PYTHONPATH, GOPATHsrc/builtins.rs (platform and toolchain detection, 52540), src/acl_mount.rs:1174, src/prompt.rs:425Platform, shell and toolchain detection and reporting. Not safety settings

Child processes: the shell removes its own IRONSHELL_* variables from a child’s environment, except the mode, policy, workspace, limit, allowlist, FIPS, deterministic, RBAC and audit settings, which pass through (CONFINING_ENV, strip_ironshell_env, src/safety.rs:1976, 2017). In agent mode, the child also loses secret-named variables unless they are in IRONSHELL_CHILD_ENV_KEEP, and the child receives IRONSHELL_MODE=agent. A variable set explicitly on the child’s command is kept.

Local evaluation records (IRONSHELL_RECORD)

Off by default. With IRONSHELL_RECORD=1, each top-level evaluation appends one JSON line to record.jsonl in the session directory (IRONSHELL_SESSION_DIR, or the per-project cache directory). Nothing is sent anywhere. Each line holds a version, a timestamp, a per-process sequence number, the surface (c, file, repl, api, sh), whether agent mode was on, the names of the builtins called at the top level, a 64-bit FNV-1a hash of the source, the duration, and on failure the error code, whether it is retryable, and whether it was a refusal. Outputs and argument values are not stored.

Each Agent API evaluation is one attempt: eval, call, pipeline, and their streamed and WebSocket forms. A pipeline is recorded as the IronShell source its steps compile to. A POSIX-subset script (ish -b, --bash input, and .sh files it runs natively) has surface sh. Its heads are the first word of each command, or an empty string when that word is not a literal. Its ok means exit status 0, and a non-zero status has no code, because the script’s own exit is not an IronShell failure. A failure that a try { } catch { } handles is not an attempt, so it is not recorded; only a failure that leaves the evaluation is.

IRONSHELL_RECORD=full also stores the source text in a cmd field. Use it only when that text is safe to keep, since it can contain secrets.

record_summary() reads the current session’s file and reports attempts, failures, refusals, failures by code, retries, recoveries and first-try successes. Any other value of the variable records nothing.

Setting Environment Variables

In your shell config (.bashrc, .zshrc, etc.):

export OPENAI_API_KEY="sk-your-key-here"
export IRONSHELL_AI="openai"
export AGENT_ALLOW_CMDS="ls,cat,grep,find,curl,http_get"

Or in IronShell:

env_set("OPENAI_API_KEY", "sk-your-key-here")

AI Provider Setup

IRONSHELL_AI chooses the backend that ai() uses. The [ai] keys in config.toml do not choose it. Set the key variable for the provider, then select the provider with IRONSHELL_AI. IRONSHELL_AI=<name>:<model> also sets the model for the providers listed below.

OpenAI

export OPENAI_API_KEY="sk-..."
export OPENAI_MODEL="gpt-4o"   # optional

With IRONSHELL_AI=openai, the model is OPENAI_MODEL, or gpt-4o-mini when that is unset.

Anthropic (Claude)

export ANTHROPIC_API_KEY="sk-ant-..."

With IRONSHELL_AI=anthropic (or claude) the model is claude-sonnet-4-20250514. No variable changes it. Use IRONSHELL_AI=anthropic:<model> to choose another model.

Google (Gemini)

export GOOGLE_API_KEY="AIza..."

With IRONSHELL_AI=google (or gemini) the model is gemini-2.5-flash. Use IRONSHELL_AI=google:<model> to choose another model.

Local Models (Ollama)

  1. Install Ollama: https://ollama.ai
  2. Pull a model: ollama pull llama3
  3. Use in IronShell:
IRONSHELL_AI=ollama ish -c 'ai("Hello")'

OLLAMA_URL sets the server (default http://localhost:11434), and OLLAMA_MODEL sets the model (default llama3).

Other providers

IRONSHELL_AI valueKey variableModel when none is given
deepseekDEEPSEEK_API_KEYdeepseek-chat
groqGROQ_API_KEYllama-3.3-70b-versatile
togetherTOGETHER_API_KEYmeta-llama/Llama-3.3-70B-Instruct-Turbo
fireworksFIREWORKS_API_KEYaccounts/fireworks/models/llama-v3p3-70b-instruct
perplexityPERPLEXITY_API_KEYsonar
xai (grok)XAI_API_KEYgrok-3
openrouterOPENROUTER_API_KEYopenai/gpt-4o
mistralMISTRAL_API_KEYmistral-large-latest
cohereCOHERE_API_KEYcommand-a
azureAZURE_OPENAI_API_KEY, endpoint from AZURE_OPENAI_ENDPOINTgpt-4o
vllmnone (no key is sent)default, at VLLM_BASE_URL (default http://localhost:8000/v1)
llamacpp (llama_cpp)nonedefault, at http://localhost:8080/v1
local (lmstudio)none (no key is sent)default, at http://localhost:1234/v1

Multiple Providers

ai() takes no model argument. Choose the provider per run with IRONSHELL_AI:

IRONSHELL_AI=openai ish -c 'ai("Query")'
IRONSHELL_AI=ollama ish -c 'ai("Query")'
IRONSHELL_AI=openrouter ish -c 'ai("Query")'

Model Storage

AI model files and their index are stored in an ai-models directory, not in the IronShell data directory. IRONSHELL_DATA_HOME does not move them.

  • Data directory (models, metadata, cache, index): $XDG_DATA_HOME/ai-models if XDG_DATA_HOME is set, else $HOME/.local/share/ai-models if HOME is set, else %APPDATA%\ai-models.
  • Config directory (config.toml, providers.toml, aliases.toml): $XDG_CONFIG_HOME/ai-models, else $HOME/.config/ai-models, else %APPDATA%\ai-models.

Inside the data directory:

  • models/gguf, models/safetensors, models/pytorch and models/onnx hold the model files, one directory per format.
  • metadata/ holds model metadata.
  • cache/ holds temporary files and downloads.
  • index.json is the model index.

The storage section of the ai-models config.toml can set custom_data_dir and custom_config_dir to use other directories. It can also set max_cache_size_gb and auto_cleanup_days. The same file holds the [server], [providers], [security] and [logging] sections for the model API. IRONSHELL_MODEL_DIR adds one more directory that is scanned for local models.

Command-Line Options

ish [OPTIONS] [FILE]
ish <SUBCOMMAND>

With no file, no -c and no subcommand, ish starts the REPL.

OptionMeaning
FILERun this script
-c, --command <CMD>Run one command string and exit
-b, --bashBash compatibility. With no FILE or -c, read the script from stdin
-z, --zshZsh compatibility. With no FILE or -c, read from stdin
-p, --pwshPowerShell compatibility. With no FILE or -c, read from stdin
-a, --agenticAgentic syntax. With no FILE or -c, read from stdin
--agentAgent mode: default-deny for dangerous effects (IRONSHELL_MODE=agent)
--workspace <DIR>Workspace root that confines writes (IRONSHELL_WORKSPACE)
--policy <strict|permissive>Safety policy (IRONSHELL_POLICY)
--budget <N>Token budget for output (IRONSHELL_TOKEN_BUDGET)
--explainPrint the static effect report as JSON instead of running. Use with -c, a file, or stdin
--deterministicCanonical, byte-stable JSON output (IRONSHELL_DETERMINISTIC=1)
-h, --helpPrint help
-V, --versionPrint the version

Subcommands:

SubcommandAliasMeaning
tuiStart the terminal UI
aimodelAI models: serve [--host 127.0.0.1] [--port 8080] [--cors], list [--provider <P>] [--local] (alias ls), download <MODEL>, config, keys store|get|delete|list (alias key)
mcpmcp-serverMCP server: serve [--host 127.0.0.1] [--port 3001] [--cors] [--safety caution] [--admin], tools [--category <C>], stdio (JSON-RPC over stdin and stdout)
agentagent-apiAgent API: serve [--host 127.0.0.1] [--port 3002] [--cors] [--token <T>] [--request-timeout 300], execute [REQUEST] (alias exec; reads stdin when no request is given), schema [--format compact] [--output <F>] [--module <M>] [-q <TEXT>], builtins [--category <C>] [--json] (alias ls), interactive
assistai-assistNatural language to IronShell code: [QUERY] [-x] [-i] [-c] [-s]. -x runs the result, -i is interactive, -c adds system context, -s suggests commands

ish agent serve listens on port 3002 by default, and ish ai serve on port 8080. The API token can come from IRONSHELL_API_TOKEN instead of --token, which keeps it out of the process list.

Startup Scripts

IronShell does not run a startup script. config_path() reports the path of init.ish in the config directory, but nothing executes that file. No code reads login.ish.

Basic Syntax

This guide covers the fundamental syntax of IronShell.

Comments

# This is a single-line comment

// This also works for single-line comments

# Multi-line comments use consecutive single-line comments
# like this
# and this

Expressions

Everything in IronShell is an expression that returns a value:

# Arithmetic
1 + 2 * 3        # 7
10 / 3           # 3 (integer division)
10.0 / 3.0       # 3.333...
10 % 3           # 1 (modulo)
2 ** 10          # 1024 (power)

# Comparison
1 < 2            # true
3 >= 3           # true
"a" == "a"       # true
"a" != "b"       # true

# Logical
true && false    # false
true || false    # true
!true            # false

# String concatenation
"Hello, " + "World!"  # "Hello, World!"

# Ternary-like with match
match x > 0 {
    true => "positive",
    false => "non-positive"
}

Statements

Variable Declaration

# Immutable (default)
let x = 42
let name = "Alice"
let numbers = [1, 2, 3]

# Mutable
let mut counter = 0
counter = counter + 1

# Multiple assignments
let (a, b, c) = (1, 2, 3)

Blocks

Blocks are sequences of expressions. The last expression is the return value:

let result = {
    let x = 10
    let y = 20
    x + y  # Block returns 30
}

print(result)  # 30

Control Flow

If Expressions

let max = if a > b { a } else { b }

# Multi-branch
let grade = if score >= 90 {
    "A"
} else if score >= 80 {
    "B"
} else if score >= 70 {
    "C"
} else {
    "F"
}

Match Expressions

let result = match value {
    0 => "zero",
    1 => "one",
    n if n < 0 => "negative",
    n if n > 100 => "large",
    _ => "other"
}

# Destructuring
let point = { x: 10, y: 20 }
match point {
    { x: 0, y: 0 } => "origin",
    { x: 0, y: _ } => "on y-axis",
    { x: _, y: 0 } => "on x-axis",
    _ => "somewhere else"
}

Loops

# For-each loop
for item in [1, 2, 3, 4, 5] {
    print(item)
}

# With index
for (i, item) in enumerate([1, 2, 3]) {
    print(string(i) + ": " + string(item))
}

# While loop
let mut n = 0
while n < 5 {
    print(n)
    n = n + 1
}

# Loop (infinite, use break)
let mut count = 0
loop {
    count = count + 1
    if count > 10 {
        break
    }
}

Functions

Lambda Syntax

# Single parameter
let double = fn(x) => x * 2
double(21)  # 42

# Multiple parameters
let add = fn(a, b) => a + b
add(2, 3)  # 5

# With block body
let greet = fn(name) => {
    let greeting = "Hello, " + name + "!"
    print(greeting)
    greeting
}

# No parameters
let now = fn() => timestamp()

Named Functions

# Define a named function (syntax sugar)
let factorial = fn(n) => match {
    0 => 1,
    1 => 1,
    n => n * factorial(n - 1)
}

factorial(5)  # 120

Closures

Functions capture their environment:

let make_counter = fn() => {
    let mut count = 0
    fn() => {
        count = count + 1
        count
    }
}

let counter = make_counter()
counter()  # 1
counter()  # 2
counter()  # 3

Operators

Arithmetic

OperatorDescriptionExample
+Addition1 + 2 → 3
-Subtraction5 - 3 → 2
*Multiplication4 * 3 → 12
/Division10 / 3 → 3
%Modulo10 % 3 → 1
**Power2 ** 8 → 256

Comparison

OperatorDescription
==Equal
!=Not equal
<Less than
<=Less than or equal
>Greater than
>=Greater than or equal

Logical

OperatorDescription
&&Logical AND
||Logical OR
!Logical NOT

Pipeline

OperatorDescription
|Pipe (pass as argument)
|>Pipe (method-style)
?>Optional pipe (skip on null)

String Interpolation

let name = "Alice"
let age = 30

# Concatenation
print("Name: " + name + ", Age: " + string(age))

# Using format
print(format("Name: {}, Age: {}", name, age))

Array Spread

let a = [1, 2, 3]
let b = [4, 5, 6]
let combined = [...a, ...b]  # [1, 2, 3, 4, 5, 6]

Record Spread

let base = { x: 1, y: 2 }
let extended = { ...base, z: 3 }  # { x: 1, y: 2, z: 3 }

# Override fields
let updated = { ...base, x: 10 }  # { x: 10, y: 2 }

Types and Values

IronShell is a typed shell where every expression produces a structured Value. Unlike traditional shells that pipe raw text, IronShell pipelines carry rich, typed data.

Core Types

TypeExampleDescription
NullnullAbsence of value
Booltrue, falseBoolean
Int42, -764-bit signed integer
Float3.14, -0.564-bit floating point
String"hello"Text with interpolation support
Uriopenai:gpt-4o-miniURI with scheme (RFC 3986)
Array[1, "two", true]Heterogeneous ordered list
Record{name: "Ada", age: 36}Ordered key-value map
Tableoutput of lsStructured table with schema
Lambdafn(x) => x * 2First-class function
Errorthrow "oops"Error value

Integers and Floats

let x = 42       # Int
let y = 3.14     # Float
let z = x + y    # Float (auto-promoted)

Integer division always produces a Float:

10 / 3   # => 3.3333...

Strings

Strings support ${expr} interpolation:

let name = "world"
let greeting = "Hello, ${name}!"    # => "Hello, world!"
let math = "2 + 2 = ${2 + 2}"      # => "2 + 2 = 4"

String concatenation works with + between two Strings. A String and a number do not combine: that is refused, and the other operand is converted with interpolation or str():

"count: ${42}"        # => "count: 42"
"count: " + str(42)   # => "count: 42"
"100" + " items"      # => "100 items"
"count: " + 42        # error: E_BAD_ARG, cannot combine String and Int

URIs

URIs identify resources with a scheme prefix, commonly used for AI model references:

let model = openai:gpt-4o-mini
let local = ollama:llama3

Arrays

Arrays hold any mix of types:

let nums = [1, 2, 3]
let mixed = [1, "two", true, [4, 5]]

Records

Records are key-value maps with sorted keys:

let person = {name: "Ada", age: 36, langs: ["Rust", "Python"]}
person.name    # => "Ada"
person.langs   # => ["Rust", "Python"]

Tables

Tables are structured arrays of records with a defined schema. Many builtins return tables:

ls "."    # => Table with columns: name, path, ext, is_dir, size, modified

Tables get special pretty-printed column-aligned output in the terminal.

Type Conversion

IronShell performs automatic numeric promotion in arithmetic:

ExpressionResult TypeRule
Int + IntIntInteger arithmetic
Int + FloatFloatPromote to float
Float + FloatFloatFloat arithmetic
Int ^ Int (positive)IntInteger power
Int ^ Int (negative)FloatFloat power
Int / IntFloatAlways float division

Equality comparison between Int and Float works via casting.

Truthiness

All values have a boolean interpretation:

FalsyTruthy
nullNon-null values
falsetrue
0, 0.0Non-zero numbers
"" (empty string)Non-empty strings
[] (empty array)Non-empty arrays
{} (empty record)Non-empty records
Error(...)Lambdas, Builtins

JSON Interop

Values convert bidirectionally with JSON:

let data = from_json '{"name": "test", "count": 42}'
data.name    # => "test"

let json_str = to_json {name: "test", count: 42}
# => '{"count":42,"name":"test"}'

Type Inspection

describe 42          # => "Int"
describe "hello"     # => "String"
describe [1, 2, 3]   # => "Array(3 elements)"

Variables and Bindings

IronShell supports several forms for declaring and binding variables, from explicit let declarations to concise shorthand syntax.

Let Bindings

The standard way to declare a variable:

let name = "IronShell"
let version = 3
let features = ["typed", "pipelines", "AI"]

Variables are immutable by default. Attempting to reassign an immutable variable produces an error:

let x = 10
x = 20    # Error: Cannot reassign immutable variable 'x'. Use 'let mut x' to make it mutable.

Mutable Variables

Use mut to allow reassignment:

let mut counter = 0
counter = counter + 1    # OK
counter = 42             # OK

Or with shorthand:

mut counter = 0
counter = counter + 1

Shorthand Syntax

IronShell offers shorter forms for common patterns:

# These are all equivalent:
let x = 10
x = 10       # Inferred let
x := 10      # Walrus-style binding

Public Variables

Variables can be marked public for export from modules:

pub let API_URL = "https://api.example.com"
pub let VERSION = "1.0.0"

Public variables are accessible when the module is imported.

Type Annotations

Optional type annotations can be added to bindings:

let name: String = "hello"
let count: Int = 42

Type annotations are parsed but currently used for documentation purposes. The type inference engine (typecheck.rs) handles type validation.

Scoping Rules

IronShell uses a flat environment model:

  • All variables share a single scope
  • Lambda parameters are temporarily bound during execution and restored after
  • There are no block-level scopes or shadowing in the traditional sense
  • Variables are visible once declared and remain available for the rest of the session
let x = 10
let f = fn(x) => x * 2    # Lambda parameter 'x' temporarily shadows outer 'x'
f(5)                        # => 10
x                           # => 10 (outer x unchanged)

Environment Variables

Shell environment variables are accessible and can be set:

# Access environment variable
let home = $HOME

# Set environment variable
export PATH = "/usr/local/bin:${$PATH}"

Assignment vs. Declaration

SyntaxMeaning
let x = exprImmutable declaration
let mut x = exprMutable declaration
x = exprShorthand immutable declaration (or reassign if mutable)
x := exprShorthand immutable declaration
mut x = exprShorthand mutable declaration
pub let x = exprPublic immutable declaration

Functions and Lambdas

IronShell treats functions as first-class values using lambda syntax. Lambdas can be passed to pipelines, stored in variables, and composed freely.

Lambda Syntax

The basic form is fn(params) => body:

let double = fn(x) => x * 2
let add = fn(a, b) => a + b
let greet = fn() => "hello"

Lambdas are expressions — they return the value of their body:

double(5)      # => 10
add(3, 4)      # => 7
greet()        # => "hello"

Multi-Parameter Lambdas

let clamp = fn(value, lo, hi) =>
    if value < lo { lo }
    else if value > hi { hi }
    else { value }

clamp(15, 0, 10)    # => 10
clamp(-5, 0, 10)    # => 0

Calling Conventions

IronShell supports multiple ways to call functions:

Parenthesized Calls

Standard function calling:

double(5)
add(3, 4)

Word Calls

At the top level, functions can be called without parentheses (shell-style):

print "hello"          # equivalent to print("hello")
echo "world"           # equivalent to echo("world")
cd "/home"             # equivalent to cd("/home")

Note: Word-call syntax is disabled inside lambda bodies to prevent ambiguous parsing.

Pipeline Calls

Functions can receive input through the pipe operator:

5 | double             # => 10
[1, 2, 3] | double     # => [2, 4, 6] (auto-maps over arrays)

Auto-Mapping

When a 1-parameter lambda receives an Array through a pipeline, it automatically maps over each element:

[1, 2, 3] | fn(x) => x * 2           # => [2, 4, 6]
["a", "b"] | fn(s) => s + "!"        # => ["a!", "b!"]

A single (non-array) value is passed directly:

5 | fn(x) => x * 2    # => 10

Index Parameter

Callback functions used with map and where can accept a second parameter for the index:

["a", "b", "c"] | map fn(item, i) => "${i}: ${item}"
# => ["0: a", "1: b", "2: c"]

Closures

Lambdas capture variables from the enclosing environment by reference. Variables are resolved at call time:

let multiplier = 3
let scale = fn(x) => x * multiplier
scale(10)              # => 30

# If multiplier changes (if mutable), scale reflects the new value

Async Functions

Async lambdas create futures that must be awaited:

let fetch_data = async fn(url) => http_get(url)
let future = fetch_data("https://api.example.com/data")
let result = await future

Recursion

Lambdas can reference themselves through their binding name:

let factorial = fn(n) =>
    if n <= 1 { 1 }
    else { n * factorial(n - 1) }

factorial(5)    # => 120

This works because variable lookup happens at evaluation time, so factorial resolves to the lambda in the environment.

Higher-Order Functions

Functions that take or return functions:

let apply_twice = fn(f, x) => f(f(x))
apply_twice(fn(x) => x + 1, 0)    # => 2

let make_adder = fn(n) => fn(x) => x + n
let add5 = make_adder(5)
add5(10)    # => 15

Builtins as Values

Builtin functions can be referenced and passed around:

let my_sort = sort
[3, 1, 2] | my_sort    # => [1, 2, 3]

Common Patterns

Pipeline with inline lambda

ls "." | where fn(f) => f.size > 1000 | map fn(f) => f.name

Compose transformations

let transform = fn(data) =>
    data
    | where fn(r) => r.active
    | map fn(r) => {name: r.name, score: r.points * 10}
    | sort

Reduce / fold

[1, 2, 3, 4, 5] | reduce fn(acc, x) => acc + x, 0    # => 15

Pipelines

Pipelines are the heart of IronShell. The pipe operator | connects expressions so that the output of one becomes the input of the next — but unlike traditional shells, IronShell pipelines carry typed, structured data, not raw text.

Basic Syntax

expression | transform | transform | ...

Each | takes the value on the left and passes it to the right:

[3, 1, 4, 1, 5] | sort | reverse | first
# [3,1,4,1,5] → [1,1,3,4,5] → [5,4,3,1,1] → 5

How Values Flow

The pipe operator is left-associative: a | b | c is parsed as (a | b) | c.

When the right side of a pipe is evaluated, the left side’s value is available as pipeline input. How it’s consumed depends on what’s on the right:

Right-hand sideBehavior
Lambda literalCalled with left value as argument
Named lambda (variable)Called with left value as explicit argument
Builtin functionReceives left value as input parameter
Other expressionLeft value set as implicit input during evaluation

Example: builtins in pipelines

ls "."                                # Array of file records
| where fn(f) => f.size > 1000       # Filter: keep large files
| map fn(f) => f.name                # Transform: extract names
| sort                                # Sort alphabetically

Example: lambda in pipelines

42 | fn(x) => x * 2       # => 84
"hello" | fn(s) => upper(s) # => "HELLO"

Auto-Mapping

When a 1-parameter lambda receives an Array, it automatically maps over each element:

[1, 2, 3] | fn(x) => x * 2     # => [2, 4, 6]

This applies to both inline lambdas and named lambdas. If you want to operate on the array as a whole, use the length or similar builtin directly:

[1, 2, 3] | length    # => 3  (operates on the whole array)

Data Pipeline Builtins

These builtins are designed for pipeline use:

Filtering

[1, 2, 3, 4, 5] | where fn(x) => x > 3
# => [4, 5]

ls "." | where fn(f) => f.ext == "rs"
# Only Rust files

Mapping

[1, 2, 3] | map fn(x) => x * 10
# => [10, 20, 30]

# With index parameter
["a", "b", "c"] | map fn(item, i) => "${i}: ${item}"
# => ["0: a", "1: b", "2: c"]

Reducing

[1, 2, 3, 4, 5] | reduce fn(acc, x) => acc + x, 0
# => 15

Selecting fields

ls "." | select "name" "size"
# => Array of records with only name and size fields

Grouping

ls "." | group "ext"
# Records grouped by file extension

Sorting

[3, 1, 4, 1, 5] | sort
# => [1, 1, 3, 4, 5]

Structured Data Pipelines

Since ls, ps, and other builtins return structured data, you can build powerful queries:

# Find the 5 largest Rust files
ls "src"
| where fn(f) => f.ext == "rs"
| sort
| reverse
| first 5
| select "name" "size"
# Calculate total size of all .toml files
ls "."
| where fn(f) => f.ext == "toml"
| map fn(f) => f.size
| reduce fn(a, b) => a + b, 0

Format Conversion Pipelines

Convert between data formats inline:

# JSON to CSV
from_json '[{"name":"Ada","age":36},{"name":"Bob","age":30}]' | to_csv

# Process HTTP response
http_get "https://api.example.com/users" | from_json | where fn(u) => u.active

Pipeline Input in Builtins

Builtins can access pipeline input implicitly. For example, sort works both ways:

sort [3, 1, 2]        # Direct call with argument
[3, 1, 2] | sort      # Pipeline: input received implicitly

This dual calling convention makes builtins equally useful in both interactive and pipeline contexts.

Chaining with AI

Pipelines compose naturally with AI operations:

# Read a file, ask AI to summarize it
cat "README.md" | ai "Summarize this document in 3 bullet points"
# Generate code, then format it
ai "Write a Python function to sort a list" | save "sort.py"

Pattern Matching

IronShell provides match expressions for destructuring values and branching on their structure. Pattern matching works with all value types including arrays, records, and constructor-style tagged values.

Basic Match

let x = 42

match x {
    0 => "zero",
    1 => "one",
    _ => "something else"
}
# => "something else"

The _ wildcard matches anything without binding a name.

Binding Patterns

Identifier patterns match any value and bind it to a name:

let result = match get_status() {
    "ok" => "all good",
    "error" => "something broke",
    other => "unexpected: ${other}"
}

Literal Patterns

Match against exact values:

match value {
    42 => "the answer",
    "hello" => "greeting",
    true => "affirmative",
    null => "nothing",
    _ => "default"
}

Supported literal patterns:

PatternMatches
42Exact integer
"hello"Exact string
true / falseExact boolean
nullNull value

Array Patterns

Destructure arrays by matching their elements:

match [1, 2, 3] {
    [] => "empty",
    [x] => "one element: ${x}",
    [x, y] => "two: ${x}, ${y}",
    [x, y, z] => "three: ${x}, ${y}, ${z}",
    _ => "more than three"
}
# => "three: 1, 2, 3"

Array patterns require an exact length match. [x, y] won’t match a 3-element array.

Patterns can be nested:

match [[1, 2], [3, 4]] {
    [[a, b], [c, d]] => a + b + c + d,
    _ => 0
}
# => 10

Record Patterns

Match records by their fields:

let person = {name: "Ada", age: 36, role: "engineer"}

match person {
    {name: "Ada", role} => "Found Ada, role: ${role}",
    {name, age} => "${name} is ${age} years old",
    _ => "unknown"
}
# => "Found Ada, role: engineer"

Shorthand: {name} is equivalent to {name: name} — it matches the field name and binds its value to variable name.

Record patterns match if all specified fields exist. Extra fields are ignored:

# This matches even though the record has 'role' too
match {name: "Ada", age: 36, role: "engineer"} {
    {name, age} => "${name}, ${age}",
    _ => "no match"
}
# => "Ada, 36"

Constructor Patterns

IronShell uses tagged records to represent algebraic data types. The Some and None constructors create tagged records:

let result = Some(42)    # => {_tag: "Some", _value: 42}
let empty = None         # => {_tag: "None"}

Match on constructors:

match Some(42) {
    Some(x) => "got value: ${x}",
    None => "nothing",
    _ => "unexpected"
}
# => "got value: 42"

Zero-argument constructors just check the _tag:

match None {
    Some(x) => "got ${x}",
    None => "nothing here",
}
# => "nothing here"

Guards

Add conditions to match arms with if:

match value {
    x if x > 100 => "large",
    x if x > 10 => "medium",
    x if x > 0 => "small",
    0 => "zero",
    x => "negative: ${x}"
}

Guards are evaluated with the pattern’s bindings in scope. If the guard is falsy, the next arm is tried:

match {name: "Ada", age: 36} {
    {name, age} if age >= 21 => "${name} is an adult",
    {name, age} => "${name} is ${age} years old",
    _ => "unknown"
}
# => "Ada is an adult"

Exhaustiveness

If no arm matches the value, a runtime error is produced:

Error: match: no arm matched the value

Always include a _ wildcard as the last arm to handle unexpected cases:

match status {
    "active" => handle_active(),
    "paused" => handle_paused(),
    _ => throw "Unknown status: ${status}"
}

Match as Expression

match is an expression — it returns a value:

let label = match count {
    0 => "none",
    1 => "one",
    _ => "many"
}

print label

Practical Examples

Implicit scrutinee in lambdas

When match is used inside a lambda body, the scrutinee can be omitted — it defaults to the lambda’s first parameter:

# Explicit scrutinee (always works)
let grade = fn(score) => match score {
    s if s >= 90 => "A",
    s if s >= 80 => "B",
    _       => "C"
}

# Implicit scrutinee (same result, cleaner)
let grade = fn(score) => match {
    s if s >= 90 => "A",
    s if s >= 80 => "B",
    _       => "C"
}

grade(85)  # "B"

This works with any single-parameter lambda, including in pipelines:

[1, 2, 3, 100] | map fn(x) => match {
    _ if x > 50 => "big",
    _            => "small"
}
# => ["small", "small", "small", "big"]

Note: match expr { ... } with an explicit scrutinee is always supported. The implicit form only applies inside lambdas and uses the first parameter.

Processing command output

let files = ls "."

files | map fn(f) => match {
    {is_dir: true, name} => "📁 ${name}/",
    {ext: "rs", name} => "🦀 ${name}",
    {ext: "md", name} => "📝 ${name}",
    {name} => "   ${name}"
}

Option handling

let find_user = fn(id) =>
    if id == 1 { Some({name: "Ada", role: "admin"}) }
    else { None }

match find_user(1) {
    Some({name, role}) => "Found ${name} (${role})",
    None => "User not found"
}
# => "Found Ada (admin)"

HTTP response handling

let response = http_get "https://api.example.com/status"

match response {
    {status: 200, body} => from_json(body),
    {status: 404} => throw "Not found",
    {status} => throw "HTTP error: ${status}"
}

Records and Tables

Records and tables are IronShell’s structured data types. Records are key-value maps; tables are arrays of records with a defined schema. Together they enable typed data processing pipelines.

Records

Creating Records

let person = {name: "Ada", age: 36, active: true}
let config = {host: "localhost", port: 8080, debug: false}

Record keys are always strings. Values can be any type, including nested records and arrays:

let project = {
    name: "IronShell",
    version: {major: 0, minor: 3, patch: 0},
    tags: ["shell", "rust", "ai"]
}

Field Access

Use dot notation to access fields:

person.name      # => "Ada"
person.age       # => 36
project.version.major    # => 0

Accessing a non-existent field produces an error:

person.email     # Error: field 'email' not found in record

Record Operations

# Get all keys
{name: "Ada", age: 36} | keys
# => ["age", "name"]   (sorted alphabetically)

# Merge records (later values win)
let defaults = {color: "blue", size: 10}
let custom = {size: 20, bold: true}
merge defaults custom
# => {bold: true, color: "blue", size: 20}

Records in Pipelines

Records flow through pipelines as structured data:

let users = [
    {name: "Ada", score: 95},
    {name: "Bob", score: 82},
    {name: "Eve", score: 91}
]

users
| where fn(u) => u.score > 85
| map fn(u) => {name: u.name, grade: "A"}
# => [{name: "Ada", grade: "A"}, {name: "Eve", grade: "A"}]

Tables

Tables are structured data with named columns, returned by many builtins.

Table Structure

A table has:

  • rows: Array of records (each row is a {key: value} map)
  • schema: List of column names defining the structure

Built-in Table Sources

# List files — returns a table
ls "."
# Columns: name, path, ext, is_dir, size, modified

# Process listings
ps
# Columns: pid, name, cpu, memory

Pretty Printing

Tables get special column-aligned display in the terminal:

┌──────────────┬──────┬─────┬────────┐
│ name         │ ext  │ dir │ size   │
├──────────────┼──────┼─────┼────────┤
│ main.rs      │ rs   │ no  │ 2,451  │
│ lib.rs       │ rs   │ no  │ 1,089  │
│ Cargo.toml   │ toml │ no  │ 456    │
└──────────────┴──────┴─────┴────────┘

Data Pipeline Operations

select — Project Fields

Keep only specific columns:

ls "." | select "name" "size"
# Records with only name and size fields

where — Filter Rows

Keep rows matching a predicate:

ls "." | where fn(f) => f.size > 1000
ls "." | where fn(f) => f.ext == "rs"

map — Transform Rows

Create new values from each row:

ls "." | map fn(f) => {
    file: f.name,
    kb: f.size / 1024
}

sort — Order Rows

[3, 1, 4, 1, 5] | sort
# => [1, 1, 3, 4, 5]

group / group_by — Group Rows

Group records by a field value:

ls "." | group "ext"
# Records grouped by file extension

reduce — Aggregate

Collapse an array into a single value:

ls "." | map fn(f) => f.size | reduce fn(a, b) => a + b, 0
# Total size of all files

first / last — Take Elements

[1, 2, 3, 4, 5] | first 3    # => [1, 2, 3]
[1, 2, 3, 4, 5] | last 2     # => [4, 5]

reverse — Reverse Order

[1, 2, 3] | reverse    # => [3, 2, 1]

unique — Remove Duplicates

[1, 2, 2, 3, 3, 3] | unique    # => [1, 2, 3]

columns — Get Column Names

ls "." | columns
# => ["ext", "is_dir", "modified", "name", "path", "size"]

Format Conversion

Convert between structured data and serialization formats:

# JSON
let data = from_json '{"name": "test"}'
data | to_json

# CSV
let csv_data = from_csv "name,age\nAda,36\nBob,30"
csv_data | to_csv

# YAML
let yaml_data = from_yaml "name: test\ncount: 42"
yaml_data | to_yaml

Practical Examples

Analyze project files

ls "src"
| where fn(f) => f.ext == "rs"
| map fn(f) => {name: f.name, kb: f.size / 1024}
| sort
| reverse
# Rust files sorted by size, largest first

Process API response

http_get "https://api.github.com/repos/user/repo/issues"
| from_json
| where fn(i) => i.state == "open"
| map fn(i) => {title: i.title, labels: i.labels | map fn(l) => l.name}
| first 10

Build a report

let files = ls "src" | where fn(f) => f.ext == "rs"
let total_size = files | map fn(f) => f.size | reduce fn(a, b) => a + b, 0
let count = files | length

{
    total_files: count,
    total_bytes: total_size,
    avg_size: total_size / count,
    largest: files | sort | reverse | first 1
}

Error Handling

IronShell provides structured error handling with try/catch expressions and first-class Error values. Errors can be created with throw, caught with try/catch, and inspected like any other value.

Error Values

Error is a first-class value type. It’s falsy and carries a string message:

let err = throw "something went wrong"
# err is Value::Error("something went wrong")

Try / Catch

Catch errors and recover gracefully:

let result = try {
    cat("config.toml")
} catch {
    "fallback value"
}

Binding the Error Message

Use catch variable to capture the error message:

let result = try {
    http_get "https://unreachable.example.com"
} catch e {
    print "Request failed: ${e}"
    null
}

The catch variable receives the error message as a String.

Throw

Create an error value explicitly:

throw "file not found"
throw "invalid argument: ${arg}"

throw evaluates its expression, converts it to a string, and returns a Value::Error. If the thrown value is already a String, it’s used directly; otherwise it’s formatted.

What Gets Caught

try/catch handles two categories of errors:

  1. Error values — produced by throw:

    try { throw "oops" } catch e { "caught: ${e}" }
    # => "caught: oops"
    
  2. Runtime errors — produced by invalid operations:

    try { 1 / 0 } catch e { "caught: ${e}" }
    try { null.field } catch e { "caught: ${e}" }
    

If the try block succeeds (returns a non-error value), the catch branch is not executed:

try { 42 } catch { "never reached" }
# => 42

Common Runtime Errors

ErrorCause
"unknown builtin: name"Calling a function that doesn’t exist
"cannot call null"Trying to call a null value as a function
"field 'x' not found in record"Accessing a missing record field
"Cannot reassign immutable variable 'x'"Reassigning a let binding
"match: no arm matched the value"Non-exhaustive match statement
"lambda arity mismatch"Wrong number of arguments
"expected Bool"Using non-boolean in if condition

Error Propagation

Without try/catch, errors propagate up and terminate the current execution:

let validate = fn(x) =>
    if x < 0 { throw "must be non-negative" }
    else { x }

# This will produce an error since we don't catch it
validate(-5)

Wrap the call in try/catch to handle it:

let result = try { validate(-5) } catch e {
    print "Validation failed: ${e}"
    0
}
# result is 0

Practical Patterns

Default on failure

let config = try { from_json(cat "config.json") } catch {
    {host: "localhost", port: 8080}
}

Retry logic

let fetch_with_retry = fn(url) => {
    let result = try { http_get url } catch { null }
    if result { result }
    else {
        sleep 1000
        try { http_get url } catch e {
            throw "Failed after retry: ${e}"
        }
    }
}

Validate and collect errors

let validate_user = fn(user) => {
    if !user.name { throw "name is required" }
    if user.age < 0 { throw "age must be non-negative" }
    if !user.email { throw "email is required" }
    user
}

let result = try { validate_user({name: "", age: -1}) } catch e {
    print "Invalid user: ${e}"
    null
}

Pipeline error handling

# Errors in pipelines can be caught at any stage
let safe_pipeline = fn(data) =>
    try {
        data
        | from_json
        | where fn(r) => r.value > 0
        | map fn(r) => r.value * 2
    } catch e {
        print "Pipeline failed: ${e}"
        []
    }

Error vs. Null

  • null represents absence of a value (intentional)
  • Error(msg) represents a failure with a message (exceptional)
  • Both are falsy, but Error carries diagnostic information
let result = find_user("nonexistent")

match result {
    null => "not found",
    Error(msg) => "error: ${msg}",
    user => "found: ${user.name}"
}

Builtins Overview

IronShell provides a rich set of built-in commands that return structured data for pipeline processing.

Core Philosophy

Unlike traditional shells where commands return text, IronShell builtins return typed Value objects:

# Traditional shell: ls returns text
# IronShell: ls returns Array[Record]

ls "."
# Returns: [
#   { name: "file.txt", size: 1234, modified: "2024-01-01T12:00:00Z", is_dir: false },
#   { name: "src", size: 4096, modified: "2024-01-01T10:00:00Z", is_dir: true },
#   ...
# ]

This enables powerful pipeline operations:

ls "."
  | where(fn(f) => f.size > 1000)
  | sort_by("modified", "desc")
  | take(5)
  | select("name", "size")

Categories

File System

CommandDescriptionReturns
ls pathList directoryArray[Record]
cat fileRead file contentsString
read fileRead file (alias)String
write file contentWrite to fileBool
mkdir pathCreate directoryBool
rm pathRemove file/dirBool
mv src dstMove/renameBool
cp src dstCopyBool
pwdCurrent directoryString
cd pathChange directory()

Data Processing

CommandDescriptionReturns
map(fn)Transform each elementArray
filter(fn)Keep matching elementsArray
reduce(fn, init)Fold to single valueAny
sort_by(field, dir)Sort by fieldArray
where(fn)Filter (alias)Array
select(fields...)Pick fieldsArray[Record]
take(n)First n elementsArray
skip(n)Skip n elementsArray
flatten()Flatten nested arraysArray
unique()Remove duplicatesArray
group_by(field)Group by fieldRecord

Text Processing

CommandDescriptionReturns
grep pattern pathSearch in filesArray[Record]
split str delimSplit stringArray[String]
join arr delimJoin to stringString
trim strRemove whitespaceString
replace old new strReplace textString
uppercase strTo uppercaseString
lowercase strTo lowercaseString

Network

CommandDescriptionReturns
http_get urlGET requestRecord
http_post url bodyPOST requestRecord
http_put url bodyPUT requestRecord
http_delete urlDELETE requestRecord

JSON/Data

CommandDescriptionReturns
json_parse strParse JSONAny
json_stringify valSerialize to JSONString
csv_parse strParse CSVArray[Array]
csv_stringify dataSerialize to CSVString

System

CommandDescriptionReturns
envAll env varsRecord
env_get nameGet env varString?
env_set name valSet env var()
exec cmd argsRun commandRecord
which nameFind executableString?
psProcess listArray[Record]

Math

CommandDescriptionReturns
sum arrSum of numbersNumber
avg arrAverageFloat
min arrMinimumNumber
max arrMaximumNumber
abs nAbsolute valueNumber
floor nFloorInt
ceil nCeilingInt
round nRoundInt
sqrt nSquare rootFloat

Type Conversion

CommandDescriptionReturns
int valConvert to intInt
float valConvert to floatFloat
string valConvert to stringString
bool valConvert to boolBool
array valConvert to arrayArray

Output

CommandDescriptionReturns
print valPrint value()
println valPrint with newline()
debug valDebug output()
format str args...Format stringString

AI

CommandDescriptionReturns
ai prompt opts?AI queryString
agent prompt opts?Create agentAgent

Return Value Structure

File System Records

# ls returns:
{
    name: String,      # File name
    path: String,      # Full path
    size: Int,         # Size in bytes
    modified: String,  # ISO timestamp
    is_dir: Bool,      # Is directory
    is_file: Bool,     # Is file
    extension: String, # File extension
    permissions: String # Unix permissions
}

HTTP Response

# http_get/post returns:
{
    status: Int,       # HTTP status code
    headers: Record,   # Response headers
    body: String,      # Response body
    ok: Bool          # status >= 200 && status < 300
}

Grep Match

# grep returns:
{
    file: String,      # File path
    line: Int,         # Line number
    content: String,   # Matching line
    match: String      # Matched text
}

Pipeline Examples

# Find large Rust files
ls "src"
  | where(fn(f) => f.extension == "rs" && f.size > 10000)
  | sort_by("size", "desc")
  | select("name", "size")

# API data processing
http_get("https://api.example.com/users")
  | json_parse()
  | where(fn(u) => u.active)
  | map(fn(u) => { name: u.name, email: u.email })
  | take(10)

# Log analysis
cat("app.log")
  | split("\n")
  | where(fn(line) => line.contains("ERROR"))
  | map(fn(line) => {
      let parts = split(line, " ")
      { timestamp: parts[0], message: join(skip(parts, 2), " ") }
  })

Core Operations

Core builtins provide essential shell operations: output, environment management, JSON handling, option types, diagnostics, and shell interop.

Output

print

Print a value to stdout without a trailing newline.

print "hello"       # hello
print 42            # 42
print [1, 2, 3]     # [1, 2, 3]

echo

Print a value followed by a newline. Equivalent to println in many languages.

echo "Hello, world!"
echo { name: "Ada", age: 36 }

debug / dbg

Print a value with type and structure information, useful for development.

debug [1, "two", 3.0]
# Array(3): [Int(1), String("two"), Float(3.0)]

let rec = { x: 1, y: [2, 3] }
dbg rec

Help & Inspection

help

Display available commands and usage information.

help              # List all builtins
help "map"        # Help for a specific builtin

type_of / typeof

Return the type name of a value as a string.

type_of 42           # "Int"
type_of "hello"      # "String"
type_of [1, 2, 3]    # "Array"
type_of { x: 1 }     # "Record"
typeof fn(x) => x    # "Lambda"

inspect

Return a detailed string representation of a value including internal structure.

inspect [1, "two", true]
# "[Int(1), String(\"two\"), Bool(true)]"

Option Types

IronShell has first-class Some/None for representing optional values.

Some

Wrap a value in an option.

let result = Some(42)
echo result          # Some(42)

None

The empty option value.

let missing = None
echo missing         # None

Options are useful in pipelines where operations may not find a result:

let found = [1, 2, 3] | first
# found is Some(1) or None if array is empty

Environment Variables

env

Return all environment variables as a Record.

let vars = env
echo vars.PATH
echo vars.HOME

set_env

Set an environment variable for the current session.

set_env "MY_VAR" "hello"
echo (env).MY_VAR    # hello

JSON

json_parse

Parse a JSON string into a structured Value (Record, Array, etc.).

let data = json_parse '{"name": "Ada", "langs": ["Rust", "Python"]}'
echo data.name       # Ada
echo data.langs[0]   # Rust

json_stringify

Serialize any value to a JSON string.

let rec = { x: 1, y: [2, 3] }
let s = json_stringify rec
echo s               # {"x":1,"y":[2,3]}

save_json / write_json

Write a value as formatted JSON to a file.

let config = { debug: true, port: 8080 }
save_json "config.json" config

Timing & Sleep

time

Measure execution time of an expression. Returns the elapsed time.

time (ls "." | where(fn(f) => f.size > 1000))
# Elapsed: 12ms

now / timestamp

Return the current Unix timestamp in milliseconds.

let start = now
# ... do work ...
let elapsed = now - start
echo "Took ${elapsed}ms"

sleep

Pause execution for a given number of milliseconds.

sleep 1000           # Sleep for 1 second

Shell Interop

sh / shell

Execute a raw shell command and return its output as a string.

let result = sh "git status --short"
echo result

# Capture structured output by parsing
sh "git branch" | split "\n" | map(fn(b) => trim b)

call

Call a function or builtin by name (as a string).

call "echo" "hello"
let op = "upper"
call op "hello"      # "HELLO"

exit

Exit the shell with an optional exit code.

exit           # Exit with code 0
exit 1         # Exit with code 1

Diagnostics

assert

Assert that a condition is true. Throws an error if false.

assert (2 + 2 == 4)            # passes
assert (len [1,2,3] == 3)      # passes
assert false                    # ERROR: assertion failed

type_assert / assert_type

Assert that a value has a specific type.

type_assert 42 "Int"            # passes
type_assert "hi" "String"       # passes
type_assert 42 "String"         # ERROR: expected String, got Int

is_error

Check whether a value is an Error.

let result = try { json_parse "invalid" } catch(e) { e }
echo (is_error result)          # true

echo (is_error 42)              # false

trace

Print a trace message with context, useful for debugging pipelines.

[1, 2, 3]
  | map(fn(x) => { trace "processing" x; x * 2 })
  | reduce(fn(a, b) => a + b, 0)

Membership

in

Test whether a value exists in an array or a key exists in a record.

echo (3 in [1, 2, 3])           # true
echo ("x" in { x: 1, y: 2 })   # true
echo (5 in [1, 2, 3])           # false

Configuration

config

Display the current shell configuration.

config                   # Show all config

config_get / config_set

Read or write individual configuration values.

config_get "theme"
config_set "theme" "dark"
config_set "editor" "vim"

config_path

Return the path to the configuration file.

echo (config_path)       # ~/.config/ironshell/config.toml

config_init

Create a default configuration file.

config_reload

Reload configuration from disk.

themes

List available shell themes.

themes
# ["dark", "light", "monokai", "solarized", ...]

Collections

Collection builtins are the backbone of IronShell’s pipeline-oriented design. They operate on Arrays, Records, and Tables, returning structured data for further processing.

Transforming

map

Apply a function to each element in an array, returning a new array.

[1, 2, 3] | map(fn(x) => x * 2)
# [2, 4, 6]

# With records
ls "." | map(fn(f) => { name: f.name, kb: f.size / 1024 })

each

Like map but intended for side effects. Returns the original array unchanged.

[1, 2, 3] | each(fn(x) => print "${x} ")
# Prints: 1 2 3
# Returns: [1, 2, 3]

Filtering

where

Keep only elements that satisfy a predicate.

[1, 2, 3, 4, 5] | where(fn(x) => x > 3)
# [4, 5]

ls "." | where(fn(f) => f.extension == "rs")

any

Return true if at least one element satisfies the predicate.

[1, 2, 3] | any(fn(x) => x > 2)   # true
[1, 2, 3] | any(fn(x) => x > 5)   # false

all

Return true if every element satisfies the predicate.

[2, 4, 6] | all(fn(x) => x % 2 == 0)   # true
[2, 4, 5] | all(fn(x) => x % 2 == 0)   # false

Reducing

reduce

Fold an array down to a single value with an accumulator.

[1, 2, 3, 4] | reduce(fn(acc, x) => acc + x, 0)
# 10

# Build a record from an array
["a", "b", "c"] | reduce(fn(acc, x) => { ...acc, [x]: true }, {})
# { a: true, b: true, c: true }

sum

Sum all numeric elements.

[1, 2, 3, 4] | sum        # 10
ls "." | map(fn(f) => f.size) | sum

avg / mean

Compute the arithmetic mean.

[10, 20, 30] | avg         # 20.0

product

Multiply all elements together.

[2, 3, 4] | product        # 24

min / max

Return the minimum or maximum value.

[3, 1, 4, 1, 5] | min      # 1
[3, 1, 4, 1, 5] | max      # 5

Selecting

first

Return the first element of an array.

[10, 20, 30] | first        # 10

last

Return the last element of an array.

[10, 20, 30] | last         # 30

take

Return the first N elements.

[1, 2, 3, 4, 5] | take 3   # [1, 2, 3]

slice

Extract a sub-array by start index and length.

[10, 20, 30, 40, 50] | slice 1 3
# [20, 30, 40]

Ordering

sort_by

Sort an array of records by a field, with optional direction.

ls "." | sort_by "size" "desc" | take 5
# Top 5 largest files

let people = [
  { name: "Charlie", age: 30 },
  { name: "Alice", age: 25 },
  { name: "Bob", age: 28 }
]
people | sort_by "name" "asc"

reverse

Reverse the order of elements.

[1, 2, 3] | reverse        # [3, 2, 1]

Combining

push

Append an element to an array.

[1, 2, 3] | push 4         # [1, 2, 3, 4]

concat

Concatenate two arrays.

concat [1, 2] [3, 4]       # [1, 2, 3, 4]

zip

Combine two arrays into an array of pairs.

zip ["a", "b", "c"] [1, 2, 3]
# [["a", 1], ["b", 2], ["c", 3]]

flatten

Flatten nested arrays by one level.

[[1, 2], [3, 4], [5]] | flatten
# [1, 2, 3, 4, 5]

Uniqueness

unique

Remove duplicate values from an array.

[1, 2, 2, 3, 3, 3] | unique    # [1, 2, 3]

Generators

range

Generate a sequence of integers.

range 1 5         # [1, 2, 3, 4]
range 0 10 2      # [0, 2, 4, 6, 8]  (with step)

Record Operations

keys

Return the keys of a record as an array.

keys { x: 1, y: 2, z: 3 }     # ["x", "y", "z"]

values

Return the values of a record as an array.

values { x: 1, y: 2, z: 3 }   # [1, 2, 3]

Size

len / length

Return the number of elements in an array, characters in a string, or keys in a record.

len [1, 2, 3]       # 3
len "hello"          # 5
len { a: 1, b: 2 }  # 2

Membership

in

Test if a value is in an array or a key is in a record.

3 in [1, 2, 3]                # true
"name" in { name: "Ada" }     # true

Pipeline Composition

Collections compose naturally through the pipe operator:

# Data analysis pipeline
ls "src"
  | where(fn(f) => f.extension == "rs")
  | map(fn(f) => { name: f.name, lines: len(split(cat(f.path), "\n")) })
  | sort_by "lines" "desc"
  | take 5

# Aggregation
range 1 100
  | where(fn(x) => x % 3 == 0 || x % 5 == 0)
  | sum
# 2318

# Nested transformation
[
  { dept: "eng", people: ["Alice", "Bob"] },
  { dept: "hr", people: ["Charlie"] }
]
  | map(fn(d) => d.people | map(fn(p) => { name: p, dept: d.dept }))
  | flatten

File System

File system builtins return structured data, making file operations composable in pipelines.

Reading

ls / list

List directory contents. Returns Array[Record] with file metadata.

ls "."
# [
#   { name: "main.rs", path: "./main.rs", size: 2048, modified: "...", is_dir: false, extension: "rs" },
#   { name: "src", path: "./src", size: 4096, modified: "...", is_dir: true, extension: "" },
#   ...
# ]

# Filter to Rust files over 1KB
ls "src" | where(fn(f) => f.extension == "rs" && f.size > 1024) | sort_by "size" "desc"

cat

Read the entire contents of a file as a string.

let content = cat "README.md"
echo content

# Use in pipelines
cat "data.csv" | split "\n" | take 5

read_text

Read a file as text (alias-like behavior to cat).

let cfg = read_text "config.toml"

Read the first N lines of a file.

head "log.txt" 10    # First 10 lines

tail

Read the last N lines of a file.

tail "log.txt" 20    # Last 20 lines

Searching

grep

Search for a pattern in file(s). Returns Array[Record] with match details.

grep "TODO" "src/"
# [
#   { file: "src/main.rs", line: 42, content: "// TODO: refactor this", match: "TODO" },
#   ...
# ]

# Count matches per file
grep "unwrap" "src/" | map(fn(m) => m.file) | unique | len

find

Find files matching criteria recursively.

find "." "*.rs"
# ["./src/main.rs", "./src/lib.rs", "./tests/eval.rs", ...]

wc

Word/line/character count. Returns a Record.

wc "README.md"
# { lines: 150, words: 892, chars: 5431 }

Text Processing (File-oriented)

sort

Sort lines of input alphabetically.

cat "names.txt" | sort

uniq

Remove adjacent duplicate lines (typically used after sort).

cat "words.txt" | sort | uniq

Writing

file_write / write_file

Write content to a file, creating it if needed, overwriting if it exists.

file_write "output.txt" "Hello, world!\n"

# Write data as JSON
let data = { name: "report", items: [1, 2, 3] }
file_write "data.json" (json_stringify data)

file_append / append_file

Append content to the end of a file. A file that does not exist is created, as >> does, so the first append starts it; its folder must already exist (mkdir makes one). The append is journaled like a write, so undo() removes a file it created.

file_append "log.txt" "New log entry\n"

file_insert / insert_lines

Insert content at a specific line number, or after or before a line that contains an anchor. An anchor no line contains is an E_NOT_FOUND error, and nothing is written.

file_insert "config.txt" 5 "new_setting = true"

Editing

file_replace / str_replace_in_file

Replace text in a file. Text the file does not contain is an E_NOT_FOUND error, and nothing is written.

file_replace "config.toml" "debug = false" "debug = true"

file_patch / patch_file

Apply a structured patch to a file.

file_patch "main.rs" [
  { line: 10, old: "let x = 1;", new: "let x = 2;" }
]

file_edit / edit_file

Perform line-based edits on a file.

file_edit "src/lib.rs" { delete_lines: [5, 6], insert: { 10: "// new comment" } }

file_delete_lines / delete_lines

Remove specific lines from a file.

file_delete_lines "output.txt" 3 5    # Remove lines 3-5
delete_lines("output.txt", "# DEPRECATED")               # Every line containing this literal text
delete_lines("output.txt", {regex: "^tmp[0-9]+$"})       # Every line matching a regular expression

The pattern, contains, matching and exact selectors match literal text. Use {regex: ...} for a regular expression; an invalid one is refused.

File Operations

file_copy / cp

Copy a file or directory.

file_copy "src/main.rs" "backup/main.rs"
cp "data/" "data_backup/"

file_move / mv

Move or rename a file or directory.

file_move "old_name.txt" "new_name.txt"
mv "temp/output.csv" "results/final.csv"

file_mkdir / mkdir

Create a directory (and parents if needed).

mkdir "output/reports/2024"

file_exists / exists

Check whether a file or directory exists.

if (file_exists "config.toml") {
  echo "Config found"
} else {
  echo "Using defaults"
}

file_diff

Compare two files and return their differences.

file_diff "v1.txt" "v2.txt"

file_backup

Create a timestamped backup copy of a file.

file_backup "important.db"
# Creates important.db.20240115_143022.bak

pwd

Return the current working directory as a string.

echo (pwd)     # /home/user/project

Extended File System

fs_stat / stat

Get detailed file metadata.

fs_stat "main.rs"
# { size: 2048, modified: "...", created: "...", permissions: "rw-r--r--", ... }

fs_glob / glob

Find files matching a glob pattern.

fs_glob "src/**/*.rs"
# ["src/main.rs", "src/lib.rs", "src/eval.rs", ...]

fs_tree / tree

Display directory structure as a tree.

fs_tree "src" 2    # Depth limit of 2

fs_du / du

Disk usage for a path.

fs_du "target"
# { total: 524288000, files: 1234 }

fs_df / df

Disk space information for mounted filesystems.

fs_df
# [{ mount: "/", total: 500000000000, used: 250000000000, available: 250000000000 }, ...]

fs_walk

Recursively walk a directory tree, returning all entries.

fs_walk "src" | where(fn(f) => f.extension == "rs") | len
# 15

Create a symbolic link.

fs_symlink "target_path" "link_path"

Read the target of a symbolic link.

fs_readlink "link_path"    # "/actual/target/path"

fs_realpath

Resolve a path to its absolute canonical form.

fs_realpath "../src/main.rs"
# "/home/user/project/src/main.rs"

fs_tempfile / fs_tempdir

Create temporary files or directories.

let tmp = fs_tempfile
file_write tmp "scratch data"

let tmpdir = fs_tempdir
# Use tmpdir for temporary work

fs_watch / fs_unwatch

Watch a path for filesystem changes.

fs_watch "src/" fn(event) => {
  echo "Changed: ${event.path} (${event.kind})"
}
# Later:
fs_unwatch "src/"

Pipeline Examples

# Find the 10 largest files recursively
fs_walk "."
  | where(fn(f) => !f.is_dir)
  | sort_by "size" "desc"
  | take 10
  | map(fn(f) => { name: f.name, mb: round(f.size / 1048576.0) })

# Batch rename files
ls "photos"
  | where(fn(f) => f.extension == "jpeg")
  | each(fn(f) => {
      let new_name = replace f.name ".jpeg" ".jpg"
      mv f.path "photos/${new_name}"
  })

# Disk usage report
ls "."
  | where(fn(f) => f.is_dir)
  | map(fn(d) => { dir: d.name, size: (fs_du d.path).total })
  | sort_by "size" "desc"

HTTP & Networking

IronShell provides comprehensive networking builtins for HTTP requests, web automation, and low-level network operations. All return structured data for pipeline processing.

HTTP Requests

http_get

Make an HTTP GET request. Returns a Record with status, headers, body, and ok fields.

let resp = http_get "https://api.github.com/repos/rust-lang/rust"
echo resp.status     # 200
echo resp.ok         # true

# Parse JSON response
let repo = json_parse resp.body
echo repo.stargazers_count

web_get / curl

Extended GET request with options support.

web_get "https://api.example.com/data" {
  headers: { "Authorization": "Bearer ${token}" },
  timeout: 5000
}

web_post

Make a POST request with a body.

let resp = web_post "https://api.example.com/items" {
  headers: { "Content-Type": "application/json" },
  body: json_stringify { name: "widget", price: 9.99 }
}

web_fetch / fetch

General-purpose HTTP client supporting all methods.

let resp = web_fetch "https://api.example.com/items/1" {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: json_stringify { name: "updated widget" }
}

JSON APIs

web_json_get

GET request that automatically parses the JSON response body.

let users = web_json_get "https://api.example.com/users"
users | where(fn(u) => u.active) | map(fn(u) => u.name)

web_json_post

POST request with automatic JSON serialization/deserialization.

let result = web_json_post "https://api.example.com/login" {
  username: "admin",
  password: "secret"
}
echo result.token

Web Scraping

web_scrape / scrape

Scrape content from a web page with CSS selectors.

let titles = web_scrape "https://news.ycombinator.com" "a.storylink"
titles | take 10 | each(fn(t) => echo t.text)

web_html_to_text

Extract plain text from HTML content.

let html = (http_get "https://example.com").body
let text = web_html_to_text html

web_html_to_markdown

Convert HTML content to Markdown.

let md = web_html_to_markdown (http_get "https://example.com").body
file_write "page.md" md

web_extract_emails

Extract email addresses from text or HTML.

let emails = web_extract_emails (cat "contacts.html")
echo emails   # ["user@example.com", ...]

web_extract_phones

Extract phone numbers from text.

let phones = web_extract_phones page_content

URL Operations

web_parse_url

Parse a URL into its components.

web_parse_url "https://example.com:8080/path?key=val#section"
# { scheme: "https", host: "example.com", port: 8080, path: "/path", query: "key=val", fragment: "section" }

web_encode_url / web_decode_url

URL-encode or decode a string.

web_encode_url "hello world & more"    # "hello%20world%20%26%20more"
web_decode_url "hello%20world"          # "hello world"

web_parse_query / web_build_query

Parse query strings to Records, or build query strings from Records.

web_parse_query "name=Ada&lang=Rust"
# { name: "Ada", lang: "Rust" }

web_build_query { page: 2, limit: 50 }
# "page=2&limit=50"

web_check_url

Check if a URL is reachable (returns status info).

let check = web_check_url "https://example.com"
echo check.reachable    # true
echo check.status       # 200

Downloads

web_download

Download a file from a URL.

web_download "https://example.com/data.csv" "downloads/data.csv"

web_open_url

Open a URL in the system default browser.

web_open_url "https://docs.ironshell.dev"

Advanced Web

web_rest_api

High-level REST API client with authentication and pagination support.

let client = web_rest_api "https://api.example.com" {
  auth: { type: "bearer", token: env.API_TOKEN },
  base_headers: { "Accept": "application/json" }
}

web_websocket

Connect to a WebSocket endpoint.

let ws = web_websocket "wss://stream.example.com/events"

web_graphql

Execute a GraphQL query.

let result = web_graphql "https://api.example.com/graphql" {
  query: "{ users { id name email } }",
  variables: { limit: 10 }
}

web_headers / web_cookies

Extract headers or cookies from a response.

let h = web_headers (http_get "https://example.com")
echo h["Content-Type"]

web_robots_txt / web_sitemap

Fetch and parse robots.txt or sitemap.xml.

let robots = web_robots_txt "https://example.com"
let sitemap = web_sitemap "https://example.com"

web_json_path / web_xpath

Query JSON with JSONPath or HTML with XPath expressions.

let data = web_json_get "https://api.example.com/data"
web_json_path data "$.users[*].name"

let html = (http_get "https://example.com").body
web_xpath html "//h1/text()"

Network Operations

net_interfaces / ifconfig

List network interfaces with their addresses and status.

net_interfaces
# [{ name: "eth0", ip: "192.168.1.100", mac: "aa:bb:cc:dd:ee:ff", up: true }, ...]

net_ip

Get the machine’s IP address.

echo (net_ip)    # 192.168.1.100

net_dns_lookup

Resolve a hostname to IP addresses.

net_dns_lookup "example.com"
# ["93.184.216.34"]

net_ping / ping

Ping a host and return latency information.

net_ping "google.com"
# { host: "google.com", latency_ms: 12.5, reachable: true }

net_ports

List open ports on the local machine.

net_ports | where(fn(p) => p.state == "LISTEN")

net_connections

List active network connections.

net_connections
  | where(fn(c) => c.remote_port == 443)
  | map(fn(c) => c.remote_addr)
  | unique

net_whois

WHOIS lookup for a domain.

net_whois "example.com"

net_traceroute

Trace the route to a host.

net_traceroute "example.com"
# [{ hop: 1, ip: "192.168.1.1", latency_ms: 1.2 }, ...]

net_stats / net_bandwidth

Network statistics and bandwidth usage.

net_stats
# [{ section: "Tcp", counter: "active connection openings", value: 1234 }, ...]

Pipeline Examples

# API data analysis
web_json_get "https://api.github.com/repos/rust-lang/rust/contributors?per_page=100"
  | map(fn(c) => { login: c.login, commits: c.contributions })
  | sort_by "commits" "desc"
  | take 10

# Health check multiple endpoints
let endpoints = ["https://api1.example.com/health", "https://api2.example.com/health"]
endpoints
  | map(fn(url) => {
      let check = web_check_url url
      { url: url, status: check.status, ok: check.reachable }
  })
  | where(fn(e) => !e.ok)

# Download and process CSV
web_download "https://data.example.com/export.csv" "/tmp/data.csv"
let rows = cat "/tmp/data.csv" | split "\n" | map(fn(line) => split line ",")
echo "Loaded ${len rows} rows"

String Operations

String builtins provide text manipulation, splitting, joining, and pattern matching. All return new values without modifying the original.

Case Conversion

upper

Convert a string to uppercase.

upper "hello"          # "HELLO"
"hello" | upper        # "HELLO"

lower

Convert a string to lowercase.

lower "HELLO"          # "hello"
"HELLO" | lower        # "hello"

Whitespace

trim

Remove leading and trailing whitespace.

trim "  hello  "       # "hello"
"  spaced  " | trim    # "spaced"

Splitting & Joining

split

Split a string by a delimiter, returning an array.

split "a,b,c" ","      # ["a", "b", "c"]
"one two three" | split " "   # ["one", "two", "three"]

# Split into lines
cat "log.txt" | split "\n"

# Split CSV row
"Alice,25,Engineer" | split "," 
# ["Alice", "25", "Engineer"]

join

Join an array of strings with a delimiter.

join ["a", "b", "c"] ","      # "a,b,c"
["hello", "world"] | join " " # "hello world"

# Reassemble after transformation
cat "data.csv"
  | split "\n"
  | where(fn(line) => contains line "ERROR")
  | join "\n"

Search & Match

contains

Check if a string contains a substring.

contains "hello world" "world"    # true
"hello world" | contains "xyz"    # false

# Filter lines
cat "log.txt" | split "\n" | where(fn(l) => contains l "ERROR")

starts_with

Check if a string starts with a prefix.

starts_with "hello" "hel"     # true
"README.md" | starts_with "READ"  # true

ends_with

Check if a string ends with a suffix.

ends_with "main.rs" ".rs"     # true
"photo.jpg" | ends_with ".png"    # false

Replacement

replace

Replace all occurrences of a substring.

replace "hello world" "world" "Rust"    # "hello Rust"
"foo-bar-baz" | replace "-" "_"          # "foo_bar_baz"

# Multi-step replacement
"Hello, World!"
  | replace "," ""
  | replace "!" ""
  | lower
# "hello world"

String in Pipelines

Strings integrate seamlessly with IronShell’s pipeline model. When a single-parameter lambda receives an array, it auto-maps:

# Uppercase every element
["hello", "world"] | upper
# ["HELLO", "WORLD"]

# Trim all strings
["  a  ", " b ", "c"] | trim
# ["a", "b", "c"]

Practical Examples

Log Parsing

cat "app.log"
  | split "\n"
  | where(fn(line) => contains line "ERROR")
  | map(fn(line) => {
      let parts = split line " "
      {
        timestamp: parts[0],
        level: parts[1],
        message: join (slice parts 2 (len parts)) " "
      }
  })
  | sort_by "timestamp" "desc"
  | take 10

CSV Processing

let lines = cat "users.csv" | split "\n"
let headers = split (first lines) ","
let rows = lines | slice 1 (len lines) | map(fn(line) => split line ",")

rows | map(fn(row) => {
  name: trim row[0],
  email: lower (trim row[1]),
  dept: upper (trim row[2])
})

Text Transformation

# Snake_case to camelCase
let snake = "my_variable_name"
let parts = split snake "_"
let camel = (first parts) + (parts | slice 1 (len parts) | map(fn(p) => {
  let chars = split p ""
  (upper (first chars)) + (join (slice chars 1 (len chars)) "")
}) | join "")
echo camel   # "myVariableName"

Batch File Renaming

ls "."
  | where(fn(f) => ends_with f.name ".txt")
  | each(fn(f) => {
      let new_name = replace f.name ".txt" ".md"
      mv f.path new_name
  })

Encoding (via ab_encode / ab_decode)

For base64 and hex encoding, see the crypto builtins (crypto_base64_encode, crypto_hex_encode), or use the general ab_encode / ab_decode:

ab_encode "hello" "base64"    # "aGVsbG8="
ab_decode "aGVsbG8=" "base64" # "hello"

Math Operations

Math builtins operate on numeric values (Int and Float). They handle type promotion automatically — operations between Int and Float produce Float results.

Rounding

floor

Round down to the nearest integer.

floor 3.7     # 3
floor(-2.3)   # -3
floor 5       # 5 (no-op for Int)

ceil

Round up to the nearest integer.

ceil 3.2      # 4
ceil(-2.7)    # -2
ceil 5        # 5 (no-op for Int)

round

Round to the nearest integer (half rounds up).

round 3.5     # 4
round 3.4     # 3
round(-2.5)   # -2

Absolute Value

abs

Return the absolute (non-negative) value.

abs(-42)      # 42
abs 3.14      # 3.14
abs(-99.9)    # 99.9

Powers & Roots

sqrt

Return the square root as a Float.

sqrt 16       # 4.0
sqrt 2        # 1.4142135623730951

pow

Raise a number to a power.

pow 2 10      # 1024
pow 3.0 0.5   # 1.7320508075688772 (same as sqrt 3)

Min / Max

min

Return the smaller of two values, or the minimum of an array.

min 3 7       # 3
[5, 2, 8, 1] | min   # 1

max

Return the larger of two values, or the maximum of an array.

max 3 7       # 7
[5, 2, 8, 1] | max   # 8

Aggregation

These also appear in Collections since they operate on arrays:

sum

Sum all numeric elements of an array.

[1, 2, 3, 4, 5] | sum     # 15
range 1 101 | sum           # 5050

avg / mean

Compute the arithmetic mean.

[10, 20, 30] | avg          # 20.0
[1.5, 2.5, 3.0] | mean      # 2.3333333333333335

product

Multiply all elements together.

[1, 2, 3, 4, 5] | product   # 120 (5!)
range 1 11 | product         # 3628800 (10!)

Arithmetic Operators

Standard arithmetic operators work on both Int and Float:

OperatorDescriptionExampleResult
+Addition3 + 47
-Subtraction10 - 37
*Multiplication6 * 742
/Division10 / 33
%Modulo10 % 31

Type promotion: When mixing Int and Float, the result is Float:

3 + 2.0       # 5.0 (Float)
10 / 3        # 3   (Int division)
10 / 3.0      # 3.3333... (Float division)

Comparison Operators

OperatorDescriptionExample
==Equal3 == 3
!=Not equal3 != 4
<Less than3 < 5
>Greater than5 > 3
<=Less or equal3 <= 3
>=Greater or equal5 >= 5

Practical Examples

Statistical Summary

let data = [23, 45, 12, 67, 34, 89, 11, 56]

let stats = {
  count: len data,
  sum: data | sum,
  min: data | min,
  max: data | max,
  avg: data | avg,
  range: (data | max) - (data | min)
}
echo stats
# { count: 8, sum: 337, min: 11, max: 89, avg: 42.125, range: 78 }

File Size Analysis

let sizes = ls "src" | map(fn(f) => f.size)

echo "Total: ${sum sizes} bytes"
echo "Average: ${round(avg sizes)} bytes"
echo "Largest: ${max sizes} bytes"
echo "Smallest: ${min sizes} bytes"

Fibonacci Sequence

range 0 10 | reduce(fn(acc, _) => {
  let n = len acc
  if n < 2 { push acc (n) }
  else { push acc (acc[n-1] + acc[n-2]) }
}, [])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Distance Calculation

let p1 = { x: 3.0, y: 4.0 }
let p2 = { x: 7.0, y: 1.0 }
let dist = sqrt(pow(p2.x - p1.x, 2) + pow(p2.y - p1.y, 2))
echo dist    # 5.0

Percentage Breakdown

ls "src"
  | where(fn(f) => !f.is_dir)
  | map(fn(f) => { name: f.name, size: f.size })
  | map(fn(f) => {
      let total = ls "src" | map(fn(g) => g.size) | sum
      { ...f, pct: round(f.size * 100.0 / total) }
  })
  | sort_by "pct" "desc"

Databases & SQL

IronShell can put a corpus into SQLite and query it with SQL, from inside the shell. That matters more than it sounds, and the measurement is in benches/agentic/.

Why: parsing per query is the cost

A pipeline that reads and parses a document on every invocation pays for that parse every time. On 500 GitHub issue records — 334 KB of JSON — answering ten questions costs:

RouteTotal tokensTotal time
cat(f) | from_json | where(…) per query448308 ms
sqlite_query against a prepared database38680 ms
bare sqlite3 outside the shell25852 ms

Same answers — all ten correct by every route. The difference is that SQLite reads an index and the pipeline re-parses the document. Nothing about the evaluator closes that gap; not re-parsing does.

This is also the configuration a published comparison of agent setups found best overall: a shell for exploration and SQL for the query, rather than either alone. Here it is one shell, so the typed output, the structured errors and the effect gate are still around the query.

Ingest, then query

Note the argument order: database first, then the source file, then the table. It reads backwards from the name.

db_json_to_sqlite("issues.db", "issues.json", "issues")
# true

sqlite_query("issues.db", "SELECT count(*) FROM issues WHERE is_pr=1")
# [{count(*): 293}]

db_csv_to_sqlite does the same from CSV. db_json_query and db_csv_query query those formats directly when a database would be overkill.

The result is ordinary typed data, so it composes with the rest of the language:

sqlite_query("issues.db", "SELECT user, count(*) n FROM issues GROUP BY user")
    | where(fn(r) => r.n > 10)
    | sort_by("n")
    | last(5)
    | len
# 4

Two things to know before relying on this

sqlite_query is not in the ontology. It works, and ontology_describe("sqlite_query") reports that it is not a known builtin, so an agent that discovers the surface by asking — which is what the ontology is for — will not find it. Use db_sqlite_* names, which are listed, or call sqlite_query knowing it is currently undiscoverable. This is a real gap, not a documentation convention.

The listed entries carry no signatures. ontology_describe( "db_json_to_sqlite") returns signature=db_json_to_sqlite() -> Value with an empty parameter list and no examples, which is why the argument order above is spelled out here. The same is true of much of the Database category.

Safety

sqlite_query goes through the same execution guard as any other builtin, so agent mode’s effect gate applies to it. A query against a database outside the workspace is not silently allowed: in agent mode it raises an approval request carrying the blast radius, rather than a flat refusal, because reading a database is a ReadLocal effect rather than a destructive one.

See Security & Auth.

AI Integration

IronShell has a built-in ai() function. It sends one prompt to the provider named by IRONSHELL_AI, and returns the reply as a string. For tool use, see agent in Creating Agents.

Quick Start

# Simple AI query
ai("What is the capital of France?")
# → "The capital of France is Paris."

Signature

ai(prompt, config?)  ->  String
  • prompt is a String. With piped input, the piped string is the prompt and the first argument is ignored.
  • config is an optional record. It accepts images, audio and video, each an array of readable file paths that are sent with the prompt. A model field is accepted and not used.
  • At most 30 calls per minute per process.

Choosing a Provider

ai() takes no model argument. The provider is read from the IRONSHELL_AI environment variable, and the call fails with “no AI provider is configured” when it is unset.

export IRONSHELL_AI=openai
export OPENAI_API_KEY=sk-...

Names the router accepts include openai, anthropic, ollama, vllm, llamacpp, tgi, lmstudio, compat, openrouter, groq, mistral, google, azure, and irongate. The full list is in complete_sync_router in src/ai.rs.

Context

ai() does not read a context field. To give the model a file, read it into a variable and put it in the prompt:

code = file.read("src/main.rs")
ai("Review this code for security issues: ${code}")

Multimodal

ai("What's in this image?", {images: ["photo.jpg"]})
ai("Transcribe this", {audio: ["meeting.mp3"]})
ai("Summarize this clip", {video: ["demo.mp4"]})

Not Supported

Temperature, max_tokens, system prompts, message history, JSON schema output, streaming and tool definitions are not parameters of ai(). A call that passes them gets no effect from them.

Error Handling

let answer = try { ai("Query that might fail") } catch err { "fallback response" }

Best Practices

  1. Set the provider with IRONSHELL_AI and its API key before starting ish.
  2. Keep prompts deliberate - put only the file text you need into the prompt.
  3. Handle errors - provider calls can fail; wrap them in try.

AI Providers

IronShell has built-in AI capabilities with a provider-agnostic architecture. Connect to cloud APIs, local models, or self-hosted inference servers — all using the same simple syntax.

Quick Start

# Set your API key
set_env "OPENAI_API_KEY" "sk-..."

# Ask a question
ai "What is Rust's ownership model?"

# Specify a model
ai "Explain monads" { model: "openai:gpt-4o" }

Model URI Scheme

IronShell uses a URI scheme to reference models across providers:

URIProviderExample
openai:model-nameOpenAIopenai:gpt-4o-mini
ollama:model-nameOllama (local)ollama:llama3
compat:model-nameOpenAI-compatible APIcompat:mixtral
tgi:model-nameHuggingFace TGItgi:mistral-7b
vllm:model-namevLLMvllm:meta-llama/Llama-3-8B
llamacpp:model-namellama.cppllamacpp:mistral-7b
# Use different providers
ai "Hello" { model: "openai:gpt-4o" }
ai "Hello" { model: "ollama:llama3" }
ai "Hello" { model: "compat:mixtral" }

Providers

OpenAI

The default cloud provider. Requires an API key.

set_env "OPENAI_API_KEY" "sk-..."
set_env "IRONSHELL_AI" "openai"

ai "Explain closures in Rust"

Environment variables:

  • OPENAI_API_KEY — API authentication key
  • OPENAI_MODEL — Default model (default: gpt-4o-mini)

Ollama (Local)

Run models locally with Ollama. No API key needed.

set_env "IRONSHELL_AI" "ollama"

ai "Summarize this code" { model: "ollama:codellama" }

Environment variables:

  • OLLAMA_URL — Ollama endpoint (default: http://localhost:11434)
  • OLLAMA_MODEL — Default model (default: llama3)

OpenAI-Compatible

Any server implementing the OpenAI API format (LiteLLM, LocalAI, etc.).

set_env "IRONSHELL_AI" "compat"
set_env "IRONSHELL_COMPAT_BASE" "http://localhost:8000/v1"

ai "Hello" { model: "compat:mixtral" }

Environment variables:

  • IRONSHELL_COMPAT_BASE — API base URL (default: http://localhost:8000/v1)
  • IRONSHELL_COMPAT_MODEL — Default model (default: mixtral)

HuggingFace TGI

Connect to a Text Generation Inference server.

set_env "IRONSHELL_AI" "tgi"
set_env "TGI_URL" "http://localhost:8080"

vLLM

Connect to a vLLM inference server.

set_env "VLLM_URL" "http://localhost:8000/v1"
set_env "VLLM_MODEL" "meta-llama/Llama-3-8B"

llama.cpp

Connect to a llama.cpp server.

set_env "LLAMACPP_URL" "http://localhost:8080/v1"

Provider Selection

The IRONSHELL_AI environment variable selects the default provider:

set_env "IRONSHELL_AI" "openai"    # Use OpenAI
set_env "IRONSHELL_AI" "ollama"    # Use Ollama
set_env "IRONSHELL_AI" "compat"    # Use OpenAI-compatible server
set_env "IRONSHELL_AI" "tgi"       # Use TGI

Override per-call with the model option:

# Default is OpenAI, but use Ollama for this one call
ai "Quick question" { model: "ollama:llama3" }

Multimodal AI

IronShell supports sending images, audio, and video to models that accept them.

Images

ai "Describe this image" { images: ["photo.jpg"] }
ai "Compare these" { images: ["before.png", "after.png"] }

Audio

ai "Transcribe this recording" { audio: ["meeting.mp3"] }

Video

ai "What happens in this clip?" { video: ["demo.mp4"] }

Combined

ai "Analyze this screenshot and narration" {
  images: ["screen.png"],
  audio: ["narration.mp3"]
}

Note: Multimodal support depends on the provider. OpenAI supports images; Ollama supports images with vision models. Audio and video support varies by model.

Backend Detection

Discover which AI backends are available on your system:

ai_backends
# [
#   { provider: "openai", available: true, model: "gpt-4o-mini" },
#   { provider: "ollama", available: true, url: "http://localhost:11434", models: ["llama3", "codellama"] },
#   { provider: "vllm", available: false },
#   ...
# ]

Shell Helpers

Four helpers answer from the shell’s own catalogue. No model is consulted (use ai(...) for that), so they work offline and give the same answer every time.

# Declared builtins whose name or description matches every word
suggest("sha256 digest")
# [{name: "sha256", signature: "sha256(...) -> String", doc: "SHA-256 hex digest of a string."}]

# A builtin's declared signature, or what an error code means
explain("crypto_hash")

# Builtin names starting with a prefix
complete("git_st")
# ["git_stash", "git_stash_list", "git_stash_pop", "git_status"]

# The minimal repair context for an error, from its text or a caught record
try { crypto_hash("x", "blake3") } catch e { fix(e) }

Pipeline Integration

AI calls compose naturally with pipelines:

# Summarize a file
cat "README.md" | ai "Summarize this document"

# Classify data
["bug report", "feature request", "question"]
  | map(fn(item) => {
      let category = ai "Classify: ${item}" { model: "openai:gpt-4o-mini" }
      { text: item, category: category }
  })

# Generate documentation
ls "src" 
  | where(fn(f) => f.extension == "rs")
  | map(fn(f) => { file: f.name, doc: ai "Write a one-line description of: ${cat f.path}" })

Global Override

Set a global model URI that overrides all defaults:

set_env "IRONSHELL_MODEL_URI" "ollama:codellama"
# All ai/agent calls now use this model unless explicitly overridden

Creating Agents

An agent is a model in a ReAct loop: it is given a goal and a set of tools, emits a JSON tool call, sees the result, and repeats until it emits a final answer or runs out of steps. agent runs that loop and returns the final answer as a string.

Prerequisites

An agent needs a provider. Without one, every call fails immediately and says so:

agent("list the files here")
# error[E_UNKNOWN]: No AI provider configured.
# Set IRONSHELL_AI environment variable to: irongate, openai, ollama, or compat

See Configuring Providers.

Running an agent

agent(<goal>, [tools...], [max_steps], [dry_run])

Only the goal is required. It is a task, not a persona — the system prompt is supplied by the loop, and the goal is the user turn.

agent("Find the three largest files under src/")

Tools are named individually or as an array. They are ordinary builtins:

agent("Find every .log file over 1 MB", "ls", "find", "stat")

agent("Summarise what this project builds", ["cat", "ls", "grep"])

After the tools come two optional positional arguments: an integer step limit (default 8) and a boolean dry-run flag.

# Twenty steps, and do not actually execute the tool calls.
agent("Reorganise the test fixtures", ["ls", "mv"], 20, true)

The record form

The same call can be written as a record, which is easier to build programmatically. Exactly four keys are read — goal, tools, max_steps and dry_run — and anything else is ignored:

agent({
    goal: "Summarise the open TODOs",
    tools: ["grep", "cat"],
    max_steps: 12,
    dry_run: false
})

A record with no goal is refused with agent config requires {goal: String}.

Each call starts fresh

agent builds a new dialogue every time — a system prompt and your goal — and returns a string. There is no session, no conversation history, and no reset builtin, so a second call knows nothing about the first. To carry context forward, put it in the next goal:

let plan = agent("Break down building a CLI todo app into steps")
let code = agent("Implement this plan: " + plan, ["write", "cat"])

This is also how multiple agents are composed; see Agent Swarms for the coordinated form.

Which tools an agent may run

Naming a tool in the call does not by itself permit it. Shell-command execution is default-deny: with AGENT_ALLOW_CMDS unset, no command is allowed, and the refusal says exactly that.

export AGENT_ALLOW_CMDS=ls,cat,grep,git

The list is read once, when the security configuration is first built, so export it before starting ish. Setting it from inside a running shell with env_set or set_env takes effect only if no command has been validated yet.

Anything outside the list is refused by name, and both the allowed and the refused attempts are written to the security audit log.

MCP tools

agent_with_mcp takes a goal and an array of tool names. Each name is a builtin: an mcp: prefix is stripped, and a name the dispatcher does not serve is E_NOT_FOUND. No MCP server is contacted. Passing an endpoint argument is refused with E_UNIMPLEMENTED, and the run is refused if it ends without a final answer:

agent_with_mcp("Check the deployment status", ["k8s_pods", "k8s_logs"])

Rate limiting

agent is capped at 10 calls per minute per process. Exceeding it fails with Agent rate limit exceeded rather than queuing.

Security

An agent runs real commands. Beyond AGENT_ALLOW_CMDS:

  • ish --agent puts the shell in default-deny mode, gating destructive effect classes behind approval.
  • ish --workspace <dir> confines writes and destructive operations to that directory.
  • dry_run lets you watch the loop’s intent without executing it.

See Security & Auth.

Agent Swarms

What swarm currently does

swarm is a builtin, and it works, but not the way its name suggests: it takes the same arguments as agent and delegates to the same single-agent loop. ai::agents::swarm::run_sync is one line — it calls ai::agents::run_sync.

swarm("Analyze this project thoroughly", ["ls", "cat", "grep"], 12)

That runs one agent with those three tools and a twelve-step limit. It does not create multiple agents.

The multi-agent engine below exists in the library and is not reachable from the shell. Swarm, its blackboard and its two coordinators are defined in src/ai.rs and never constructed by any builtin. They are documented here so the distinction is on the record, not because you can call them today.

  • Coordination policies. RoundRobin takes agents in turn; Router sends each turn to the agent whose declared capabilities best match. Both coordinators are implemented; neither is selectable from the shell, because the swarm builtin reads only goal, tools, max_steps and dry_run.
  • Blackboard. Agents would share a message list with kinds note, thought and final, and could delegate with {"type": "delegate", "target": "...", "input": "..."}.

If you want several agents today, chain calls and pass each result into the next goal — see Creating Agents.

Model override

Both builtins honour an environment variable for the model URI:

set_env("IRONSHELL_AGENT_MODEL_URI", "openai:gpt-4o")
set_env("IRONSHELL_SWARM_AGENT_MODEL_URI", "ollama:llama3")

See Workflows for the orchestration surface, which is registered and runnable.

There is no model key in the record form. agent/swarm read exactly four keys — goal, tools, max_steps, dry_run — and silently ignore the rest, so a model: entry has no effect.

Choosing tools

Tools are the builtins the agent may call. Name them individually, or as an array:

agent("Find large files", ["ls", "cat", "grep"])

An empty array gives the agent no tools at all, not every tool. Tool names are resolved one by one against the registry, so an empty list resolves to an empty toolset and the agent can only answer from the model.

agent("Analyze the project", [])   # no tools; the model answers unaided

A name that is not a builtin is not rejected either: the resolver wraps any string it is given, so a typo — or a model URI passed where the tool list belongs – is accepted here and only fails when the agent tries to call it.

Agent security

Command allowlist

AGENT_ALLOW_CMDS restricts which shell commands an agent may run, and it is default-deny: unset, nothing is allowed, and the refusal says so.

export AGENT_ALLOW_CMDS=ls,cat,grep,wc

The list is read once, when the security configuration is first built. Setting it from inside a running shell with set_env only takes effect if no command has been validated yet, so prefer exporting it before starting ish.

Measured limits

  • Prompt validation — goals are capped at 4,000 characters and at most 50 newlines, rejected if empty or containing a null byte, and screened for injection.
  • Argument validation — shell metacharacters are rejected in tool arguments.
  • Rate limiting — 10 agent calls per minute, and within the agent API, 10 plans and 5 executions per minute.
  • Output cap — 10 MB per execution.
  • Timeout — 30 seconds per execution, on every platform.
  • Memory — 512 MB, on Linux and macOS only. The Windows configure_sandbox is a documented no-op: Job Object sandboxing is a TODO, so on Windows you get the timeout and the output cap and nothing else.
  • No shell escape — the sh builtin is gated behind IRONSHELL_ALLOW_SH=true and is unavailable by default.

Agents with MCP tools

agent_with_mcp takes a goal and an array of tool names. The tools are builtins (an mcp: prefix is stripped), and no MCP server is contacted. An endpoint argument is refused with E_UNIMPLEMENTED, so the call below is refused as written:

agent_with_mcp("Analyze repository", ["ls", "cat"])                     # works
agent_with_mcp("Analyze repository", ["ls", "cat"], "http://localhost:9090")  # E_UNIMPLEMENTED

The agent can call only the named builtins during its loop, and each call passes the same dispatch guard as any other builtin call. Note that mcp_server_start takes a configuration record, not a URL string.

Practical examples

Code review

agent({
  goal: "Review src/main.rs for potential bugs, style issues, and missing error handling",
  tools: ["cat", "grep", "wc"],
  max_steps: 8
})

Project analysis

agent({
  goal: "Describe this Rust project: structure, dependencies, and test coverage",
  tools: ["ls", "cat", "grep", "find", "wc", "fs_tree"],
  max_steps: 25
})

Git summary

export AGENT_ALLOW_CMDS=ls,cat,grep,git
agent({
  goal: "Summarize all changes since the last release tag",
  tools: ["git_log", "git_diff", "cat"],
  max_steps: 10
})

Tool Use

An agent is only as useful as the things it can actually run. IronShell ships a catalogue of external OS tools — grep, curl, git, and so on — each with a description, a parameter list, the platforms it runs on, and a safety level. The same catalogue backs both the agent loop and the tool_* builtins below, so what an agent can reach is exactly what you can inspect from the prompt.

Listing the catalogue

tool_list() | len
# 198

tool_list() | first
# {category: TextProcessing, command: grep, description: Search text patterns in files,
#  name: grep, requires_admin: false, safety: Safe, supported_os: [len=5]}

Finding a tool

tool_search takes a query and returns the tools recommended for it, falling back to a plain name and description match when nothing is recommended:

tool_search("http") | len
# 7

tool_info returns the full record for one tool, including its parameters and worked examples:

tool_info("ls")
# {category: FileSystem, command: ls, common_args: [len=3],
#  description: List directory contents, examples: [len=1], name: ls,
#  parameters: [len=3], requires_admin: false, safety_level: Safe,
#  supported_os: [len=5]}

Schemas for model tool-calling

tool_schema renders the catalogue as OpenAI-style function schemas, ready to hand to a model that supports tool calling:

tool_schema() | first
# {function: {…}, type: function}

Running a tool

tool_exec(<name>, [args], [allow_dangerous])

tool_execute is an alias for the same builtin. Only the name is required; args may be an array of strings or a single string.

tool_exec("git", ["status", "--short"])

Two things can stop a call, and both report rather than guess:

  • Platform. A tool is only run where it is supported. On Windows, tool_exec("ls", ["docs/book"]) returns error[E_UNKNOWN]: Tool execution failed: Tool 'ls' is not supported on Windows instead of falling back to something that merely looks similar.

  • Safety. Every tool carries a level — Safe, Caution, Dangerous, or Critical. Of the 198 catalogued tools, 131 are Safe, 49 are Caution, 14 are Dangerous and 4 are Critical:

    tool_list() | where(fn(t) => t.safety == "Dangerous") | len
    # 14
    

    Safe and Caution run normally. Dangerous and Critical are refused outright unless the third argument is true, and the refusal names the level rather than failing vaguely:

    tool_exec("iptables", ["-L"])
    # error[E_UNKNOWN]: Tool execution failed: Tool 'iptables' has safety level
    # Critical. Set allow_dangerous=true to execute.
    

    A tool marked requires_admin is additionally refused unless the process is actually privileged.

Over MCP

ish mcp stdio serves the shell as an MCP server. tools/list returns three tools, not one per builtin:

ToolPurpose
ontology_manifestthe categories, with counts and effect classes
ontology_describeexpand a category into its builtins, or one builtin into full detail
ironshellinvoke a builtin by name

That is deliberate. Advertising several hundred tool schemas would spend an agent’s context before it had read a single result; the manifest is the compact index and detail is fetched for the slice actually needed. Every builtin is reachable through ironshell, and every call goes through the same safety model as one typed at the prompt.

tool_exec also passes through the ordinary execution guard, so agent mode’s effect gate and the workspace jail apply to it exactly as they do to any other process-spawning builtin. See Security & Auth.

Workflows

A workflow is a template of steps plus an instance that runs it. Steps are records, a step’s run names a builtin, and the engine passes data between them through named variables.

This chapter was removed from the book once, because the sixteen workflow_* names existed in the source and none of them were registered: workflow_create at the prompt answered unknown builtin. Everything below was run against the shell before being written down.

A first workflow

let t = workflow_pipeline("demo", [
  {id: "up",    run: "upper", args: ["hello"], output: "$.shouted"},
  {id: "count", run: "len",   input: "$.shouted"}
])

let w = workflow_create(t, {input: null})
workflow_execute(w)
# 5

workflow_pipeline returns a template id; workflow_create returns a workflow id; workflow_execute runs it and returns the last step’s value. The pipeline template declares input as a required parameter, which is why the record is passed even when the value is unused.

Steps

A step is a record. Exactly one key decides what kind of step it is:

KeyStepExample
runcall a builtin{run: "grep", args: ["TODO", "src"]}
agentrun an agent{agent: "reviewer", prompt: "$.task"}
urlmake an HTTP request{url: "https://example.com", method: "POST", body: {...}}
delay_mswait (at most one hour){delay_ms: 500}
emitemit an event{emit: "ready", payload: 7}
waitwait for an event (at most one hour; no timeout_ms means one hour){wait: "ready", timeout_ms: 2000}
workflowrun another template{workflow: "<template id>"}

Any step also takes:

KeyMeaning
idits name in results and events (defaults to step-N)
inputthe variable to feed it, as $.name
outputwhere to store its result, as $.name
whena condition; the step is skipped unless it holds
timeout_mshow long it may run before failing (at most one hour)
retrieshow many times to retry, with exponential backoff
compensatethe step to run if a later saga step fails

input/output are how stages connect. Above, up stores "HELLO" in shouted and count reads it.

Conditions

when is IronShell source, evaluated against the workflow’s variables:

let t = workflow_pipeline("guarded", [
  {id: "big", run: "upper", args: ["over"], when: "threshold > 5"}
])

A condition that does not hold skips the step; one that fails to parse is treated as not holding, because a guard you cannot read must not count as satisfied.

Patterns

workflow_pipeline(name, steps)                    # sequential
workflow_map_reduce(name, mapper, reducer)        # parallel map, then reduce
workflow_fan_out(name, workers, aggregator)       # parallel workers, then fan-in
workflow_scatter_gather(name, targets, strategy)  # scatter to agents, gather
workflow_saga(name, transactions)                 # with compensation on failure
workflow_register({name: ..., steps: [...]})      # a plain sequence

Fan-out runs its workers concurrently and hands their results to the aggregator:

let t = workflow_fan_out("scan",
  [{id: "a", run: "upper", args: ["a"]},
   {id: "b", run: "upper", args: ["b"]}],
  {id: "agg", run: "len"})

workflow_execute(workflow_create(t, {input: null}))
# 2

workflow_scatter_gather’s third argument is a strategy, not a step: "all", "first", {first: 3}, {timeout_ms: 5000} or {consensus: 0.75}.

Sagas

A saga takes [action, compensation] pairs. If a later action fails, the compensations for the actions that already succeeded run in reverse order:

let t = workflow_saga("order", [
  [{id: "charge", run: "upper", args: ["charged"]},
   {id: "refund", run: "upper", args: ["refunded"], output: "$.undone"}],
  [{id: "ship",   run: "nope_not_real"},
   {id: "unship", run: "upper", args: ["unshipped"]}]
])

workflow_execute(workflow_create(t, {input: null}))
# error[E_UNKNOWN]: Saga failed at step saga-step-1: unknown builtin: nope_not_real

The refund ran; $.undone holds "REFUNDED".

Inspecting a run

workflow_status(w)
# {id: …, template: …, status: completed, steps_completed: 1, variables: {…}}

workflow_status(w).variables.out
workflow_list()
workflow_templates()

workflow_cancel, workflow_pause and workflow_resume take a workflow id.

  • workflow_cancel stops a run that is in progress. It takes effect at the next step boundary or within about 100 ms during a wait; the run ends cancelled and a step that has not started does not start. Cancelling a run that has already completed or failed is refused, and cancelling a cancelled run is a no-op. A cancelled saga does not run its compensations: cancel stops the run, it does not roll it back.
  • workflow_pause holds a running workflow at its next step boundary. A delay_ms, a retry backoff or a wait already running is held too: it does not end while the run is paused, and the paused time does not count toward it, so an event wait does not time out during a pause. Any other step already running finishes first.
  • workflow_resume lets a paused workflow continue.

No wait may last longer than one hour (3600000 ms). A template with a longer delay_ms, timeout_ms or wait timeout is refused by workflow_execute before any step runs; the error names the limit.

Circuit breakers

circuit_breaker_create("payments", {failure_threshold: 3})
circuit_breaker_status("payments")
# closed

States are closed, open and half-open. The optional record also takes success_threshold and reset_timeout_ms.

Safety

A workflow is not a way around the shell’s gates.

  • Every run step goes through the same dispatcher as a call typed at the prompt, so the effect gate, the workspace jail and the audit chain all apply to it.
  • An agent step goes through the agent builtin, so AGENT_ALLOW_CMDS and the rate limit still apply.
  • A url step goes through the same egress allowlist and SSRF validation as http_get.
  • In agent mode, workflow_execute and workflow_create require approval — running a workflow is the same capability as running the commands inside it. workflow_templates and the other read-only calls do not.
  • A workflow step that reaches its own template is refused after 8 levels rather than recursing until the stack ends.

Limits

  • Templates and instances live in the shell process. They are not persisted, so they are gone when it exits.
  • Workflow builtins cannot be called from inside a single-threaded async context; blocking it would deadlock, so they report instead. Use a workflow step to nest one workflow inside another.
  • Choreography runs its steps sequentially and emits events; there is no event-driven scheduler behind it.

TUI Overview

IronShell’s Terminal User Interface (TUI) provides a rich, multi-pane interface for AI chat, agent management, media browsing, and more — all within your terminal.

Launching the TUI

ish tui              # standard launch
RUST_LOG=debug ish tui   # with debug logging

Interface Layout

The TUI has three main areas:

┌──────────────────────────────────────────────────────┐
│  Chat │ Agents │ Media │ Settings │ Distributed │ …  │  ← Tabs
├──────────────────────────────────────────────────────┤
│                                                      │
│                   Main Content                       │  ← Mode-specific
│                                                      │
├──────────────────────────────────────────────────────┤
│  > Type a message...                    │ Help: ?    │  ← Input + Help
└──────────────────────────────────────────────────────┘
  1. Header — Tab bar showing the current mode
  2. Main Content — Changes based on the active tab
  3. Footer — Text input (70%) and help hints (30%)

Tabs / Modes

Switch between modes using Tab / Shift+Tab or number keys 1-6:

#TabDescription
1ChatAI conversation with multimodal support
2AgentsCreate and manage AI agent swarms
3MediaBrowse and select images, audio, video
4SettingsConfigure model, preferences
5DistributedManage distributed agent networks
6ReasoningAdvanced reasoning chains and knowledge

Input Modes

The TUI operates in two input modes:

Normal Mode

Key presses are interpreted as navigation commands. Use arrow keys, j/k for movement, Tab to switch tabs, q to quit.

Editing Mode

Key presses go to the text input field. Press Enter or i to enter Editing mode, Esc to return to Normal mode.

The current mode is indicated in the input box border style.

Configuration

The TUI reads configuration from environment variables and defaults:

SettingDefaultDescription
Model$IRONSHELL_AIDefault AI model for chat
Max messages1,000Message history limit
Auto-scrollOnScroll to latest message
TimestampsOnShow message timestamps
Media previewOnEnable in-terminal image preview
Agent update interval1,000msAgent status refresh rate

Quick Start

  1. Set your AI provider:

    export IRONSHELL_AI=openai
    export OPENAI_API_KEY=sk-...
    
  2. Launch the TUI:

    ish tui
    
  3. Press Enter to start typing, write your message, press Enter to send

  4. Press Tab to explore other modes (Agents, Media, etc.)

  5. Press q or Ctrl+C to exit

Navigation

The TUI uses a modal key binding system with Normal and Editing modes.

Global Keys (Normal Mode)

These work from any tab:

KeyAction
q / Esc / Ctrl+C / Ctrl+QQuit
TabNext tab (cycles through all modes)
Shift+TabPrevious tab
1-6Jump to specific tab
↑ / kMove selection up (wraps around)
↓ / jMove selection down (wraps around)

Chat Mode

Normal Mode

KeyAction
Enter / iEnter Editing mode
cClear conversation
mSwitch to Media tab
aSwitch to Agents tab
Ctrl+EExport conversation to Markdown
Ctrl+JExport conversation to JSON
Ctrl+LClear conversation
Ctrl+FOpen Search

Editing Mode

KeyAction
EnterSend message
EscCancel, return to Normal mode
Arrow keysNavigate within text input
Any keyTypes into the input field

Agent Swarm Mode

Normal Mode

KeyAction
nCreate new agent
d / DeleteRemove selected agent
Enter / sEnter Editing mode (type task)
mView agent metrics
rRestart selected agent
cSwitch to Chat

Media Browser Mode

Normal Mode

KeyAction
Space / EnterToggle file selection
oOpen/add file
cClear all selections
d / DeleteRemove file from library
bReturn to Chat with selected media

Search Mode

Normal Mode

KeyAction
i / /Enter search Editing mode
↓ / jNext search result
↑ / kPrevious search result
EscClear search, return to Chat
Ctrl+CCopy selected result

Distributed Agents Mode

KeyAction
sStart distributed swarm
dStop distributed swarm
rRefresh network status
tTest connection

Advanced Reasoning Mode

KeyAction
nNew reasoning session
pView planning goals
kBrowse knowledge base
eExport reasoning chains
iImport knowledge
  • Use j/k (vim-style) or arrow keys to scroll through lists
  • List selection wraps around — pressing ↑ at the top jumps to the bottom
  • Number keys (1-6) provide the fastest way to switch tabs
  • Esc always returns to Normal mode from Editing mode
  • In Chat mode, Ctrl+F enters Search for finding messages in history

Chat Interface

The Chat tab is the primary AI interaction mode — a conversational interface with multimodal support, message history, and export capabilities.

Sending Messages

  1. Press Enter or i to enter Editing mode
  2. Type your message
  3. Press Enter to send

The TUI sends your message to the configured AI model and displays the response.

Message Types

Messages are color-coded by role:

RoleColorEmojiDescription
UserCyan👤Your messages
AssistantGreen🤖AI responses
SystemYellow⚙️Status and system messages

Each message displays:

[14:32:05] 👤 [gpt-4o-mini] How does ownership work in Rust?
[14:32:08] 🤖 [gpt-4o-mini] Ownership is one of Rust's key features...

Media Attachments

Attach media files to your messages for multimodal AI interaction:

  1. Switch to the Media tab (m or 3)
  2. Select files with Space or Enter
  3. Press b to return to Chat with files attached

Attached media appears with a 📎 prefix in the sidebar. When you send a message, the selected media is included as context for the AI.

Chat Layout

The chat area is split into two panels:

┌──────────────────────────┬────────────────┐
│                          │ 📎 Media       │
│   Message History        │ 🤖 Agents      │
│                          │ 📊 Stats       │
│                          │ • Total: 24    │
│                          │ • User: 12     │
│                          │ • Chars: 8,432 │
└──────────────────────────┴────────────────┘
  • Left (70%) — Scrollable message history
  • Right (30%) — Sidebar with attached media, active agents, and conversation statistics

Conversation Statistics

The sidebar shows real-time stats:

  • Total messages — Count of all messages
  • User / Assistant / System — Breakdown by role
  • Total characters — Sum of all message content
  • Average length — Mean characters per message
  • Media attachments — Count of attached files
  • Active agents — Number of running agents

Chat Sessions

The TUI supports multiple chat sessions through the ChatManager:

Session Settings

SettingDefaultDescription
auto_summarizefalseAuto-summarize when context grows too large
context_window_size4,096Max tokens in context window
enable_media_analysistrueAnalyze media files with vision models
temperature0.7AI response temperature
max_tokensNoneMax tokens per response
system_promptNoneCustom system prompt

Auto-Summarization

When enabled and the conversation exceeds the context window size, older messages are automatically summarized into a single system message, preserving recent context.

Search through your conversation history:

  1. Press Ctrl+F to enter Search mode
  2. Type your search query
  3. Press Enter to search
  4. Use ↑/↓ to navigate results
  5. Press Esc to return to Chat

Search is case-insensitive and matches against message content.

Export

Export your conversation for sharing or archival:

ShortcutFormatFile
Ctrl+EMarkdownconversation_export.md
Ctrl+JJSONconversation_export.json

Markdown Export

# IronShell Conversation Export

**Model:** gpt-4o-mini
**Messages:** 24
**Exported:** 2024-01-15T14:32:05Z

---

**👤 User** *14:30:00 [gpt-4o-mini]*
How does ownership work?

**🤖 Assistant** *14:30:03 [gpt-4o-mini]*
Ownership is a set of rules that govern...

JSON Export

{
  "exported_at": "2024-01-15T14:32:05Z",
  "model": "gpt-4o-mini",
  "messages": [
    {
      "role": "user",
      "content": "How does ownership work?",
      "timestamp": "2024-01-15T14:30:00Z"
    }
  ]
}

Clear History

  • Press c (Normal mode) to clear all messages
  • Press Ctrl+L to clear the conversation
  • This removes all messages from the current session

Tips

  • If no AI model is configured, the TUI shows a warning with setup instructions
  • Messages are limited by max_messages (default 1,000) — oldest messages are dropped
  • Auto-scroll keeps the latest messages visible; scroll up manually to review history
  • The model name appears in each message header so you can see which model responded

Multimodal Support

The TUI recognises image, video and audio files, classifies them by extension, and carries the reference into the conversation so a multimodal model can be asked about them.

It does not render them. Inline image display is intended, not present: the source contains no kitty, iterm or sixel support. What follows describes attaching and referencing files, not viewing them in the terminal. See TUI Guide.

Supported Formats

CategoryExtensions
Imagejpg, jpeg, png, gif, bmp, webp, tiff, svg
Videomp4, avi, mov, mkv, wmv, flv, webm, m4v
Audiomp3, wav, flac, aac, ogg, wma, m4a

Media Browser

Access the Media Browser from the Chat tab by pressing m or switching to tab 3.

Layout

┌────────── Files ──────────┬──────── Preview ────────┐
│  photo.jpg                │  Path: ./photo.jpg      │
│  diagram.png          ✓   │  Type: Image            │
│  recording.mp3            │  Size: 1920×1080        │
│  demo.mp4                 │                         │
│                           │  [Image Preview]        │
└───────────────────────────┴─────────────────────────┘
  • Left panel: File list with ✓ markers for selected files
  • Right panel: Metadata and preview for the highlighted file

Controls

KeyAction
Space / EnterToggle file selection
↑ / ↓ / j / kNavigate file list
oOpen/add file
d / DeleteRemove file from library
cClear all selections
bReturn to Chat with selected files attached

Image Preview

Images are rendered directly in the terminal using Unicode block characters. The preview adapts to your terminal size and supports:

  • Inline rendering: Images displayed within the TUI layout
  • Automatic thumbnailing: 64×64 pixel thumbnails for the file list
  • Full preview: Larger rendering in the preview panel

Tip: Image quality depends on your terminal emulator. Modern terminals like iTerm2, Kitty, and WezTerm provide the best results.

Attaching Media to Chat

To send images or other media with your chat message:

  1. Press m to open Media Browser
  2. Select files with Space (multiple selections allowed)
  3. Press b to return to Chat
  4. Type your message and press Enter

The selected media is sent as context to the AI model:

📎 Attached: photo.jpg, diagram.png

👤 What differences do you see between these two images?
🤖 The first image shows... while the second...

Media Analysis

When enable_media_analysis is enabled (default), the TUI automatically analyzes attached media:

  • Images: Sent to a vision-capable model for description
  • Audio: Duration and format metadata extracted
  • Video: Duration and format metadata extracted

Analysis results appear as annotations in the conversation:

[Media Analysis: The image shows a flowchart diagram with 5 nodes connected by arrows...]

Multimodal Agents

Agents in the TUI support different modalities:

Supported modalities:
• Text ✓
• Image ✓ (requires vision model)
• Audio ✓ (model-dependent)
• Video ✓ (model-dependent)

Each agent declares which modalities it supports. When you assign a task involving media, the system validates that the selected agent can handle the required modality.

Display Info

Files show contextual information in the browser:

TypeDisplay Format
Image🖼️ 1920×1080 - photo.jpg
Video🎬 demo.mp4 (30.5s)
Audio🎵 recording.mp3 (120.0s)
Unknown❓ file.xyz

Terminal Compatibility

Image rendering quality varies by terminal:

TerminalImage Support
iTerm2Excellent (native image protocol)
KittyExcellent (native image protocol)
WezTermGood (Sixel graphics)
Windows TerminalBasic (Unicode blocks)
VS Code TerminalBasic (Unicode blocks)
Standard terminalsBasic (Unicode blocks/ASCII art)

For the best multimodal experience, use a terminal that supports inline image protocols.

TUI Guide

IronShell’s Terminal User Interface (TUI) provides a rich, interactive environment for working with AI, viewing multimodal content, and managing agents.

Starting TUI Mode

# Start in TUI mode
ish tui

# Or from the REPL
tui()

Interface Overview

┌─────────────────────────────────────────────────────────────┐
│  IronShell TUI                                    [Agents]│
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  User: Explain the concept of monads                        │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ AI: A monad is a design pattern used in functional      ││
│  │ programming to handle computations with context...       ││
│  │                                                          ││
│  │ Think of it as a wrapper that:                          ││
│  │ 1. Contains a value                                      ││
│  │ 2. Has a way to wrap values (return/unit)               ││
│  │ 3. Has a way to chain operations (bind/flatMap)         ││
│  └─────────────────────────────────────────────────────────┘│
│                                                             │
│  User: Show me an example in Rust                           │
│  ...                                                        │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│ > Enter message...                                     [?]  │
└─────────────────────────────────────────────────────────────┘

Key Bindings

KeyAction
EnterSend message
Ctrl+CCancel/Exit
Ctrl+LClear screen
TabSwitch panels
↑/↓Scroll history
Ctrl+NNew conversation
Ctrl+SSave conversation
Ctrl+OOpen file
Ctrl+AToggle agent panel
Ctrl+MToggle model selector
?Help

Chat Commands

Within the TUI, you can use these commands:

/model <name>       - Switch AI model
/clear              - Clear conversation
/save <file>        - Save conversation
/load <file>        - Load conversation
/agent <name>       - Switch to agent
/system <prompt>    - Set system prompt
/image <path>       - Send image
/help               - Show help

Multimodal Content

Viewing Images

Not implemented. Inline image rendering is intended, not present. The source contains no kitty, iterm or sixel support and reads no IRONSHELL_TUI_IMAGE_PROTOCOL — exporting it does nothing.

# In TUI, send an image
/image screenshot.png

# Or via AI vision
ai("What's in this image?", { images: ["photo.jpg"] })

Supported terminals:

  • Kitty - Full image support
  • iTerm2 - macOS with inline images
  • WezTerm - Cross-platform
  • Sixel - Many terminals

Code Blocks

Code responses are syntax highlighted:

AI: Here's the implementation:

┌─rust─────────────────────────────────────────────┐
│ fn fibonacci(n: u64) -> u64 {                    │
│     match n {                                    │
│         0 => 0,                                  │
│         1 => 1,                                  │
│         n => fibonacci(n - 1) + fibonacci(n - 2)│
│     }                                            │
│ }                                                │
└──────────────────────────────────────────────────┘

Tables

Data is rendered as formatted tables:

AI: Here are the results:

┌──────────┬───────┬──────────────┐
│ Name     │ Size  │ Modified     │
├──────────┼───────┼──────────────┤
│ main.rs  │ 2.4KB │ 2 hours ago  │
│ lib.rs   │ 1.8KB │ 3 hours ago  │
│ test.rs  │ 892B  │ 1 day ago    │
└──────────┴───────┴──────────────┘

Agent Panel

Press Ctrl+A to toggle the agent panel:

┌─ Agents ──────────────────────────┐
│                                   │
│ ● coder (idle)                    │
│   "You are a Python expert"       │
│   Tools: [cat, write, grep]       │
│                                   │
│ ○ devops (idle)                   │
│   "You help with infrastructure"  │
│   Tools: [ls, ps, curl]           │
│                                   │
│ [+ New Agent]                     │
└───────────────────────────────────┘

Model Selector

Press Ctrl+M to select a model:

┌─ Select Model ─────────────────────┐
│                                    │
│ OpenAI                             │
│   ● gpt-4o                         │
│   ○ gpt-4o-mini                    │
│   ○ gpt-4-turbo                    │
│                                    │
│ Anthropic                          │
│   ○ claude-3-opus                  │
│   ○ claude-3-sonnet                │
│                                    │
│ Local (Ollama)                     │
│   ○ llama3                         │
│   ○ codellama                      │
│                                    │
└────────────────────────────────────┘

Reasoning Display

When using reasoning models (o1, R1), the TUI shows the reasoning process:

┌─ Thinking... ──────────────────────────────────┐
│ Let me break down this problem:                │
│ 1. First, I need to understand the constraint  │
│ 2. The input array could be empty              │
│ 3. Edge case: negative numbers                 │
│ ...                                            │
└────────────────────────────────────────────────┘

Final Answer:
Here's the optimized solution...

Streaming Responses

Responses stream in real-time:

AI: The quick brown fox |  ← Cursor shows typing

Configuration

TUI settings in ~/.config/ironshell/config.toml:

[tui]
# Theme
theme = "catppuccin-mocha"

# Image display
show_images = true
image_protocol = "kitty"  # kitty, iterm, sixel
max_image_width = 80

# Chat
show_timestamps = true
show_token_count = true

# Colors (Catppuccin Mocha)
background = "#1e1e2e"
foreground = "#cdd6f4"
accent = "#cba6f7"

Themes

Built-in themes:

  • catppuccin-mocha (default)
  • catppuccin-latte
  • dracula
  • nord
  • solarized-dark
  • solarized-light

Keyboard Shortcuts Reference

General

  • Ctrl+C - Exit/Cancel
  • Ctrl+L - Clear screen
  • Ctrl+Q - Quit TUI
  • ? - Help overlay
  • Tab - Next panel
  • Shift+Tab - Previous panel
  • ↑/↓ - Scroll/History
  • PgUp/PgDn - Page scroll

Actions

  • Enter - Send/Confirm
  • Ctrl+N - New conversation
  • Ctrl+S - Save
  • Ctrl+O - Open

Panels

  • Ctrl+A - Agents panel
  • Ctrl+M - Model selector
  • Ctrl+H - History

Agent API

The Agent API provides HTTP endpoints for executing IronShell code, managing agents, orchestrating workflows, and accessing the marketplace. The server uses axum and runs on port 3000 by default.

Starting the Server

ish serve                    # Start on default port 3000
ish serve --port 8080        # Custom port

Execution Endpoints

POST /api/v1/execute

Execute an IronShell command and return the result.

Request:

{
  "command": "ls \"src\" | where(fn(f) => f.extension == \"rs\") | len"
}

Response:

{
  "success": true,
  "result": "15",
  "type": "Int"
}

POST /api/v1/call/:builtin

Call a specific builtin by name with arguments.

Request:

{
  "args": ["src"]
}

Example: POST /api/v1/call/ls

POST /api/v1/pipeline

Execute a multi-step pipeline.

Request:

{
  "input": [1, 2, 3, 4, 5],
  "steps": ["map(fn(x) => x * 2)", "where(fn(x) => x > 4)"]
}

POST /api/v1/eval

Evaluate an arbitrary IronShell expression.

Request:

{
  "code": "let x = 42; x * 2"
}

Streaming Endpoints (SSE)

These endpoints return Server-Sent Events for long-running operations.

POST /api/v1/stream/execute

Stream execution results as they’re produced.

POST /api/v1/stream/pipeline

Stream pipeline results step-by-step.

POST /api/v1/stream/eval

Stream evaluation output.

SSE Event Format:

event: start
data: {"id": "exec-123"}

event: progress
data: {"step": 1, "total": 5, "message": "Processing..."}

event: data
data: {"result": "partial output"}

event: complete
data: {"result": "final result", "elapsed_ms": 150}

event: error
data: {"message": "Syntax error at line 3"}

Discovery Endpoints

GET /api/v1/schema

Return the complete IronShell language schema (types, builtins, syntax).

GET /api/v1/schema/:format

Return the schema in a specific format (e.g., json, openapi).

GET /api/v1/builtins

List all available builtins with their descriptions.

Response:

[
  { "name": "ls", "description": "List directory contents", "category": "filesystem" },
  { "name": "map", "description": "Transform each element", "category": "collections" },
  ...
]

GET /api/v1/builtins/:name

Get detailed information about a specific builtin.

Response:

{
  "name": "map",
  "description": "Apply a function to each element in an array",
  "category": "collections",
  "signature": "map(fn) -> Array",
  "examples": ["[1,2,3] | map(fn(x) => x * 2)"]
}

GET /api/v1/types

List all IronShell value types and their properties.

Orchestration Endpoints

GET /api/v1/orchestration/agents

List all registered agents and their status.

Response:

[
  {
    "id": "agent-1",
    "status": "idle",
    "capabilities": ["code-review", "testing"],
    "model": "openai:gpt-4o-mini"
  }
]

GET /api/v1/orchestration/tasks

List all tasks.

POST /api/v1/orchestration/tasks

Create a new task.

Request:

{
  "goal": "Analyze code quality in src/",
  "tools": ["ls", "cat", "grep"],
  "max_steps": 10
}

POST /api/v1/orchestration/workflows

Create and start a new workflow.

GET /api/v1/orchestration/workflows

List all workflows.

GET /api/v1/orchestration/workflows/:id

Get workflow details and status.

POST /api/v1/orchestration/workflows/:id/cancel

Cancel a running workflow.

GET /api/v1/orchestration/metrics

Get orchestration metrics (agent count, task counts, performance).

Response:

{
  "total_agents": 3,
  "active_tasks": 2,
  "completed_tasks": 15,
  "avg_task_duration_ms": 2300
}

Marketplace Endpoints

GET /api/v1/marketplace/search?q=code-review&category=dev

Search the agent marketplace.

GET /api/v1/marketplace/agents

List all marketplace agents.

POST /api/v1/marketplace/install

Install an agent from the marketplace.

Request:

{
  "name": "code-reviewer",
  "version": "1.0.0"
}

POST /api/v1/marketplace/uninstall

Uninstall a marketplace agent.

Request:

{
  "name": "code-reviewer"
}

POST /api/v1/marketplace/publish

Publish an agent to the marketplace.

Request:

{
  "name": "my-agent",
  "description": "A helpful coding agent",
  "system_prompt": "You are a code reviewer...",
  "tools": ["cat", "grep"],
  "model": "openai:gpt-4o-mini"
}

Health

GET /health

Health check endpoint.

Response:

{
  "status": "healthy",
  "version": "0.3.0",
  "uptime_seconds": 3600
}

AI Model API

The AI Model API provides an OpenAI-compatible HTTP interface for managing local AI models and performing inference. It supports model downloading, format conversion, and serving multiple providers through a unified API.

Starting the Server

aimodel serve                   # Start on default port
aimodel serve --port 8080       # Custom port

OpenAI-Compatible Endpoints

POST /v1/chat/completions

Chat completion API, compatible with the OpenAI format.

Request:

{
  "model": "llama3",
  "messages": [
    { "role": "system", "content": "You are a helpful assistant." },
    { "role": "user", "content": "What is Rust?" }
  ],
  "temperature": 0.7,
  "max_tokens": 1024,
  "stream": false
}

Response:

{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1705300000,
  "model": "llama3",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Rust is a systems programming language..."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 150,
    "total_tokens": 175
  }
}

Streaming: Set "stream": true to receive Server-Sent Events:

data: {"id":"chatcmpl-abc123","choices":[{"delta":{"content":"Rust"},"index":0}]}

data: {"id":"chatcmpl-abc123","choices":[{"delta":{"content":" is"},"index":0}]}

data: [DONE]

POST /v1/embeddings

Generate text embeddings.

Request:

{
  "model": "nomic-embed-text",
  "input": "What is the meaning of life?"
}

Response:

{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [0.023, -0.041, 0.015, ...]
    }
  ],
  "model": "nomic-embed-text",
  "usage": { "prompt_tokens": 8, "total_tokens": 8 }
}

Model Management

GET /v1/models

List all available models.

Response:

{
  "object": "list",
  "data": [
    {
      "id": "llama3",
      "object": "model",
      "owned_by": "local",
      "created": 1705300000
    }
  ]
}

GET /v1/models/:model_id

Get details about a specific model.

POST /v1/models/:model_id/download

Download a model from a supported source.

Request:

{
  "source": "huggingface",
  "revision": "main"
}

POST /v1/models/:model_id/convert

Convert a model between formats (e.g., GGUF, ONNX).

Request:

{
  "target_format": "gguf",
  "quantization": "q4_0"
}

DELETE /v1/models/:model_id

Delete a downloaded model.

Provider Management

GET /v1/providers

List configured inference providers.

Response:

[
  { "id": "ollama", "status": "available", "url": "http://localhost:11434" },
  { "id": "openai", "status": "available" },
  { "id": "vllm", "status": "unavailable" }
]

POST /v1/providers/:provider_id/validate

Test connectivity to a provider.

Response:

{
  "provider": "ollama",
  "valid": true,
  "latency_ms": 12,
  "models_available": 3
}

Storage Management

GET /v1/storage/stats

Get storage usage statistics for downloaded models.

Response:

{
  "total_size_bytes": 15000000000,
  "model_count": 5,
  "cache_size_bytes": 500000000,
  "storage_path": "/home/user/.ironshell/models"
}

POST /v1/storage/cleanup

Clean up cached files and temporary data.

Health & Status

GET /v1/health

Quick health check.

Response:

{ "status": "ok" }

GET /v1/status

Detailed server status with provider information.

Response:

{
  "status": "running",
  "version": "0.3.0",
  "uptime_seconds": 7200,
  "providers": { "ollama": "connected", "openai": "configured" },
  "models_loaded": 2,
  "requests_served": 150
}

Documentation

GET /swagger-ui

Interactive API documentation (when enable_openapi is configured).

GET /api-docs/openapi.json

OpenAPI specification in JSON format.

Client Usage

The AI Model API is compatible with any OpenAI client library:

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8080/v1",
    api_key="not-needed"  # Local models don't need keys
)

response = client.chat.completions.create(
    model="llama3",
    messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content)
curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"llama3","messages":[{"role":"user","content":"Hello"}]}'

WebSocket & SSE

IronShell provides real-time communication through WebSocket connections and Server-Sent Events (SSE) for streaming results.

WebSocket

Connecting

Connect to the WebSocket endpoint:

ws://localhost:3000/api/v1/ws
const ws = new WebSocket("ws://localhost:3000/api/v1/ws");

ws.onopen = () => console.log("Connected");
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  console.log(msg.type, msg);
};

Client Messages

Messages from client to server are JSON objects with a type field.

execute

Execute an IronShell command.

{
  "type": "execute",
  "id": "req-1",
  "request": {
    "command": "ls \"src\" | len"
  }
}

register

Register as an agent on the network.

{
  "type": "register",
  "agent_id": "my-agent",
  "capabilities": ["code-review", "testing"]
}

agent_message

Send a message to another registered agent.

{
  "type": "agent_message",
  "to": "target-agent-id",
  "payload": { "task": "review", "file": "main.rs" }
}

broadcast

Broadcast a message to all subscribers of a channel.

{
  "type": "broadcast",
  "channel": "status-updates",
  "payload": { "status": "analysis complete" }
}

subscribe / unsubscribe

Subscribe to or unsubscribe from a broadcast channel.

{ "type": "subscribe", "channel": "status-updates" }
{ "type": "unsubscribe", "channel": "status-updates" }

ping

Keep-alive ping.

{ "type": "ping", "id": "ping-1" }

Server Messages

Messages from server to client.

response

Result of an execute request.

{
  "type": "response",
  "id": "req-1",
  "response": {
    "success": true,
    "result": "15",
    "value_type": "Int"
  }
}

stream

Streaming event from an execute/eval/pipeline operation.

{
  "type": "stream",
  "id": "req-1",
  "event": {
    "kind": "data",
    "data": "partial result"
  }
}

channel

Broadcast message received on a subscribed channel.

{
  "type": "channel",
  "channel": "status-updates",
  "payload": { "status": "analysis complete" }
}

pong

Response to a ping.

{
  "type": "pong",
  "id": "ping-1",
  "timestamp": 1705300000000
}

error

Error notification.

{
  "type": "error",
  "id": "req-1",
  "message": "Unknown command: invalid_builtin"
}

agent_message

Message from another agent.

{
  "type": "agent_message",
  "from": "analyzer-agent",
  "payload": { "result": "3 issues found" }
}

registered

Confirmation of agent registration.

{
  "type": "registered",
  "agent_id": "my-agent"
}

agents

List of currently registered agents (sent on request or when agent list changes).

{
  "type": "agents",
  "agents": [
    { "id": "agent-1", "capabilities": ["code-review"] },
    { "id": "agent-2", "capabilities": ["testing"] }
  ]
}

Server-Sent Events (SSE)

SSE endpoints provide one-way streaming from server to client. They’re used for long-running operations where you want incremental results.

Endpoints

MethodPathDescription
POST/api/v1/stream/executeStream command execution
POST/api/v1/stream/pipelineStream pipeline processing
POST/api/v1/stream/evalStream code evaluation

Event Format

SSE uses the standard text/event-stream format:

event: start
data: {"id": "exec-123", "timestamp": 1705300000}

event: progress
data: {"step": 1, "total": 5, "message": "Loading files..."}

event: data
data: {"result": "[{\"name\": \"main.rs\", \"size\": 2048}]"}

event: complete
data: {"result": "final output", "elapsed_ms": 250}

Event Types

EventDescription
startOperation has begun
progressProgress update with step number and message
dataIntermediate data result
completeOperation finished successfully
errorOperation failed

Client Usage

// SSE with fetch
const response = await fetch("/api/v1/stream/execute", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ command: "ls src | map(fn(f) => f.name)" })
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const text = decoder.decode(value);
  // Parse SSE events from text
  for (const line of text.split("\n")) {
    if (line.startsWith("data: ")) {
      const data = JSON.parse(line.slice(6));
      console.log(data);
    }
  }
}

AI Chat Streaming

The AI Model API at /v1/chat/completions also supports SSE streaming when "stream": true:

const response = await fetch("http://localhost:8080/v1/chat/completions", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "llama3",
    messages: [{ role: "user", content: "Hello!" }],
    stream: true
  })
});

// Process SSE chunks
// Each chunk: data: {"choices":[{"delta":{"content":"token"}}]}
// Final: data: [DONE]

Dashboard WebSocket

The web dashboard connects to WebSocket at /api/v1/ws for real-time updates. The dashboard automatically:

  • Reconnects on disconnection (3-second retry)
  • Receives agent status updates
  • Gets workflow progress notifications
  • Monitors marketplace changes
// Dashboard auto-connect pattern
const ws = new WebSocket(`ws://${location.host}/api/v1/ws`);
ws.onclose = () => setTimeout(connect, 3000); // Auto-reconnect

Python SDK

The IronShell Python SDK (ironshell package v1.5.0) provides a Pythonic interface for evaluating IronShell code, running agents, building pipelines, and integrating with Python AI ecosystems.

Installation

pip install ironshell

Quick Start

from ironshell import evaluate, pipeline

# Evaluate IronShell code
result = evaluate('[1, 2, 3] | map(fn(x) => x * 2)')
print(result)  # [2, 4, 6]

# Build a pipeline
result = pipeline([1, 2, 3, 4, 5]).filter(lambda x: x > 2).map(lambda x: x * 10).run()
print(result)  # [30, 40, 50]

IronShellRuntime

The core runtime class for evaluating IronShell code.

from ironshell import IronShellRuntime

runtime = IronShellRuntime()

# Evaluate a single expression
result = runtime.eval('42 * 2')

# Evaluate a file
result = runtime.eval_file('script.ish')

Creating Agents

agent = runtime.create_agent(
    name="finder",
    tools=["ls", "cat", "grep"],
    max_steps=10,
)
result = await agent.run("Find all files over 10KB")
print(result.result)  # the final answer; trace and steps_taken are empty

Creating Swarms

planner = runtime.create_agent("planner", tools=["ls", "cat"])
reviewer = runtime.create_agent("reviewer", tools=["cat", "grep", "wc"])
swarm = runtime.create_swarm([planner, reviewer], max_iterations=20)
result = await swarm.run("Review src/ for code quality issues")
print(result.result)  # the combined answer

The swarm sends the swarm builtin {goal, tools, max_steps}. tools is the union of the agents’ tools and max_iterations is max_steps. The builtin has no model, policy or per-agent step option, so run raises ValueError for a policy other than "round_robin", an agent model other than the default openai:gpt-4o-mini, or an agent max_steps other than 10. See the Python SDK reference.

A2UI Events

Subscribe to agent-to-UI events for real-time feedback:

def on_event(event):
    if event.type == "progress":
        print(f"Progress: {event.data['step']}/{event.data['total']}")
    elif event.type == "notification":
        print(f"[{event.level}] {event.message}")

runtime.subscribe_a2ui(on_event)

PipelineBuilder

A fluent API for building data transformation pipelines:

from ironshell import pipeline

result = (
    pipeline([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    .filter(lambda x: x % 2 == 0)     # Keep even numbers
    .map(lambda x: x ** 2)             # Square them
    .sort()                             # Sort ascending
    .take(3)                            # First 3
    .run()
)
print(result)  # [4, 16, 36]

Pipeline Methods

MethodDescription
.map(fn)Transform each element
.filter(fn)Keep elements matching predicate
.reduce(fn, init)Fold to single value
.sort() / .sort(key)Sort elements
.reverse()Reverse order
.flatten()Flatten nested arrays
.unique()Remove duplicates
.take(n)First N elements
.skip(n)Skip N elements
.to_code()Generate IronShell code
.run()Execute the pipeline

Generating IronShell Code

code = (
    pipeline([1, 2, 3])
    .map(lambda x: x * 2)
    .filter(lambda x: x > 2)
    .to_code()
)
print(code)  # [1, 2, 3] | map(fn(x) => x * 2) | where(fn(x) => x > 2)

Workflows

Build structured AI workflows with retry and circuit-breaker patterns:

from ironshell.workflows import Workflow, WorkflowStep, WorkflowPattern

# Create a sequential workflow
wf = Workflow(pattern=WorkflowPattern.SEQUENTIAL)
wf.add_step(WorkflowStep(name="gather", code='ls "src"'))
wf.add_step(WorkflowStep(name="analyze", code='grep "TODO" "src/"'))
wf.add_step(WorkflowStep(name="report", code='echo "Analysis complete"'))

result = await wf.run(input_data={})
print(result.outputs)

MapReduce Workflow

from ironshell.workflows import MapReduceWorkflow

wf = MapReduceWorkflow(
    map_code='fn(item) => ai("Summarize: " + item)',
    reduce_code='fn(summaries) => join(summaries, "\n\n")'
)
result = await wf.run(["doc1.md", "doc2.md", "doc3.md"])

Circuit Breaker

Protect against cascading failures:

from ironshell.workflows import CircuitBreaker

breaker = CircuitBreaker(
    failure_threshold=3,
    recovery_timeout=30.0
)

try:
    result = await breaker.call_async(lambda: runtime.eval('http_get "https://api.example.com"'))
except Exception:
    print("Circuit open — using fallback")

Metrics

Production-grade observability with Prometheus-compatible metrics:

from ironshell.metrics import Counter, Histogram, Timer

# Count operations
requests = Counter("requests_total", "Total requests processed")
requests.inc()

# Track latencies
latency = Histogram("request_duration_seconds", "Request latency")

with Timer(latency):
    result = runtime.eval('ai("Summarize this")')

# Export for Prometheus
print(latency.to_prometheus())

Distributed

Service discovery and leader election for multi-node deployments:

from ironshell.distributed import ServiceRegistry, ServiceInfo

registry = ServiceRegistry()

# Register a service
registry.register(ServiceInfo(
    id="worker-1",
    name="ironshell-worker",
    address="192.168.1.10",
    port=3000,
    metadata={"gpu": "true"}
))

# Discover services
workers = registry.get_services_by_name("ironshell-worker")
for w in workers:
    print(f"{w.id} at {w.address}:{w.port}")

Leader Election

from ironshell.distributed import LeaderElection

election = LeaderElection(service_info)

election.on_leadership_change(lambda is_leader:
    print("I am the leader!" if is_leader else "Following leader")
)

if election.is_leader():
    # Coordinate work distribution
    pass

LangChain Integration

Use IronShell tools within LangChain agents:

from ironshell.langchain import IronShellTool, IronShellAgentTool

# Use IronShell as a LangChain tool
shell_tool = IronShellTool()
result = shell_tool.run('ls "src" | where(fn(f) => f.size > 1000)')

# Run an IronShell agent from LangChain
agent_tool = IronShellAgentTool(tools=["ls", "cat"])
result = agent_tool.run("Find TODO comments in the project")

Cloud Deployment

Deploy IronShell as serverless functions:

from ironshell.cloud import LambdaRuntime, FunctionConfig

runtime = LambdaRuntime()
handler = runtime.create_handler(FunctionConfig(
    name="data-processor",
    code='fn(event) => event.body | json_parse | map(fn(x) => x * 2)',
    timeout=30
))

# Generate deployment configuration
deployment = runtime.generate_deployment()

Supported platforms:

  • AWS Lambda via LambdaRuntime
  • Azure Functions via AzureFunctionsRuntime

Plugins

IronShell supports a plugin system for extending the shell with custom builtins, tools, and integrations.

Managing Plugins

Listing Plugins

plugins
# [
#   { name: "git-tools", version: "1.0", enabled: true, category: "dev" },
#   { name: "docker-tools", version: "0.5", enabled: false, category: "containers" },
#   ...
# ]

Plugin Information

plugin_info "git-tools"
# {
#   name: "git-tools",
#   version: "1.0",
#   description: "Git integration tools for IronShell",
#   builtins: ["git_status", "git_log", "git_diff"],
#   enabled: true
# }

Enabling and Disabling

plugin_enable "docker-tools"
plugin_disable "git-tools"

Loading and Unloading

plugin_load takes the path to a plugin’s TOML manifest, not a plugin name. It registers the plugin with status: "registered". Each [builtins] entry is one lambda literal and its name becomes callable. A core name, or a name another loaded plugin provides, is refused. A manifest with [builtins] is code, so IRONSHELL_PLUGINS=off refuses it and agent mode needs IRONSHELL_PLUGIN_ALLOW. Agent mode approves each plugin builtin call. Native and script plugins are not loaded by this builtin. A duplicate id is E_BAD_STATE.

plugin_load "./plugins/my-custom-plugin/plugin.toml"   # Register its metadata
plugin_unload "my-custom-plugin"                       # Remove it (built-in plugins cannot be unloaded)

Categories

plugin_categories
# ["dev", "containers", "cloud", "data", "security", ...]

Plugin Architecture

Plugins are Rust crates that implement the tool and builtin interfaces. They provide:

  1. Custom builtins — New commands available in the shell
  2. Tool schemas — OpenAI-compatible function descriptions for AI agent use
  3. Configuration — Plugin-specific settings

Feature Flags

IronShell uses feature flags to enable/disable built-in capabilities:

features                    # List all features
feature_enabled "ai"        # Check if AI is enabled
feature_enable "distributed"
feature_disable "experimental"
feature_set "max_agents" 10

Distributed Computing

IronShell has a node registry, a local in-memory consensus tally and a local aggregate_results over an Array. Work that would go to other machines is not implemented in this build: job submission and remote execution refuse with E_UNIMPLEMENTED, and no node is contacted by the cluster builtins.

Cluster Management

Creating a Cluster

cluster_create("my-cluster")
# {name: "my-cluster", status: "created", node_count: 0, created_at: …}

cluster_create takes an optional name of at most 128 bytes (default default) and at most one argument. It contacts no node and stores the name nowhere: nodes added with cluster_add_node join one process-wide registry that does not record which cluster they belong to.

Adding Nodes

cluster_add_node("node-1", "192.168.1.10:3000")
cluster_add_node("node-2", "192.168.1.11:3000", ["compute"])

{id: "node-3", address: "192.168.1.12:3000"} | cluster_add_node

The third argument is an Array of String capabilities (default compute and storage). The address must have no whitespace. Each call returns {id, address, capabilities, status: "registered"}. Nothing contacts the node, so it is registered, not online, and no heartbeat is checked.

Cluster Status

cluster_status()
# {total_nodes, online_nodes, busy_nodes, total_jobs, pending_jobs,
#  running_jobs, average_load}

online_nodes counts nodes stored with an Online status. A node added by cluster_add_node is stored that way, so this count is not a liveness check.

Node Management

cluster_nodes()
# [ {id: "node-1", address: "192.168.1.10:3000", load: 0.0, status: …}, … ]

cluster_remove_node("node-2")   # reports whether that node existed

Job Scheduling

Submitting Jobs

Job submission is not implemented in this build. job_submit (alias submit) has no executor behind it, so it refuses with E_UNIMPLEMENTED. No job is queued and nothing runs:

try { job_submit("build", "cargo test") } catch e { e.error.code }
# "E_UNIMPLEMENTED"

Job Status, Results and Listing

job_status, job_results and job_list read the distributed job table. In this build nothing adds to it, because job_submit refuses, so the table is empty:

job_list()
# []

job_results("job-abc123")   # an unknown id gives an empty result
job_status("job-abc123")    # an unknown id is E_NOT_FOUND

Remote Execution

remote_exec refuses with E_UNIMPLEMENTED; it runs nothing. No SSH or cluster transport is wired in, so the command is not sent to the node. The node and command are checked first: a node that is not registered is E_NOT_FOUND, and a valid call is refused as above.

try { remote_exec("node-1", "ls src") } catch e { e.error.code }
# "E_UNIMPLEMENTED" for a registered node, "E_NOT_FOUND" for an unknown one

# Run a command on a real host:
ssh_exec "user@192.168.1.10" "ls src | wc -l"

Result Aggregation

aggregate_results combines an Array that is already in hand. It does not collect anything from nodes:

aggregate_results([1, 2, 3], "sum")    # adds finite numbers
[1, 2, 3] | aggregate_results("sum")   # the piped form takes the strategy only
aggregate_results(["a", "b"])          # concat, the default, returns the Array

The strategies are concat (the default), sum, count, first and last. sum refuses any element that is not a finite number instead of skipping it, first and last refuse an empty Array, and an unknown strategy is E_BAD_ARG.

NANDA Consensus

For coordinated multi-agent decisions, NANDA keeps a consensus tally in this process. Two limits apply before you rely on it. Nothing identifies a voter, so the electorate is the caller’s assertion. And the tally is in memory only: it is gone when the process exits. Nothing is sent to other agents.

# Open a proposal: a name, and a Record that sets its threshold and electorate
let p = nanda_propose("deploy-v2", {threshold: 0.66, electorate: 3})

# Each vote is a Bool against the proposal id
nanda_vote(p.id, true)
nanda_vote(p.id, true)

# How much of the electorate has balloted (quorum is half of it)
nanda_quorum(p.id)
# {quorum_reached: true, participation: 0.67, votes_cast: 2, electorate: 3}

# The approval ratio against the threshold
nanda_consensus(p.id)

# Commit the outcome in this process: refused unless there is quorum and the
# approval ratio is at or above the threshold. A ratio below the threshold
# rejects the proposal.
nanda_commit(p.id)

TUI Distributed Panel

The TUI provides a dedicated Distributed Agents tab (Tab 5) for visual management:

  • s — Start distributed swarm
  • d — Stop distributed swarm
  • r — Refresh network status
  • t — Test node connections

See TUI Navigation for all key bindings.

Marketplace

The IronShell Marketplace is a registry for sharing and discovering pre-built AI agents.

Browsing

Search

# Search for agents
marketplace_search "code review"
# [
#   { name: "code-reviewer", author: "ironshell", version: "1.0.0", downloads: 500 },
#   { name: "pr-analyzer", author: "community", version: "0.8.0", downloads: 120 },
#   ...
# ]

The web dashboard provides a visual marketplace browser at the Marketplace tab with filtering, sorting, and one-click installation.

API Endpoints

MethodEndpointDescription
GET/api/v1/marketplace/search?q=...Search agents
GET/api/v1/marketplace/agentsList all agents
POST/api/v1/marketplace/installInstall an agent
POST/api/v1/marketplace/uninstallUninstall an agent
POST/api/v1/marketplace/publishPublish an agent

Installing Agents

marketplace_install "code-reviewer"
marketplace_install "code-reviewer" "1.0.0"    # Specific version

marketplace_install downloads the package’s archive from the registry into the package cache and unpacks it. Nothing in the package is run, and links inside the archive are refused. A package the registry does not hold is E_NOT_FOUND; there is no fallback to the local catalogue, which holds records but no archives.

Integrity: the archive must match the registry’s SHA-256 digest (64 hex digits) before anything is written. A package with no digest, or a digest of any other shape, MD5 included, is refused with a coded error. The digest comes from the same registry as the archive, so it detects corruption and truncation in transit only. It does not detect a compromised registry. Packages are not signed and no publisher keys are checked; there is no signing scheme yet.

Names: an agent name and version are one path segment: ASCII letters, digits, -, _ and ., not starting with . or -. A / is refused, so scoped names are not supported. A refused name is E_BAD_ARG; through the API it is HTTP 400.

Via the API:

curl -X POST http://localhost:3000/api/v1/marketplace/install \
  -H "Content-Type: application/json" \
  -d '{"name": "code-reviewer", "version": "1.0.0"}'

Uninstalling

marketplace_uninstall "code-reviewer"

Publishing

Share your agents with the community:

marketplace_publish {
  name: "my-agent",
  description: "Analyzes Rust code for common patterns",
  system_prompt: "You are an expert Rust developer...",
  tools: ["cat", "grep", "ls"],
  model: "openai:gpt-4o-mini",
  tags: ["rust", "code-analysis"]
}

Via the Dashboard

  1. Open the Marketplace tab
  2. Click Publish Agent
  3. Fill in the form: name, description, system prompt, tools, model
  4. Click Publish

Agent Structure

Marketplace agents include:

FieldDescription
nameUnique agent identifier
descriptionWhat the agent does
authorPublisher name
versionSemantic version
system_promptAgent’s system instructions
toolsAllowed builtin tools
modelRecommended model URI
tagsCategory tags for discovery
downloadsDownload count
starsCommunity rating

Local Registry

The RegistryClient manages local state:

  • Search: Full-text search against the registry (local fallback when offline)
  • Install: Downloads agent config and registers locally
  • Cache: Installed agents stored in ~/.ironshell/marketplace/

Dashboard Integration

The web dashboard’s Marketplace page provides:

  • Search bar with real-time results
  • Category filtering and sorting
  • Install/Uninstall buttons with loading states
  • Publish dialog with form validation
  • Agent cards showing name, description, version, downloads, and verified badges

Security

IronShell includes multiple security layers for safe AI agent execution and enterprise deployments.

Agent Sandboxing

Agents run in a restricted sandbox with configurable limits:

LimitDefaultDescription
Timeout30 secondsMaximum execution time
Output10 MBMaximum output size
Memory512 MBMaximum memory usage

Command Allowlist

Restrict which commands agents can execute:

export AGENT_ALLOW_CMDS="ls,cat,grep,wc,find"

Only listed commands will be available to agents. The allowlist is enforced by validate_command() which:

  • Checks the tool name against the allowlist
  • Blocks shell metacharacters (;, |, &&, ||, `, $())
  • Prevents path traversal in arguments

Prompt Validation

Agent goals are validated before execution:

  • Maximum 4,000 characters
  • Injection pattern detection
  • Sanitization of control characters

Rate Limiting

OperationLimit
Agent plans10 per minute
Agent executions5 per minute

RBAC (Role-Based Access Control)

Enterprise deployments support RBAC for fine-grained access control:

# Create roles
role_create "developer" { permissions: ["read", "execute", "agent"] }
role_create "admin" { permissions: ["read", "write", "execute", "agent", "admin"] }

# Grant roles to users
role_grant "alice" "developer"
role_grant "bob" "admin"

# Check permissions
check_permission "alice" "execute"    # true
check_permission "alice" "admin"      # false

# List roles
roles_list
user_roles "alice"    # ["developer"]

Audit Logging

Track all operations for compliance:

# View recent audit events
audit_log 20
# [{ timestamp: "...", user: "alice", action: "execute", details: "ls src/" }, ...]

# Query audit log
audit_query { user: "bob", action: "agent", since: "2024-01-01" }

# Export audit log
audit_export "audit_2024.json"

# Audit statistics
audit_stats
# { total_events: 1500, users: 3, actions: { execute: 800, agent: 200, ... } }

SSO (Single Sign-On)

sso_init records the provider configuration for this process. Nothing contacts the provider. It takes the provider (oauth2, saml or oidc), a client id and an https issuer URL (http is allowed only for localhost):

sso_init("oidc", "example-app", "https://auth.example.com")
# {status: "initialized", provider: "oidc"}

sso_auth                # Refused (E_UNIMPLEMENTED): the identity provider callback is not verified, so no session is started; use rbac_login
sso_validate            # Reports valid: false in this build, since sso_auth never creates a session
sso_status              # Reports the configuration and active_sessions
sso_logout("token")     # Reports not_found for a token that is not held

Compliance

Run compliance checks against a named standard (GDPR, HIPAA, SOC2 or PCI). The only scope is all:

compliance_check("SOC2")
# {standard, scope, passed, failed, unverified, compliant}
# A control the shell cannot observe (such as encryption at rest) is
# unverified, and compliant is true only when nothing failed and nothing is
# unverified.

compliance_report("SOC2")
# The compliance_check verdict, plus generated_at, the audit entry count,
# the role count and whether SSO is configured. It has no remediation steps.

Cryptographic Operations

# Hashing
crypto_hash "sha256" "Hello, world!"
crypto_hash_file "sha256" "document.pdf"

# Random data
crypto_random_bytes 32
crypto_uuid                    # Generate UUID v4

# Encoding
crypto_base64_encode "hello"   # "aGVsbG8="
crypto_base64_decode "aGVsbG8="

# Password hashing
let hash = crypto_password_hash "my-password"
crypto_password_verify "my-password" hash   # true

# JWT
crypto_jwt_decode token

API Key Management

API keys are stored securely:

  • Keyring integration: Keys stored in OS keyring when available
  • Environment fallback: OPENAI_API_KEY, IRONSHELL_API_KEY environment variables
  • No logging: API keys are never written to logs or audit trails
# Keys loaded automatically from keyring or environment
# SecureApiConfig::from_keyring_or_env() handles the priority

Best Practices

  1. Always set AGENT_ALLOW_CMDS in production — never give agents unrestricted access
  2. Use RBAC for multi-user deployments
  3. Enable audit logging for compliance-sensitive environments
  4. Review agent plans with dry_run: true before allowing execution of destructive operations
  5. Rotate API keys regularly and use the keyring for storage
  6. Set appropriate rate limits to prevent abuse

Performance

Tips and techniques for optimizing IronShell performance in production workloads.

Pipeline Optimization

Lazy Evaluation

IronShell pipelines process elements lazily where possible. Order your operations to minimize work:

# Good: filter first, then transform
ls "." | where(fn(f) => f.size > 10000) | map(fn(f) => expensive_operation(f))

# Bad: transform everything, then filter
ls "." | map(fn(f) => expensive_operation(f)) | where(fn(f) => f.size > 10000)

Use take Early

Limit results early to avoid processing unnecessary elements:

# Good: limit early
ls "." | sort_by "size" "desc" | take 5

# Bad: process all, then take (sort_by processes all anyway, but downstream operations are limited)

Prefer Builtins Over AI

Builtins execute instantly; AI calls have network latency:

# Fast: builtin string operation
"hello world" | upper

# Slow: AI for simple text tasks
ai "Convert to uppercase: hello world"

AI Performance

Semantic Caching

Avoid redundant API calls by caching responses:

# First call: hits API
let answer = ai "What is Rust?"
semantic_cache "What is Rust?" answer

# Second call: cache hit (< 1ms vs ~500ms API call)
let cached = semantic_cache_get "Tell me about Rust"

Model Selection

Choose the right model for the task:

ModelSpeedCostBest For
gpt-4o-miniFastLowSimple queries, classification
gpt-4oMediumHighComplex reasoning, code generation
ollama:llama3VariableFreePrivacy-sensitive, offline

Batch Operations

Process multiple items in a single AI call when possible:

# One call for multiple items (faster)
let items = join(data, "\n")
ai "Classify each line:\n${items}"

# vs N separate calls (slower)
data | map(fn(item) => ai "Classify: ${item}")

Agent Performance

Limit Max Steps

Set appropriate max_steps to prevent runaway agents:

# Quick lookup: 3-5 steps
agent "What's in src/main.rs?" ["cat"] 3

# Deep analysis: 10-15 steps
agent "Full project review" ["ls", "cat", "grep"] 15

Targeted Tool Sets

Give agents only the tools they need—fewer tools means faster decisions:

# Good: specific tools
agent "Find TODOs" ["grep"]

# Bad: everything
agent "Find TODOs" ["ls", "cat", "grep", "find", "wc", "head", "tail", "sort"]

RAG Performance

Index Size

The built-in RAG uses in-memory indexing with hash-based embeddings. Performance characteristics:

DocumentsIndex TimeSearch Time
100< 100ms< 10ms
1,000< 1s< 50ms
10,000< 10s< 200ms

Search Tuning

Adjust top_k based on your needs:

rag_search query 3    # Fast, fewer results
rag_search query 20   # Slower, more comprehensive

Memory Management

Max Messages

The TUI limits chat history to prevent memory growth:

config.max_messages = 1000    # Default

Older messages are dropped when the limit is reached.

Cache Limits

Semantic cache has built-in limits:

  • Max entries: 1,000
  • TTL: 1 hour
  • Oldest entries evicted automatically

Build Optimization

Release Builds

Always use release builds for production:

cargo build --release --bins

Release builds are significantly faster than debug builds (10-100x for CPU-bound operations).

Binary Size

Strip debug symbols for smaller binaries:

cargo build --release
strip target/release/ish

Data Processing

Real-world examples of using IronShell for data processing tasks.

CSV Analysis

Load and Analyze CSV

# Parse CSV manually
let lines = cat "sales.csv" | split "\n"
let headers = split (first lines) ","
let rows = lines | slice 1 (len lines) | map(fn(line) => split line ",")

# Find top sellers
rows
  | map(fn(r) => { name: r[0], amount: float(r[2]) })
  | sort_by "amount" "desc"
  | take 10

Aggregate by Category

let data = cat "products.csv" | split "\n" | slice 1 100
  | map(fn(line) => {
      let cols = split line ","
      { category: cols[1], price: float(cols[2]), qty: int(cols[3]) }
  })

# Revenue per category
data
  | map(fn(r) => { category: r.category, revenue: r.price * r.qty })
  | group_by "category"

JSON Processing

API Data Pipeline

# Fetch and process API data
let users = web_json_get "https://jsonplaceholder.typicode.com/users"

users
  | map(fn(u) => { name: u.name, city: u.address.city, company: u.company.name })
  | sort_by "city" "asc"

Merge Multiple Sources

let users = web_json_get "https://api.example.com/users"
let orders = web_json_get "https://api.example.com/orders"

# Join users with their order counts
users | map(fn(u) => {
  let user_orders = orders | where(fn(o) => o.user_id == u.id)
  { ...u, order_count: len user_orders, total_spent: user_orders | map(fn(o) => o.amount) | sum }
}) | sort_by "total_spent" "desc"

Log Analysis

Error Frequency

cat "app.log"
  | split "\n"
  | where(fn(line) => contains line "ERROR")
  | map(fn(line) => {
      let parts = split line " "
      { date: parts[0], error: join(slice(parts, 3, len(parts)), " ") }
  })
  | map(fn(e) => e.error)
  | sort
  | uniq

Request Latency Analysis

cat "access.log"
  | split "\n"
  | where(fn(line) => contains line "GET /api")
  | map(fn(line) => {
      let parts = split line " "
      float(last parts)
  })
  | map(fn(latencies) => {
      {
        count: len latencies,
        avg_ms: avg latencies,
        p50: sort latencies | nth(len(latencies) / 2),
        max: max latencies
      }
  })

File System Analysis

Disk Usage Report

ls "."
  | where(fn(f) => f.is_dir)
  | map(fn(d) => {
      let usage = fs_du d.path
      { dir: d.name, size_mb: round(usage.total / 1048576.0), files: usage.files }
  })
  | sort_by "size_mb" "desc"

Find Duplicate Files

fs_walk "."
  | where(fn(f) => !f.is_dir)
  | map(fn(f) => { path: f.path, hash: crypto_hash_file "md5" f.path, size: f.size })
  | sort_by "hash" "asc"
  | reduce(fn(acc, f) => {
      # Group by hash to find duplicates
      ...acc
  }, {})

Statistical Summary

let data = [23, 45, 12, 67, 34, 89, 11, 56, 78, 42]

let stats = {
  n: len data,
  sum: data | sum,
  mean: data | avg,
  min: data | min,
  max: data | max,
  range: (data | max) - (data | min),
  sorted: data | sort
}

echo stats

Web Scraping

Examples of web scraping and data extraction with IronShell.

Basic Page Fetching

# Fetch a page
let page = http_get "https://example.com"
echo page.status     # 200
echo page.ok         # true

# Extract text content
let text = web_html_to_text page.body
echo text

Scraping with Selectors

# Scrape article titles
let titles = web_scrape "https://news.ycombinator.com" "a.storylink"
titles | take 10 | each(fn(t) => echo t.text)

API-Based Scraping

# GitHub repository stats
let repos = ["rust-lang/rust", "tokio-rs/tokio", "serde-rs/serde"]

repos | map(fn(repo) => {
  let data = web_json_get "https://api.github.com/repos/${repo}"
  {
    name: data.full_name,
    stars: data.stargazers_count,
    forks: data.forks_count,
    language: data.language
  }
}) | sort_by "stars" "desc"

Content Extraction

HTML to Markdown

let html = (http_get "https://blog.example.com/post/1").body
let md = web_html_to_markdown html
file_write "post.md" md

Extract Emails

let page = (http_get "https://example.com/contact").body
let emails = web_extract_emails page
echo emails   # ["info@example.com", "support@example.com"]
let links = web_scrape "https://example.com" "a[href]"
links | map(fn(a) => a.href) | where(fn(h) => starts_with h "https://") | unique

Download Pipeline

# Download multiple files
let urls = [
  "https://data.example.com/dataset1.csv",
  "https://data.example.com/dataset2.csv",
  "https://data.example.com/dataset3.csv"
]

mkdir "downloads"
urls | each(fn(url) => {
  let filename = last(split(url, "/"))
  echo "Downloading ${filename}..."
  web_download url "downloads/${filename}"
})

URL Health Checking

let urls = cat "urls.txt" | split "\n" | where(fn(u) => len(u) > 0)

let results = urls | map(fn(url) => {
  let check = web_check_url url
  { url: url, status: check.status, ok: check.reachable }
})

# Report broken links
let broken = results | where(fn(r) => !r.ok)
echo "${len broken} broken links found:"
broken | each(fn(r) => echo "  ✗ ${r.url} (${r.status})")

Paginated API

# Fetch all pages from a paginated API
let all_items = []
let page = 1
let has_more = true

# Note: IronShell supports while loops via recursion
let fetch_page = fn(page, acc) => {
  let data = web_json_get "https://api.example.com/items?page=${page}&limit=100"
  let items = concat acc data.items
  if len(data.items) == 100 {
    fetch_page (page + 1) items
  } else {
    items
  }
}

let all_items = fetch_page 1 []
echo "Total items: ${len all_items}"

RSS Feed Parsing

let feed = (http_get "https://blog.example.com/feed.xml").body
let items = web_xpath feed "//item"

items | take 5 | map(fn(item) => {
  {
    title: web_xpath item "title/text()",
    link: web_xpath item "link/text()",
    date: web_xpath item "pubDate/text()"
  }
})

AI Automation

Examples of using IronShell’s AI capabilities for automation tasks.

Code Documentation Generator

# Generate documentation for all Rust files
ls "src"
  | where(fn(f) => f.extension == "rs")
  | map(fn(f) => {
      let code = cat f.path
      let doc = ai "Write a brief module-level doc comment for this Rust file. Include purpose, key types, and public API:\n\n${code}" {
        model: "openai:gpt-4o-mini"
      }
      { file: f.name, documentation: doc }
  })
  | each(fn(d) => {
      echo "## ${d.file}\n${d.documentation}\n"
  })

Intelligent Log Analysis

# Index logs for RAG
let errors = cat "app.log" | split "\n" | where(fn(l) => contains l "ERROR")
errors | each(fn(e) => rag_index e "app.log")

# Ask questions about the errors
let ctx = rag_query "What are the most common error patterns?" 5
ai "Based on these log entries:\n${ctx.context}\n\nIdentify the top 3 error patterns and suggest fixes."

Automated Code Review

# Agent-powered code review
agent {
  goal: "Review all .rs files in src/ for: 1) unwrap() calls that should use ? or expect(), 2) TODO/FIXME comments, 3) functions over 50 lines. Provide a summary with file:line references.",
  tools: ["ls", "cat", "grep", "wc"],
  max_steps: 15
}

Commit Message Generator

# Generate a commit message from the current diff
let diff = sh "git diff --staged"

if len(diff) > 0 {
  let msg = ai "Write a concise conventional commit message for this diff. Use format: type(scope): description\n\nDiff:\n${diff}" {
    model: "openai:gpt-4o-mini"
  }
  echo "Suggested commit message:"
  echo msg
} else {
  echo "No staged changes"
}

RAG-Powered Q&A System

# Step 1: Index your project docs
ls "docs" | where(fn(f) => f.extension == "md")
  | each(fn(f) => {
      echo "Indexing ${f.name}..."
      rag_index (cat f.path) f.name
  })

# Step 2: Interactive Q&A
let question = "How do I create a custom pipeline?"
let cached = semantic_cache_get question

let answer = if cached.hit {
  echo "(cached)"
  cached.response
} else {
  let ctx = rag_query question 5
  let resp = ai "Context:\n${ctx.context}\n\nQuestion: ${question}\n\nAnswer based only on the context above."
  semantic_cache question resp
  resp
}

echo answer

Knowledge Graph Builder

# Build a project knowledge graph from source code
ls "src" | where(fn(f) => f.extension == "rs") | each(fn(f) => {
  # Add each file as an entity
  let file_id = (kg_add "File" f.name { path: f.path, size: f.size }).entity_id

  # Find imports
  let imports = cat f.path | split "\n"
    | where(fn(l) => starts_with (trim l) "use crate::")
    | map(fn(l) => trim l | replace "use crate::" "" | replace ";" "")

  imports | each(fn(imp) => {
    let mod_id = (kg_add "Module" imp {}).entity_id
    kg_relate file_id mod_id "imports" {}
  })
})

# Query the graph
echo "Files importing eval:"
kg_query "eval"

Multi-Agent Analysis

# Swarm-based project analysis
swarm {
  goal: "Perform a comprehensive analysis: 1) Code quality assessment, 2) Dependency audit, 3) Test coverage estimate, 4) Documentation completeness. Produce a structured report.",
  tools: ["ls", "cat", "grep", "find", "wc", "fs_tree"],
  max_steps: 30
}

Automated Testing

# Generate test cases with AI
let source = cat "src/parser.rs"
let tests = ai "Generate 5 unit test functions in Rust for the parser module. Cover edge cases:\n\n${source}" {
  model: "openai:gpt-4o"
}

file_write "tests/generated_parser.rs" tests
echo "Generated test file"

Batch Classification

# Classify support tickets
let tickets = web_json_get "https://api.example.com/tickets?status=new"

tickets | map(fn(t) => {
  let category = ai "Classify this support ticket into one of: bug, feature, question, billing. Respond with only the category.\n\nTicket: ${t.description}" {
    model: "openai:gpt-4o-mini"
  }
  { ...t, category: trim(category) }
}) | save_json "classified_tickets.json"

DevOps

Examples of using IronShell for DevOps automation, system administration, and infrastructure management.

System Monitoring

Health Dashboard

let cpu = sys_cpu_info
let mem = sys_mem_info
let disk = sys_disk_info
let load = sys_load_avg

echo "=== System Health ==="
echo "Hostname: ${sys_hostname}"
echo "OS: ${sys_os} ${sys_arch}"
echo "Uptime: ${sys_uptime}"
echo "Load: ${load}"
echo "CPU: ${cpu.cores} cores"
echo "Memory: ${round(mem.used / 1048576)}MB / ${round(mem.total / 1048576)}MB"
echo "Disk: ${round(disk.used / 1073741824)}GB / ${round(disk.total / 1073741824)}GB"

Process Monitor

# Find top memory-consuming processes
proc_list
  | sort_by "mem_usage" "desc"
  | take 10
  | map(fn(p) => { pid: p.pid, name: p.name, mem_mb: round(p.mem_usage / 1048576) })

Port Scanning

# Check which services are listening
net_ports
  | where(fn(p) => p.state == "LISTEN")
  | map(fn(p) => { port: p.port, pid: p.pid, process: p.process })
  | sort_by "port" "asc"

Service Management

Service Status Check

let services = ["nginx", "postgresql", "redis"]

services | map(fn(svc) => {
  let status = svc_status svc
  { name: svc, status: status.state, pid: status.pid }
})

Restart All Services

["nginx", "postgresql", "redis"]
  | each(fn(svc) => {
      echo "Restarting ${svc}..."
      svc_restart svc
  })

Deployment Pipeline

Build and Deploy

# Build
echo "Building..."
let build = sh "cargo build --release 2>&1"
echo build

# Run tests
echo "Testing..."
let test_result = sh "cargo test 2>&1"
if contains test_result "FAILED" {
  echo "Tests failed! Aborting deployment."
  exit 1
}

# Deploy
echo "Deploying..."
file_copy "target/release/ish" "/opt/ironshell/ae"
svc_restart "ironshell"
echo "Deployed successfully"

Rolling Deployment

let nodes = ["192.168.1.10", "192.168.1.11", "192.168.1.12"]

nodes | each(fn(node) => {
  echo "Deploying to ${node}..."

  # Remove from load balancer
  echo "  Draining connections..."
  sleep 5000

  # Deploy. Use ssh_exec: it really runs the command. `remote_exec` refuses
  # with E_UNIMPLEMENTED and runs nothing, so it cannot deploy anything.
  ssh_exec "${node}" "sudo systemctl restart ironshell"

  # Health check
  let health = web_check_url "http://${node}:3000/health"
  if health.reachable {
    echo "  ✓ ${node} healthy"
  } else {
    echo "  ✗ ${node} FAILED - rolling back"
    exit 1
  }
})

echo "All nodes deployed successfully"

Git Operations

Change Summary

# Summarize recent changes
let log = sh "git log --oneline -20"
echo "Recent commits:"
echo log

# AI-powered summary
ai "Summarize these git commits into a changelog entry:\n${log}"

Branch Cleanup

# Find merged branches
let branches = sh "git branch --merged" | split "\n" | map(fn(b) => trim b)
  | where(fn(b) => b != "main" && b != "master" && !starts_with(b, "*"))

echo "Merged branches to clean up:"
branches | each(fn(b) => echo "  ${b}")

Container Management

# List running containers
sh "docker ps --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'"
  | split "\n"
  | map(fn(line) => {
      let parts = split line "\t"
      { name: parts[0], status: parts[1], ports: parts[2] }
  })

Backup Script

let timestamp = sh "date +%Y%m%d_%H%M%S" | trim
let backup_dir = "/backups/${timestamp}"

mkdir backup_dir

echo "Backing up database..."
sh "pg_dump mydb > ${backup_dir}/db.sql"

echo "Backing up config..."
file_copy "/etc/ironshell" "${backup_dir}/config"

echo "Backing up data..."
sh "tar czf ${backup_dir}/data.tar.gz /var/lib/ironshell"

# Cleanup old backups (keep last 7)
ls "/backups"
  | sort_by "name" "desc"
  | slice 7 (ls "/backups" | len)
  | each(fn(old) => {
      echo "Removing old backup: ${old.name}"
      sh "rm -rf ${old.path}"
  })

echo "Backup complete: ${backup_dir}"

Cron Jobs

# List scheduled jobs
cron_list
# [{ id: "1", schedule: "0 * * * *", command: "ish health-check.ish" }, ...]

# Add a new cron job
cron_add "0 2 * * *" "ish backup.ish"    # Daily at 2 AM

# Remove a job
cron_remove "1"

Network Diagnostics

let target = "api.example.com"

echo "=== Network Diagnostics for ${target} ==="

# DNS resolution
let dns = net_dns_lookup target
echo "DNS: ${dns}"

# Ping
let ping = net_ping target
echo "Ping: ${ping.latency_ms}ms (${if ping.reachable { 'OK' } else { 'FAILED' }})"

# Traceroute
echo "Route:"
net_traceroute target | each(fn(hop) => {
  echo "  ${hop.hop}. ${hop.ip} (${hop.latency_ms}ms)"
})

Development Setup

How to set up a development environment for contributing to IronShell.

Prerequisites

  • Rust 1.88+ (install via rustup)
  • Git
  • pkg-config and OpenSSL dev headers (Linux)
  • Optional: Ollama for local AI model testing

Platform-Specific

Ubuntu/Debian:

sudo apt install build-essential pkg-config libssl-dev

macOS:

brew install openssl pkg-config

Windows:

# Rust includes MSVC build tools; ensure Visual Studio C++ Build Tools are installed

Clone and Build

git clone https://github.com/nervosys/IronShell.git
cd IronShell
cargo build --bins

This produces two binaries:

  • target/debug/ish — the main shell
  • target/debug/aimodel — the AI model management CLI

Run Tests

cargo test                    # All tests (~272)
cargo test --lib              # Library unit tests
cargo test --test eval        # Specific test file
cargo test pipeline           # Tests matching "pipeline"

Launch for Development

# REPL mode
cargo run

# TUI mode
cargo run -- tui

# Execute a script
cargo run -- examples/00_hello.ish

# AI model server
cargo run --bin aimodel -- serve --port 8080

Environment Variables

VariablePurposeExample
IRONSHELL_AIDefault AI provideropenai
OPENAI_API_KEYOpenAI API keysk-...
OLLAMA_HOSTOllama server URLhttp://localhost:11434
AGENT_ALLOW_CMDSAgent tool allowlistls,git,cat
IRONSHELL_LOGLog leveldebug

IDE Setup

VS Code

Install the IronShell extension from the marketplace, or load it from source:

cd vscode-extension
npm install
npm run compile
# Press F5 to launch Extension Development Host

Recommended extensions:

  • rust-analyzer — Rust language support
  • Even Better TOML — Cargo.toml editing
  • CodeLLDB — Debugger

Configuration

.vscode/settings.json:

{
  "rust-analyzer.cargo.features": "all",
  "rust-analyzer.check.command": "clippy",
  "editor.formatOnSave": true
}

Project Layout

DirectoryContents
src/Core shell source
src/ai_api/AI model server
src/tui/Terminal UI
src/transpile/Bash transpiler
src/bin/Additional binaries
tests/Integration tests
test-scripts/Script-based tests
examples/Example .ish scripts
docs/book/mdBook documentation
web/Web dashboard & WASM
vscode-extension/VS Code extension

Code Style

Coding conventions and style guidelines for IronShell contributors.

Rust Style

General

  • Follow standard Rust conventions (rustfmt defaults)
  • Run cargo fmt before committing
  • Run cargo clippy and address warnings
  • Maximum line length: 100 characters (soft limit)

Naming

#![allow(unused)]
fn main() {
// Types: PascalCase
struct PipelineStage { ... }
enum Value { ... }

// Functions/methods: snake_case
fn evaluate_expr(expr: &Expr) -> Result<Value> { ... }

// Constants: SCREAMING_SNAKE_CASE
const MAX_PIPELINE_DEPTH: usize = 64;

// Module files: snake_case
// src/os_tools.rs, src/shell_features.rs
}

Error Handling

#![allow(unused)]
fn main() {
// Use anyhow::Result for evaluator functions
fn eval_expr(expr: &Expr, env: &mut Env) -> anyhow::Result<Value> {
    // Add context for debugging
    some_operation().context("failed to evaluate pipeline stage")?;
    
    // Avoid .unwrap() in production code — use .expect() with a message or ?
    let val = map.get("key").context("missing required key")?;
    
    Ok(val)
}
}

Return Types

Builtins must return structured Value types:

#![allow(unused)]
fn main() {
// Good: structured data for pipeline processing
fn builtin_ls(args: &[Value], env: &mut Env) -> Result<Value> {
    Ok(Value::Array(entries.into_iter().map(|e| {
        Value::Record(BTreeMap::from([
            ("name".into(), Value::String(e.name)),
            ("size".into(), Value::Int(e.size as i64)),
            ("is_dir".into(), Value::Bool(e.is_dir)),
        ]))
    }).collect()))
}

// Bad: raw text output
fn builtin_ls_bad() -> Result<Value> {
    Ok(Value::String("file1.txt\nfile2.txt".into()))
}
}

Imports

#![allow(unused)]
fn main() {
// Group imports: std, external crates, local modules
use std::collections::BTreeMap;
use std::path::PathBuf;

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::ast::{Expr, Stmt};
use crate::value::Value;
}

Commit Messages

Follow Conventional Commits:

type(scope): description

feat(parser): add pattern matching syntax
fix(eval): correct lambda capture semantics
docs(book): add pipeline examples chapter
test(builtins): add filesystem operation tests
refactor(ai): extract provider trait
chore(deps): update tokio to 1.35

Types: feat, fix, docs, test, refactor, chore, perf, ci

Scopes: parser, eval, builtins, ai, tui, agent, api, transpile, docs, deps

Documentation

Code Comments

#![allow(unused)]
fn main() {
/// Evaluates an expression in the given environment.
///
/// # Arguments
/// * `expr` - The AST expression to evaluate
/// * `env` - Mutable reference to the variable environment
///
/// # Returns
/// The resulting `Value`, or an error if evaluation fails.
///
/// # Examples
/// ```
/// let result = eval_expr(&Expr::Int(42), &mut env)?;
/// assert_eq!(result, Value::Int(42));
/// ```
fn eval_expr(expr: &Expr, env: &mut Env) -> Result<Value> { ... }
}

Module-Level Docs

Every .rs file should have a module-level doc comment:

#![allow(unused)]
fn main() {
//! Pipeline evaluation and data flow.
//!
//! This module handles the core pipeline operator (`|`), connecting
//! expressions so that the output of one feeds into the next.
}

Adding New Features

The typical flow for language features:

  1. ast.rs — Add AST node variants
  2. tokens.rs — Add tokens if new syntax is needed
  3. lexer.rs — Tokenize new syntax
  4. parser.rs — Parse tokens into AST
  5. eval.rs — Implement runtime semantics
  6. typecheck.rs — Add type inference rules
  7. tests/ — Write comprehensive tests

Testing

How to write and run tests for IronShell.

Running Tests

# All tests
cargo test

# Library tests only
cargo test --lib

# Specific test file
cargo test --test eval
cargo test --test pipeline

# Tests matching a pattern
cargo test parse_lambda
cargo test "test_builtin_"

# With output (for debugging)
cargo test -- --nocapture

# Single-threaded (for tests that share state)
cargo test -- --test-threads=1

Test Organization

LocationPurpose
tests/eval.rsCore evaluator tests
tests/parse.rsParser unit tests
tests/pipeline.rsPipeline operator tests
tests/builtins.rsBuiltin function tests
tests/typecheck.rsType inference tests
tests/smoke.rsQuick validation / smoke tests
tests/transpile_bash.rsBash transpiler tests
tests/ai_*.rsAI integration tests
tests/tui_*.rsTUI component tests
test-scripts/Script-based integration tests

Writing Tests

Evaluator Tests

Test that IronShell expressions produce expected values:

#![allow(unused)]
fn main() {
use ironshell_shell::{eval_str, Value};

#[test]
fn test_arithmetic() {
    let result = eval_str("2 + 3").unwrap();
    assert_eq!(result, Value::Int(5));
}

#[test]
fn test_pipeline() {
    let result = eval_str("[1,2,3] | map(fn(x) => x * 2)").unwrap();
    assert_eq!(result, Value::Array(vec![
        Value::Int(2), Value::Int(4), Value::Int(6)
    ]));
}

#[test]
fn test_record_access() {
    let result = eval_str("let r = {name: \"test\", val: 42}; r.val").unwrap();
    assert_eq!(result, Value::Int(42));
}
}

Parser Tests

Verify that source text parses to the expected AST:

#![allow(unused)]
fn main() {
use ironshell_shell::parser::parse;
use ironshell_shell::ast::*;

#[test]
fn test_parse_let() {
    let stmts = parse("let x = 42").unwrap();
    assert!(matches!(&stmts[0], Stmt::Let { name, .. } if name == "x"));
}

#[test]
fn test_parse_lambda() {
    let stmts = parse("fn(x) => x + 1").unwrap();
    // Verify AST structure
    assert!(matches!(&stmts[0], Stmt::Expr(Expr::Lambda { .. })));
}
}

Builtin Tests

Test that builtins return the correct structured values:

#![allow(unused)]
fn main() {
#[test]
fn test_builtin_len() {
    let result = eval_str("len [1, 2, 3]").unwrap();
    assert_eq!(result, Value::Int(3));
}

#[test]
fn test_builtin_map() {
    let result = eval_str("[1,2,3] | map(fn(x) => x * 10)").unwrap();
    assert_eq!(result, Value::Array(vec![
        Value::Int(10), Value::Int(20), Value::Int(30)
    ]));
}

#[test]
fn test_builtin_where() {
    let result = eval_str("[1,2,3,4,5] | where(fn(x) => x > 3)").unwrap();
    assert_eq!(result, Value::Array(vec![Value::Int(4), Value::Int(5)]));
}
}

Type Checker Tests

Verify type inference results:

#![allow(unused)]
fn main() {
#[test]
fn test_typecheck_int() {
    let ty = typecheck_str("42").unwrap();
    assert_eq!(ty, Type::Int);
}

#[test]
fn test_typecheck_lambda() {
    let ty = typecheck_str("fn(x) => x + 1").unwrap();
    assert!(matches!(ty, Type::Function(_, _)));
}
}

Test Patterns

Error Cases

Always test error conditions:

#![allow(unused)]
fn main() {
#[test]
fn test_divide_by_zero() {
    let result = eval_str("10 / 0");
    assert!(result.is_err());
}

#[test]
fn test_undefined_variable() {
    let result = eval_str("unknown_var");
    assert!(result.is_err());
}
}

Script-Based Tests

For complex scenarios, use .ish test scripts in test-scripts/:

# test-scripts/builtins/test_basic.ish
# Each line is a self-contained assertion

let x = 42
assert x == 42

let arr = [1, 2, 3]
assert (len arr) == 3

let s = upper "hello"
assert s == "HELLO"

AI Tests

AI tests that require API keys should check for availability:

#![allow(unused)]
fn main() {
#[test]
fn test_ai_completion() {
    if std::env::var("OPENAI_API_KEY").is_err() {
        eprintln!("Skipping: OPENAI_API_KEY not set");
        return;
    }
    
    let result = eval_str(r#"ai "Say hello" { model: "openai:gpt-4o-mini" }"#).unwrap();
    assert!(matches!(result, Value::String(_)));
}
}

Coverage

While there’s no strict coverage requirement, aim for:

  • Core evaluator: High coverage — test every expression type
  • Builtins: At least one test per builtin function
  • Parser: Test both valid syntax and error cases
  • Type checker: Test each inference rule
  • AI/TUI: Test structure and state, mock network calls

Continuous Integration

Tests run automatically on pull requests. Ensure:

  1. cargo test passes with no failures
  2. cargo clippy produces no warnings
  3. cargo fmt --check shows no formatting issues

Changelog

The changelog is maintained in one place, at the root of the repository:

This page used to carry a second, hand-written copy covering v0.1.0 to v0.3.0, with v0.3.0 marked “(Current)”. It had not been touched in the eleven major versions since, and because the book was never built, nobody saw it say so. A duplicate that drifts is worse than a link.

FAQ

General

What is IronShell?

IronShell is a next-generation shell written in Rust that combines typed functional programming with multimodal AI capabilities. Unlike traditional shells that pass raw text between commands, IronShell uses structured data types (records, arrays, tables) throughout its pipeline system.

How is IronShell different from Bash/Zsh?

FeatureBash/ZshIronShell
Data modelRaw textTyped values (Int, Record, Array, …)
PipelinesText streamsStructured data flow
FunctionsString-basedTyped lambdas with inference
AINoneBuilt-in LLM, agents, RAG
Pattern matchingCase statementsmatch expressions
Error handlingExit codesResult types with context

Can I use IronShell as my daily driver?

IronShell is under active development. It’s excellent for data processing, AI automation, and scripting. For interactive daily use, you may want to keep your current shell available while adopting IronShell incrementally.

How do I run Bash commands in IronShell?

Use the sh builtin:

sh "git status"
sh "docker ps"

Language

Why typed pipelines?

Typed pipelines eliminate an entire class of bugs. When ls returns an array of records with known fields, where, map, and sort_by can validate their arguments at parse time rather than failing silently at runtime.

Does IronShell support loops?

IronShell favors functional iteration via map, each, reduce, and where. Recursion is supported for looping patterns:

let countdown = fn(n) => if n > 0 { echo n; countdown(n - 1) } else { echo "done" }
countdown 5

What is fn(x) => expr?

This is a lambda (anonymous function). Lambdas are first-class values — they can be stored in variables, passed to builtins, and returned from functions:

let double = fn(x) => x * 2
[1,2,3] | map(double)   # [2, 4, 6]

AI

Which AI providers are supported?

IronShell supports six providers via model URIs:

  • openai:gpt-4o — OpenAI
  • ollama:llama3 — Local Ollama
  • compat:mixtral — OpenAI-compatible APIs
  • tgi:model — HuggingFace Text Generation Inference
  • vllm:model — vLLM serving
  • llamacpp:model — llama.cpp server

Do I need an API key?

For cloud providers (OpenAI), yes — set OPENAI_API_KEY. For local providers (Ollama, llama.cpp), no API key is needed.

How do agents work?

Agents use a ReAct (Reason + Act) loop: they think about the task, choose a tool, execute it, observe the result, and repeat until the goal is met:

agent {
  goal: "Find the largest file in the current directory",
  tools: ["ls", "sort_by"],
  max_steps: 5
}

What is a swarm?

A swarm is a group of AI agents that collaborate on a task. A coordinator routes subtasks to specialized agents, and they share state via a blackboard:

swarm {
  goal: "Analyze this project",
  tools: ["ls", "cat", "grep"],
  max_steps: 20
}

TUI

How do I launch the TUI?

ish tui

What are the TUI tabs?

  1. Chat — AI conversation interface
  2. Agent Swarm — Monitor multi-agent collaboration
  3. Media Browser — View images, audio, video
  4. Settings — Configure providers and appearance
  5. Distributed — Manage cluster nodes
  6. Reasoning — Advanced chain-of-thought display
  7. Search — Search chat history

How do I switch tabs?

Press Tab and Shift+Tab to cycle through tabs, or press 1–7 in Normal mode.

Troubleshooting

cargo build fails with OpenSSL errors

Install OpenSSL development headers:

# Ubuntu/Debian
sudo apt install libssl-dev pkg-config

# macOS
brew install openssl

AI commands return “no provider configured”

Set the provider environment variable:

export IRONSHELL_AI=openai
export OPENAI_API_KEY=sk-...

Tests fail with “connection refused”

Some tests require a running Ollama instance or API keys. Tests that need external services will skip gracefully if the required environment is not available.

TUI looks broken

Ensure your terminal supports 256 colors and Unicode. Recommended terminals: Windows Terminal, iTerm2, Alacritty, kitty.