A purely interactive shell.
No scripting, no POSIX compat, no plugins.
josh@mac ~/d/ish* main $
- User, host, abbreviated working directory, git branch, exit status color
- PWD shortens middle components:
~/.config/fishbecomes~/.c/fish - Green on success, red on failure
- Git branch from
.git/HEAD(cached, no subprocess). Detached HEAD shows short hash - Red
*when__DENV_DIRTY=1
| Key | Action |
|---|---|
| Ctrl+A / Home | Beginning of line |
| Ctrl+E / End | End of line |
| Ctrl+K | Kill to end of line |
| Ctrl+U | Kill to start of line |
| Ctrl+W / Ctrl+Backspace | Kill word backward |
| Ctrl+Y | Yank (paste) killed text |
| Ctrl+D | Delete forward / exit on empty line |
| Ctrl+C | Cancel line |
| Ctrl+L | Clear screen |
| Left / Right | Move by character |
| Ctrl+Left / Alt+B | Move word left |
| Ctrl+Right / Alt+F | Move word right |
Single kill ring shared across Ctrl+K/U/W. Full UTF-8 support.
Multiline editing: paste a multi-line command (with \ continuations or unclosed quotes) and move freely with Up/Down between lines. No more unwinding — the entire command lives in one buffer with free cursor movement.
| Key | Action |
|---|---|
| Up / Down | Prefix search through history |
| Ctrl+R | Fuzzy search (subsequence, case-insensitive) |
Fuzzy search opens a pager with matching characters highlighted in yellow. Up/Down to navigate, Enter to accept, Escape to cancel.
Scored ranking: results are ranked by match quality, not just recency. Contiguous matches (searching "target" finds literal target/release/ first), word-boundary alignment (deb prefers debug/ over scattered d-e-b), and PWD context (entries mentioning the current project rank higher). Optimal alignment via forward+backward scan finds the tightest match window — "test" in "the best test" finds the contiguous "test" at the end, not scattered letters.
Stored at ~/.local/share/ish/history (or $XDG_DATA_HOME/ish/history). Deduplicated on add.
| Key | Action |
|---|---|
| Ctrl+F | Open file finder |
| Down (in query) | Toggle hidden mode |
| Enter | Search / accept selection |
| Up / Down (in results) | Navigate |
| Backspace (in results) | Back to query |
| Escape | Cancel |
Type a pattern, press Enter, pick from results. The selected path is inserted at the cursor position — type vim , press Ctrl+F, pick a file, get vim src/main.rs.
Native implementation: recursive readdir with gitignore support, no external dependencies. Results sorted by depth (shallowest first) so nearby files appear before deeply nested ones. Normal mode respects .gitignore; hidden mode shows everything except .git/.
Sub-millisecond for gitignore-respecting searches (~0.85ms). No indexes, no background processes.
| Context | Behavior |
|---|---|
| Empty line | Inserts cd |
After cd |
Directories only |
$ prefix |
Environment variables |
| Everything else | Files and directories |
Completions display in a column-major grid (up to 6 columns, 10 visible rows). Navigate with arrow keys or Tab, accept with Enter, cancel with Escape. Typing filters live. Path completions sorted by modification time (most recent first) so recently-built or recently-edited files appear at the top.
Colors: blue for directories, cyan for symlinks, green for executables.
Pipes and chaining:
ls -la | grep foo > out.txt && echo done
cat err.log |& head # pipe stderr too
make 2> err.log # redirect stderr
make &> all.log # redirect both
cmd1 || cmd2 ; cmd3
Quoting:
echo 'literal $HOME' # single quotes: no expansion
echo "hello $USER" # double quotes: variables expand
echo it\'s\ a\ test # backslash escaping
Continuation: unclosed quotes, trailing |, &&, || prompt for more input.
Comments with #.
~/file # tilde → $HOME
$PATH # environment variables
$(whoami) # command substitution
`date` # backtick substitution
*.rs # glob: any characters
test? # glob: single character
src/**/*.py # glob: recursive descent
Expansion order: tilde, variables, command substitution, glob. Quoted characters skip expansion. No match on glob is an error.
| Command | Description |
|---|---|
cd [dir] |
Change directory. cd - for previous |
exit [code] |
Exit (warns if job suspended) |
fg |
Resume suspended job |
set [VAR [val]] |
Set env var. No args lists all |
unset VAR... |
Remove env vars |
alias [name [cmd]] |
Define/list aliases |
l [path] |
Native directory listing |
c |
Clear screen |
w / which / type |
Locate command |
echo [args] |
Print arguments |
pwd |
Print working directory |
true / false |
Return 0 / 1 |
copy-scrollback |
Copy session to clipboard via OSC 52 |
Equivalent to ls -plAhG, implemented without forking:
drwxr-xr-x 12 josh staff 384B Mar 17 10:23 src/
-rw-r--r-- 1 josh staff 1.2K Mar 17 09:15 Cargo.toml
lrwxr-xr-x 1 josh staff 11B Mar 10 14:02 link -> target/debug
-rwxr-xr-x 1 josh staff 184K Mar 17 17:54 ish
Human-readable sizes. Colors: blue dirs, cyan symlinks, green executables, red setuid. Sorted case-insensitively. Resolves owner/group names via getpwuid/getgrgid.
Define at the prompt or in config:
alias ll l
alias gs git status
Aliases expand inline when you press space. w/which/type check aliases first.
Ctrl+Z suspends the foreground job. fg resumes it. One job slot — simple and intentional. Shell warns before exiting with a suspended job (exit again to force).
If a pipeline was suspended mid-chain (a && b, suspended during a), fg resumes and continues the chain.
Automatic .envrc/.env loading when denv is in PATH. Runs on every cd with a fast-path check (file mtimes vs sentinel) to skip the subprocess when nothing changed. denv allow, denv deny, denv reload work as expected.
~/.config/ish/config.ish (or $XDG_CONFIG_HOME/ish/config.ish):
# Environment
set EDITOR nvim
set PAGER less
# Aliases
alias ll l
alias gs git status
alias .. "cd .."
Two directives: set and alias. Variables expand in values. Comments with #.
Every omission is deliberate. No scripting engine means no code injection, no source-based exploits, no eval chains. No if/for/while/functions means no control flow to hijack. No ${VAR} brace expansion means no expansion-based attacks. One suspended job (no & backgrounding) means no resource exhaustion through job spawning. No plugins means no supply chain.
The result: the entire shell is a single flat pipeline executor with a small, auditable attack surface. If you can't source it, you can't trick a user into source-ing it.
- No scripting.
ish script.shprints an error. - No POSIX compliance.
- No
source, noeval, no${VAR}brace expansion. - No
if/for/while/functions. - No plugins, no prompt customization, no themes.
- No background jobs (
&). One suspended job only.
Single binary crate, one dependency (libc).
The shell never subprocesses for its own operations — directory listing, git detection, glob expansion, environment loading checks are all native. Only user commands get fork/exec'd.
Signal handling uses the self-pipe pattern. Each pipeline gets its own process group. Terminal foreground control via tcsetpgrp. Raw mode via termios.