README.md

August 23, 2026 · View on GitHub

███╗   ██╗ █████╗ ████████╗██╗██╗   ██╗███████╗
████╗  ██║██╔══██╗╚══██╔══╝██║██║   ██║██╔════╝
██╔██╗ ██║███████║   ██║   ██║██║   ██║█████╗  
██║╚██╗██║██╔══██║   ██║   ██║╚██╗ ██╔╝██╔══╝  
██║ ╚████║██║  ██║   ██║   ██║ ╚████╔╝ ███████╗
╚═╝  ╚═══╝╚═╝  ╚═╝   ╚═╝   ╚═╝  ╚═══╝  ╚══════╝

Rust license status fork

[THE SHELL THAT IS ALSO GIT]

"The last binary you exec is the shell."

zshrs-native is zshrs — the first compiled Unix shell — with three sibling runtimes linked into the same address space and dispatched as shell builtins: zvcs serves git, arblang serves arb and its fzf-compatible finder, strykelang serves stryke and the @ prefix. Two of those are world firsts: no shell has ever compiled a version control system into itself, and no shell has ever compiled in an fzf-compatible finder. Everything here runs with no fork, no exec, no PATH lookup and no dynamic loader — the same treatment zshrs already gives cat, sort and find. The binary it builds is named zshrs: a drop-in superset of the thin shell, not a different one.

zshrs · zvcs · arb · strykelang · fusevm


Table of Contents


[0x00] OVERVIEW

CommandRuntimeWhat it displaces
gitzvcs — vendored gitoxide, every porcelain verb native/usr/bin/git
arbarblang — pipeline TUI, query engine, fzf finderfzf, jq, yq
stryke · st · sstrykelang — Perl-superset scripting; all three names it ships, each dispatching on its own argv[0]perl, awk
@ <code>strykelang, inline at the prompt
┌─────────────────────────────────────────────────────────────┐
│                   ONE PROCESS, ONE BINARY                   │
├───────────────┬───────────────┬──────────────┬──────────────┤
│     zshrs     │     zvcs      │   arblang    │  strykelang  │
│  shell + ZLE  │      git      │  arb + fzf   │    stryke    │
├───────────────┴───────────────┴──────────────┴──────────────┤
│                    FUSEVM EXECUTION CORE                    │
│            one VM · one JIT · one JIT disk cache            │
└─────────────────────────────────────────────────────────────┘

[0x01] INSTALL

git clone --recursive https://github.com/MenkeTechnologies/zshrs-native
cd zshrs-native
cargo build --release          # binary: target/release/zshrs
cargo install --path .         # installs as `zshrs`

--recursive is mandatory — the four runtimes are vendor/ submodules, and zvcs carries its own vendored gitoxide under src/ported.

git submodule update --remote  # move every pin to its runtime's current main

Or from the tap:

brew install MenkeTechnologies/menketech/zshrs-native      # the shell
brew install MenkeTechnologies/menketech/zshrs-native-all  # + zd, recorder, daemon

zshrs-native-all is the daily-driver package: the same four binaries zshrs-all ships — zshrs, zd, zshrs-recorder, zshrs-daemon — with the fat shell in place of the thin one. Each of these installs a binary named zshrs (and zd), so they conflict with each other and with zshrs / zshrs-all / zshrs-daemon; brew uninstall zshrs first if you have it.

zd and zshrs-recorder are bin targets of the zshrs package behind its zd / recorder features. A path dependency is not a workspace member, so neither cargo build -p zshrs --features zshrs/zd (refused: "cannot specify features for packages outside of workspace") nor re-exporting the features here can reach them — the release builds them through the submodule's own manifest, at the cost of compiling the zshrs lib a second time under that feature set. zshrs-daemon is a separate package and needs no features, so -p reaches it from the root build.

The formula is generated by .github/workflows/release.yml on a v* tag: the build matrix produces one tarball per target, the release job attaches them, and the tap job writes Formula/zshrs-native.rb with the SHA256 of each. A target that failed to build simply has no block in the formula rather than one with an empty checksum — v0.1.0 ships macOS arm64/x86_64 and Linux gnu arm64/x86_64, and no musl: the musl link picks up glibc's libtinfo.a, whose fortified __fprintf_chk / __sprintf_chk musl does not provide.


