Skip to content

Repository files navigation

kaish (会sh)

ci crates.io

Kai the hermit crab — kaish mascot — looking at kaish code

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.

Why a shell for agents?

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" | gather

Handing 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.

What's Different About kaish?

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 — $VAR is always one value, never split on spaces
  • Line iteration in for-loops — a for head splits text on \n only, never on whitespace within a line: for line in $(cat file), for i in $(seq 1 5), and for 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/false are booleans; TRUE and yes are ordinary strings
  • Pre-validation — validation stretches down into builtins, revealing errors before execution

Quick Tour

#!/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" | gather

See docs/LANGUAGE.md for the complete language reference, or ask kaish itself — help is in-band: help builtins, help syntax, help <tool>.

Getting Started

You'll need a Rust toolchain (rustup) for either path below — the REPL or an embedded kernel.

The REPL

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)> "
}

Embedding the kernel

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.

Builtins

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 — grep and sed read 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/-r takes strict ERE. awk has 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

Safety rails

  • 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 PATH or a direct path — run against the real filesystem — the VFS sandbox does not apply to them. Block them at runtime with allow_unwrapped_commands=false (it allows any program that is not a wrapped command: PATH lookup, exec, spawn, env CMD), or build without the subprocess capability feature and they don't exist at all.
  • --overlay makes a call copy-on-write: writes stay in memory unless the script runs kaish-vfs commit.
  • set -o trash (or KAISH_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 --confirm is 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.

Why build 会sh?

会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.

Building from Source

git clone https://github.com/tobert/kaish
cd kaish
cargo build --release
cargo test --all
cargo clippy --all --all-targets -- -D warnings

CI 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.

Contributing

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.

License

MIT

About

会sh — a shell for agents

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages