Koshell is a human-centric shared terminal: AI beside your terminal, not above it.
The project keeps the human as the primary terminal operator while giving AI enough
shared terminal context to explain, diagnose, and assist — without turning the terminal
into a separate chat room or an agent-owned execution loop. When the terminal raises a
question, you type #? and ask in place.
Build both binaries, then install them (with the man pages) under /usr/local:
make # cargo release build + Bun-compiled daemon binary
sudo make install # /usr/local/bin/{koshell,koshell-ai-daemon} + man pagesRun make and make install as two steps: installing does not rebuild, so
nothing runs under sudo except the file copies. For a user install without sudo:
make && make install PREFIX=$HOME/.localInstalling the two binaries side by side is what lets the terminal find and
auto-start the daemon with zero configuration (the daemon binary is a
self-contained Bun executable, so it is large — tens of MB). After installing,
man koshell and man koshell.toml document the CLI and the config format,
and sudo make uninstall removes exactly what was installed. See
docs/design-0012-system-install-makefile-and-man-pages.md.
koshell # wrap the default shell (bash/zsh get shell integration)
koshell python3 -i # launch a program directly instead of a shell; everything
# after the first positional argument goes to the program
koshell -- some-command # `--` reserves room for future koshell options and allows
# command names that start with a dashTo start koshell automatically in every new terminal, install the auto-wrap snippet at the top of your shell rc file:
eval "$(koshell shell-init zsh)" # first line of ~/.zshrc
eval "$(koshell shell-init bash)" # first line of ~/.bashrc (on macOS, make sure
# ~/.bash_profile sources ~/.bashrc)The snippet execs the just-started interactive shell into koshell; the shell koshell
spawns re-sources your rc and skips the exec because it is already wrapped on this
terminal (the tty branded into KOSHELL matches its $(tty)), so your configuration
loads exactly once, inside the wrap. The marker is tty-scoped, so a shell that lands on a
fresh terminal — a new tmux pane — wraps itself instead, and #? works in every pane (see
docs/design-0009-tty-scoped-nesting-marker.md and
docs/design-0017-consolidate-environment-into-koshell.md). It stays inert for non-interactive
shells, non-TTY stdio, and TERM=dumb. To opt out for one shell, start it with
KOSHELL_NO_AUTO=1; to disable auto-wrap without a working shell, create
~/.config/koshell/no-auto (see docs/design-0003-shell-init-auto-wrap.md).
#? works in every form: type #? <question> (or command #? <question>) and the
question fires when the line's output completes or stabilizes. Directly launched
programs use the output-stabilization path, since shell integration markers only exist
inside bash/zsh.
Answers come from the AI daemon, which the terminal starts automatically on the first
#? — you no longer run it by hand (see docs/design-0008-daemon-lifecycle-auto-spawn-and-bun-runtime.md).
One daemon is shared per user session, and it exits on its own after 10 idle minutes.
Manage it explicitly when you need to:
koshell daemon status # is it running? pid, version, uptime, connections
koshell daemon start # start it without waiting for a #?
koshell daemon stop # stop it
koshell daemon restart # stop (if running), then startTo debug the daemon in the foreground, run it directly (logs to stdout):
bun packages/ai-daemon/src/index.tsThe daemon runs on Bun (≥ 1.3), the project's only JS toolchain.
To find the daemon, auto-spawn looks for a koshell-ai-daemon executable next to the
koshell binary, then on PATH. Working from a source checkout (no installed binary),
point it at the source instead:
export KOSHELL_DAEMON_CMD="bun $PWD/packages/ai-daemon/src/index.ts"Auto-spawn is controlled by two environment variables: KOSHELL_NO_DAEMON_SPAWN=1
disables it (the terminal then degrades inline until you start the daemon yourself), and
KOSHELL_DAEMON_CMD overrides the launch command entirely (used verbatim).
Provider, model, and auth come from Koshell's own config at
$XDG_CONFIG_HOME/koshell/koshell.toml (default ~/.config/koshell/koshell.toml). The
minimal config names one model and lets the key come from the provider's environment
variable:
model = "anthropic/claude-sonnet-4-5" # "provider/id"; 30+ builtin providers
# (anthropic, openai, google, openrouter, ...)with ANTHROPIC_API_KEY exported. The builtin catalog is the embedded pi
runtime's full provider catalog, so most providers need nothing beyond their
conventional API-key environment variable (man koshell.toml lists the common
ones; a misspelled provider name makes #? print the complete list). Or put
the key in the config instead:
model = "anthropic/claude-sonnet-4-5"
[providers.anthropic]
api_key = "sk-ant-..." # literal, "$ENV_VAR", or "!command" (e.g. a keychain read)Subscription providers sign in interactively instead: koshell auth login anthropic
(Claude Pro/Max), github-copilot, or openai-codex (ChatGPT Plus/Pro) runs the
provider's OAuth flow and stores the token in $XDG_DATA_HOME/koshell/auth.json;
koshell auth status shows what is configured and from where.
The daemon reads the config when a conversation starts, so after editing it run
koshell reload to apply the change to the current terminal (its next #? picks up
the new config; the in-progress conversation is discarded). koshell reload --all
applies it to every open koshell. An invalid config is rejected without disturbing any
running session. koshell status reports the current instance: its daemon connection,
active model, and whether a conversation is live.
Conversations are currently memory-only. koshell reload starts a fresh conversation
and discards the previous transcript; disconnecting the terminal session or restarting
the daemon also leaves no conversation to resume. Terminal context is likewise bounded:
the daemon receives the current screen and a recent pushed window, but it cannot yet pull
older off-screen output. In dogfooding, output longer than one screen can therefore leave
the AI with only the currently visible portion. Transcript persistence and the read-only
terminal context tool loop are not implemented yet.
A custom provider is a full block (api, base_url, api_key, and at least one
model); this is also how you pin a non-default API type such as openai-responses:
model = "mycorp/mycorp-large"
[providers.mycorp]
api = "openai-completions"
base_url = "https://api.mycorp.example/v1"
api_key = "$MYCORP_API_KEY"
[[providers.mycorp.models]]
id = "mycorp-large"The config selects exactly one active model — there is no runtime model switching.
A new terminal uses the current config; an existing terminal applies an edit through
koshell reload, which starts a fresh conversation. If the config is missing or invalid,
the terminal keeps working and #? reports what to fix inline.
See docs/design-0011-koshell-provider-configuration.md.
Both processes log at a configurable level, set by --log-level <level> or the
KOSHELL_LOG environment variable (the argument wins). The terminal owns the screen,
so its logs go to $XDG_STATE_HOME/koshell/koshell.log (default level warn,
env_logger filter syntax). The daemon logs to its own stdout when run in the
foreground, and to $XDG_STATE_HOME/koshell/daemon.log when auto-spawned (default level
info).
Koshell is a hybrid monorepo with two runtimes:
koshell-rs(Rust, foreground) — one process per terminal window. Owns the PTY, the terminal mirror, screen snapshots, alternate-screen detection, the timeline, local terminal context, and#?detection. It stays usable as a transparent shell wrapper even when the AI daemon is unavailable.koshell-ai-daemon(Bun, shared) — one process per user session, auto-spawned by the terminal and single-instance per user (the socket is the lock). Receives#?requests over IPC, owns Koshell provider/model/auth resolution, and runs one pi-backed streaming agent conversation per terminal session. The read-only terminal context tool loop is not wired yet, so context is push-only. Its source usesnode:APIs only, so Bun is the runtime and packager, not an API dependency.
The two communicate over newline-delimited JSON (JSONL) on a Unix domain socket.
crates/koshell-rs Rust foreground terminal process (binary `koshell`)
crates/koshell-proto Shared IPC message types
packages/ai-daemon Bun AI daemon
man/ Hand-written man pages (koshell.1, koshell.toml.5)
docs/ Public docs
reference/ Frozen pre-rewrite TypeScript prototype (not built)
- Rust 1.96 or newer (pinned via
rust-toolchain.toml) - Bun 1.3 or newer
Bun is the entire JS toolchain — package manager, task runner, test runner, the daemon's
runtime, and its packager. There is no Node or pnpm dependency. The floor is enforced
through package.json engines.
Rust:
cargo build # build all crates
cargo test # run Rust tests
cargo clippy # lint
cargo fmt # formatJS (Bun):
bun install
bun run check # format check, lint, typecheck, and tests across packagesmake check runs the full validation for both runtimes with the same commands
as CI.