[0x02] WORLD FIRST: A VCS IN THE SHELL

Every Unix shell in the fifty-five years since the Bourne shell runs git the same way: fork, exec, wait. git is a foreign binary the shell knows nothing about beyond an exit status. Here it is a builtin — the whole of git, in the shell's own process. Not a status helper: zvcs serves every porcelain verb natively and has no fallback to an external git anywhere in it.

Prior artWhat it doesWhy it is not this
BusyBox / toyboxShell + utilities in one binary, no fork between themNo git applet — the set is coreutils-class
Nushell nu_plugin_gstatGit status as structured dataStatus only, and plugins are separate child processes: Nu "launches them as needed and communicates with them over stdin and stdout or local sockets"
git-shellRestricted login shell for SSH git accessPermits only server-side verbs and execs real git to serve them — confusable name, opposite architecture
bash enable -f, zsh zmodloadLoad native code into a shellNobody has shipped a git through either; both bind to private build-tree headers with no stable ABI
magit, posh-git, every shell git pluginRich git integrationAll shell out to the git binary

[0x03] WORLD FIRST: AN FZF ENGINE IN THE SHELL

Every fzf integration a shell has ever had spawns the fzf binary — zsh's CTRL-T/CTRL-R bindings, fzf-tab, fish's fzf.fish, PSFzf. The finder is a foreign process the shell pipes into and reads back. arb's is linked in.

arb --fzf is a drop-in for the binary. It honors FZF_DEFAULT_OPTS, FZF_DEFAULT_OPTS_FILE, and fzf's flag surface:

--preview  --preview-window  --bind      --expect     --nth      --with-nth
--delimiter --ansi           --tac       --tiebreak   --height   --layout
--reverse  --border          --color     --header     --header-lines
--info     --print-query     --exact     --no-sort    --filter   --cycle
--marker   --pointer         --ellipsis  --scroll-off --with-shell

So ZPWR_FZF='arb --fzf' keeps its prompt, layout, colors and key bindings, and an existing config drops in untouched. Scoring is in vendor/arb/src/tui.rs (fuzzy_score, exact_score, score_line); the flag, theme and preview layer is vendor/arb/src/fzf.rs.

Prior artWhat it doesWhy it is not this
ElvishThe nearest miss by a distance — real in-process fuzzy filtering for command history (histlist) and directory jumping (location mode), "a mini-fzf"Shell-internal UI modes, not a finder: cannot filter an arbitrary pipeline, honors none of fzf's CLI or env surface
Nushell exploreInteractive TUI pager over structured dataA viewer for nu values, not an fzf-compatible line filter
zsh fzf bindings, fzf-tab, fzf.fish, PSFzfDeep fzf integrationEvery one spawns the fzf binary
skimThe one fuzzy finder published as an embeddable Rust library rather than binary-onlyThe capability has been open to any Rust shell for years; none ships it in-process

[0x04] NO-FORK DISPATCH

OperationEvery other shellzshrs-native
git statusfork + exec + ld.so + libc initBuiltin — zero fork
… | fzffork + exec the fzf binaryBuiltin — zero fork
stryke -ne '…'fork + execBuiltin — zero fork
@ <code>not possibleInline stryke at the prompt

How a name reaches a runtime

The shell library owns a registry of host-registered native commands (vendor/zshrs/src/extensions/native_cmds.rs). It is the third builtin registry in the shell and the only one the binary writes rather than the library: EXT_BUILTIN_NAMES and the daemon's ZSHRS_BUILTIN_NAMES are const arrays owned by the zshrs crate, and the three runtimes cannot be dependencies of that crate — zvcs depends on its vendored gitoxide by path, which makes any dependent unpublishable. So src/main.rs registers them before the shell reads a line:

zsh::register_native_command("git", zvcs::run_argv);
zsh::register_native_command("arb", arb::cli::run_argv);
for name in ["stryke", "st", "s"] {
    zsh::register_native_command(name, stryke::cli::run_argv);
}

Every name a runtime ships a binary under is registered, not only the headline one: strykelang installs stryke, st and s, three identical entry points that differ by the argv[0] stryke::cli reads for itself.

