kaish is a shell for AI agents delivered as an embeddable Rust library with a reference REPL. The language is inspired by Bourne shells, following an 80/20 rule on selecting the most useful features, and dropping dangerous ones. It also adds a JSON data model. The resulting language is basically plain shell for simple things, while having direct support for JSON manipulation.
The builtins — grep, sed, awk, find, and ninety-odd more — run in-process, so most
text processing never needs fork() or exec(). All file I/O goes through a
virtual filesystem that can pass through, stay in memory, or overlay the two.
An embedded kaish gives an agent a complete scripting environment that can be
constrained naturally.
Try it now: tobert.github.io/kaish-extras —
the kernel compiled to wasm, running entirely in your browser tab. No install,
no server; the playground is seeded with kaish's own source so you can grep
the shell's implementation from inside the shell.
Agents need to compose operations such as filtering output, transforming data, and iterating over results. They are already good at Bourne shell idioms, and shell is already an ideal language for text processing. kaish inherits all of that, so piping, redirecting, and composing commands works like it always has, with a few changes.
# Filter and transform in one script
ls src/ | grep "\.rs$" | head -n 5
# Iterate over results
for f in *.log; do
wc -l "$f"
done
# Parallel processing with bounded concurrency
seq 1 10 | scatter --as N --limit 4 | echo "processing $N" | gatherHanding an agent bash -c is dangerous on many levels. It comes with
word-splitting surprises, tools that vary by platform and version, and is
difficult to sandbox. Most importantly, it lacks a way to validate the program
before execution.
kaish provides a shell that just works for 80% of the scripts agents generate. The 20% that might be rejected come with educational error messages, so models get immediate feedback and can try something else. kaish validates the program before running it so the rejections come before any code runs.
kaish's data model is JSON. A variable holds an array or a record as naturally
as a string. $(cmd) binds a typed value when the command's output is a value,
so x=$(fromjson <<< '[1,2]') binds a list. A builtin with a POSIX counterpart
binds text instead, so grep, etc. will return text as anyone would expect. All
builtins support --json. When specified, the command returns JSON instead
of the usual bare text.
kaish is sh-like but not a full Bourne shell or bash. The idea is to preserve the language that comes naturally, while providing better pre-execution syntax checking, easy embedding, and a VFS abstraction to help with sandboxing.
- JSON data model — kaish's native values are JSON types: strings, numbers, booleans, arrays, and records.
- Single brackets are JSON -
[is for json arrays and records,[[is for branching - No implicit word splitting —
$VARis always one value, never split on spaces - Line iteration in for-loops — a
forhead splits text on\nonly, never on whitespace within a line:for line in $(cat file),for i in $(seq 1 5), andfor f in $(ls)all iterate the same way - Explicit splitting — use
split "$VAR"for whitespace/delimiter/regex splitting - No backticks — only
$(cmd)substitution - Strict booleans — only lowercase
true/falseare booleans;TRUEandyesare ordinary strings - Pre-validation — validation stretches down into builtins, revealing errors before execution
#!/usr/bin/env kaish
GREETING="Hello"
echo "$GREETING, world!"
# control flow with [[ works just like bash
if [[ -f config.json ]]; then
echo "Config found"
fi
# ** recurses; the glob builtin adds options such as --exclude.
# Like $(find ...), $(glob ...) binds text, one path per line.
for file in $(glob **/*.log --exclude="*.tmp.log"); do
echo "logfile: $file"
done
# quote to join: adjacent unquoted tokens never paste together
echo "$GREETING/world.txt" # ✅ quote the whole word
# echo $GREETING/world.txt # ❌ parse error — kaish won't paste $GREETING and /world.txt
# pipes and redirects
cat urls.txt | grep "https" | head -n 10 > filtered.txt
# the data model is JSON: parse text into typed collections, index directly
CONFIG='{"name":"amy","langs":["rust","kaish"]}'
C=$(fromjson <<< "$CONFIG")
echo "${C[name]} writes ${C[langs][0]}" # amy writes rust
SERVERS=$(fromjson <<< '{"web1":"10.0.0.1","web2":"10.0.0.2"}')
for host in $(keys $SERVERS); do
echo "$host -> ${SERVERS[$host]}"
done
# glob patterns expand inline, or use the glob builtin for options
glob "**/*.rs" --exclude="*_test.rs"
# parallel execution with scatter/gather — --as N binds $N in each worker;
# --limit caps concurrency; gather emits one JSONL record per worker
seq 1 10 | scatter --as N --limit 4 | echo "processing $N" | gatherSee docs/LANGUAGE.md for the complete language reference, or
ask kaish itself — help is in-band: help builtins, help syntax, help <tool>.
You'll need a Rust toolchain (rustup) for either path below — the REPL or an embedded kernel.
cargo install kaish-repl # installs a binary named `kaish`$ kaish
会sh> for f in *.rs; do wc -l "$f"; done
142 main.rs
87 lib.rs
会sh>
The REPL loads an init file on startup — the first match of $KAISH_INIT,
~/.config/kaish/init.kai, ~/.kaishrc — for aliases, exports, and a custom
prompt. Define kaish_prompt and it's called before each input line:
# ~/.config/kaish/init.kai
alias ll='ls -la'
alias gs='git status'
export EDITOR=vim
kaish_prompt() {
echo "$(pwd)> "
}Construct a Kernel, point it at a sandbox root, call execute():
[dependencies]
kaish-kernel = "0.18"
tokio = { version = "1", features = ["full"] }use kaish_kernel::{Kernel, KernelConfig, VfsMountMode};
use std::path::PathBuf;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Sandboxed to one directory. The default build can't spawn processes
// at all — external commands are an opt-in cargo feature (`subprocess`).
let config = KernelConfig::named("my-agent")
.with_vfs_mode(VfsMountMode::Sandboxed {
root: Some(PathBuf::from("/path/to/workspace")),
})
.with_cwd(PathBuf::from("/path/to/workspace"));
let kernel = Kernel::new(config)?;
let result = kernel.execute(r#"ls | grep '\.rs$' | head -n 3"#).await?;
if result.code != 0 {
eprintln!("script failed: {}", result.err);
}
println!("{}", result.text_out());
Ok(())
}The kernel is hermetic by default — it never reads the OS environment (the
frontend supplies vars), and the OS-touching capability features (subprocess,
host, os-integration, tokens) are opt-in cargo features, so every way to
reach the host is explicit. Every execute() returns an ExecResult with
clean text output, an optional typed data payload (--json on any command),
and an exit code agents can branch on: 2 is a usage error or a refusal that
names what to do instead (e.g. kaish-trash empty without --confirm), 3
means output was truncated, 124 is a timeout.
docs/EMBEDDING.md is the full guide: kernel construction,
capability features, ExecuteOptions, custom tools, the exit-code contract, and
thread stack sizing.
Using kaish over MCP? kaish core doesn't ship an MCP server — MCP servers live in the embedders. kaibo is the showcase: agents with kaish powers in an MCP (or CLI). Kaibo agents have a kaish shell tool for exploring filesystems and text.
Not embedding, just curious? kaish-extras
compiles the kernel to wasm32-unknown-unknown and runs it in a browser tab —
try it at tobert.github.io/kaish-extras.
kaish builtins run in-process, and replace calls to the host OS tools.
Design principles:
- Verifiable — each builtin has a schema (params, types, examples) exposed via
help <tool>. This enables validation ahead of runtime and eases the building of static checks. - Convention-following — flags and behavior match the patterns deeply embedded in training data
and decades of existing scripts.
grep -rn,sed 's/old/new/g',awk '{print $1}'all work as expected. - 80/20 — implement the features used 80% of the time, deliberately omit the 20% that add complexity without proportional value.
- GNU regex, exactly —
grepandsedread GNU BRE by default, exactly as GNU grep and GNU sed do:grep 'fn consult('/sed -n '/fn consult(/p'match a literal paren,grep 'a\|b'/sed 's/a\|b/x/'alternate, and-E/-rtakes strict ERE.awkhas no BRE — it reads gawk's ERE, where the bare forms above are already the operators and\|/\(…\)are already literal.
| Category | Tools |
|---|---|
| Text | awk, base64, cut, diff, grep, head, sed, sort, split, tac, tail, tr, uniq, wc, xxd |
| Files | basename, cat, cd, checksum, cmp, cp, dd, dirname, file, find, glob, ln, ls, mkdir, mktemp, mv, patch, pwd, readlink, realpath, rm, stat, tee, touch, tree, write |
| JSON | fromjson, fromjsonl, jq, keys, tojson, tojsonl, typeof, values |
| System | alias, bg, command, date, echo, env, exec, export, fg, help, hostname, jobs, kill, plan, printf, ps, push, random, read, seq, set, sleep, spawn, timeout, tokens, type, uname, unalias, unset, wait, which |
| Parallel | scatter, gather |
| Meta | :, assert, false, test, true |
| kaish-* | kaish-ast, kaish-clear, kaish-ignore, kaish-last, kaish-mounts, kaish-output-limit, kaish-status, kaish-tools, kaish-trash, kaish-validate, kaish-vars, kaish-version, kaish-vfs |
- Builtins go through the VFS and see only its mounts — the agent preset
sandboxes to
$HOME+/tmp, with/v/as in-memory scratch under a 64 MiB budget. - External commands — resolved via
PATHor a direct path — run against the real filesystem — the VFS sandbox does not apply to them. Block them at runtime withallow_unwrapped_commands=false(it allows any program that is not a wrapped command: PATH lookup,exec,spawn,env CMD), or build without thesubprocesscapability feature and they don't exist at all. --overlaymakes a call copy-on-write: writes stay in memory unless the script runskaish-vfs commit.set -o trash(orKAISH_TRASH=1) diverts deletes and truncating overwrites to the freedesktop.org Trash instead of destroying the prior content, so a mistake is recoverable.kaish-trash empty --confirmis the one operation that always asks — it discards the recovery net itself, and no session setting turns that ask off.- kaish itself does not decide whether a statement may run — an embedder
reads
plan_program(source)for each statement's commands and variables and decides for itself, before anything executes.
Trash semantics are covered in docs/LANGUAGE.md; the
embedder-facing plan_program contract in docs/EMBEDDING.md.
会sh (kaish) was originally prototyped as part of 会術 Kaijutsu and was separate enough it made sense to split it out. Amy was also a fan of ksh and pdksh back in the 00s so k-ai-sh seemed fun. kaish is now also used by kaibo to provide agents with a read-only shell.
git clone https://github.com/tobert/kaish
cd kaish
cargo build --release
cargo test --all
cargo clippy --all --all-targets -- -D warningsCI runs five gates on every PR and push to main: tests, clippy, rustdoc, a
no-default-features test run of the kernel (the capability-feature sandbox), and a
wasm32-wasip1 build of kaish-wasi. AGENTS.md, "Gates" lists the
commands. See
.github/workflows/ci.yml; releases to crates.io
are cut manually, so that one workflow is the whole CI story.
Agent-generated PRs are welcome! 🤖 This project is built with AI agents and we love seeing what other agents come up with. All changes go through a PR.
Be sure to have your agent read AGENTS.md. Most of what we do for kaish is a standard open source process.
Please review your code before submitting PRs. kaibo subagents use kaish as their read-only shell and it does a great job of finding defects before committing or pushing that PR.
MIT