Each runtime exposes one run_argv(&[String]) -> i32 taking the whole command line, argv[0] included, and each wraps it in its own hosted::run. That wrapper is what makes a program written to own its process safe to call inside one it does not: an exit from deep in a rendering loop unwinds back instead of taking the shell down, a panic becomes an exit status, and a git -C <dir> that moved the working directory is undone on the way out.

One thing the wrapper cannot undo is a child git spawns on purpose. git forks a git child for a handful of jobs — status asks one for the submodule summary, submodule update fetches through one, rebase drives am and commit through them — and each site spawns "itself", which under a host is the host: Command::new(current_exe()).args(["submodule", "summary"]) ran the shell and answered zshrs: can't open input file: submodule. zvcs resolves the spawn target through hosted::git_exe() instead — $ZVCS_GIT_EXE, else the first git on PATH that is not the host binary itself — so those children reach a git. They are the one place a fork survives, exactly where git itself forks.

A registered name occupies the builtin slot, so zsh's alias → function → builtin → external resolution order is intact and every way of asking for the binary on disk still gets it:

Result
git statusin-process — no fork, no PATH
git() { … }; git statusthe function; a native command shadows like cat does
command git statusthe git on PATH
/usr/bin/git statusthat binary — a /-qualified word is never a registry key
disable gitenable gitfalls through to PATH and back, per shell

[0x05] WHY A SEPARATE PACKAGE

The zshrs crate is published to crates.io. zvcs can never be: it depends on its own vendored gitoxide fork by path, and a path dependency — even an optional one behind a feature flag — makes a crate unpublishable. So the fat build cannot live in zshrs as a feature. It lives here, with all four runtimes as submodules.

The shell's whole REPL is in the zshrs repo's bins/zshrs.rs, a binary target, and Cargo binaries cannot be imported by another package. src/main.rs pulls it in as a module with #[path] instead — the same trick the original fat binary used before the strykelang monorepo was split apart (strykelang/bins/zshrs.rs, removed in strykelang 405bcfeeca with the note "Removed fat zshrs binary (lives in zshrs repo now)"; it was never recreated there). zshrs_main is pub for exactly this caller.

src/main.rs        registers the runtimes, then runs the shell
vendor/zshrs       the shell
vendor/zvcs        git
vendor/arb         arb + the fzf engine
vendor/strykelang  stryke

Both are load-bearing. Violating either breaks the build or the shell.

One fusevm. It exports 67 #[no_mangle] symbols across its aot/jit/ffi modules, and Cargo treats 0.x minors as semver-incompatible, so two copies in the graph is a duplicate-symbol link failure. Every runtime must request the same minor. cargo check does not link and will not catch a violation; cargo build will. Only one #[no_mangle] fusevm_aot_register_builtins may exist per binary — the zshrs dependency takes default-features = false to drop its definition, so strykelang's is the one that links.

panic = "unwind". The zshrs crate's own release profile uses panic = "abort". Profile settings come from the root package, so this one governs the whole graph, and it deliberately differs: three large runtimes now share the shell's address space, and an abort in any of them would take an interactive shell down. Unwinding lets the dispatch boundary catch a panic and return an exit status.

Linking the four surfaced three upstream conflicts, each fixed at its source:

RuntimeConflictResolution
strykelangfusevm 0.17 vs zshrs's 0.22Bumped to 0.22 with the zshrs dep (7455abc)
arbratatui 0.29 pinned unicode-width =0.2.0; zvcs's prodash needs ^0.2.2 — no version satisfies bothratatui 0.30, which moved the dep to ratatui-core (1bea073)
zvcsrusqlite 0.31 vs 0.32 elsewhere; libsqlite3-sys carries links = "sqlite3", so Cargo permits exactly oneBumped to 0.32 (12026bf)

[0x07] STATUS

State
Four runtimes link into one binaryWorking — one fusevm, one libsqlite3-sys
git / arb / stryke as builtinsWorking. whence -w reports builtin, ${+builtins[git]} is 1, and with PATH emptied git --version, stryke -e … and arb --filter … all still answer while ls reports command not found — nothing is resolved through PATH or spawned
@ <code> → strykeRegistered, not reached. zsh::try_stryke_dispatch is consulted from bins/zshrs.rs:process_line, which serves only the line-by-line script reader; the prompt and -c go through the ported lexer in the library, where a leading @ is still an ordinary character

[0xFF] LICENSE

MIT. See LICENSE.