# brush: a Bash-compatible shell written in Rust, usable as a library

> Most shells are either compatible or embeddable. brush claims both by making the interactive shell and the Rust crates the same code, then testing every change against Bash itself.

**reubeno/brush** — 🐚bash/POSIX-compatible shell implemented in Rust 🦀

- Repository: https://github.com/reubeno/brush
- Website: https://brush.sh
- Stars: 2,251 · Forks: 128
- Language: Rust
- License: MIT
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/reubeno-brush

## One implementation, two audiences

The README's central claim is structural rather than functional: the brush shell and its libraries are the same code. Embedding `brush-core` gives your software the behaviour the shell has, and an improvement to one is an improvement to the other. That single property changes how you should read the project, because it means the interactive shell is not a downstream consumer of a library, it is the library's primary test surface.

The README offers two distinct ways in. Run it as your everyday shell, where it loads your existing `.bashrc`, aliases, functions and completions, runs the scripts you already have, and adds history-based suggestions plus live syntax highlighting. Or build with it, where `brush-core` is a tested implementation of Bash semantics your own Rust software can embed and extend.

The crate table in the README separates the pieces by function: `brush-core` for the shell semantics, `brush-parser` to turn shell source into a syntax tree, `brush-interactive` for the line editing layer, `brush-builtins`, `brush-coreutils-builtins` and `brush-experimental-builtins` for the builtin sets, and `brush-test-harness` for compatibility testing.

The parser deserves separate attention because it is already used by other projects. The README states that brush-parser is used on its own by Zed, Vite+ and oh-my-pi to parse shell commands. Three independent consumers, including an editor, is a considerably stronger signal than the star count suggests.

## Compatibility is measured against Bash, and it is not complete

The README is unusually direct about the state of the project. Under the heading about scripts, it says not everything is there yet, and names what is missing: `select`, `wait -n`, `disown`, some traps, and a set of edge cases. It then points to the compatibility reference, which lists what works, what is partial, and what is not implemented.

That admission is paired with a testing strategy that explains why the gaps are enumerated rather than guessed at. The README says keeping Bash behaviour means being faithful, so every change is tested against Bash itself. The badge in the header reports 2,500+ compatibility tests, pointing at the `brush-shell/tests/cases` directory, and the repository tree confirms that directory exists alongside a dedicated `brush-test-harness` crate.

Differential testing against the reference implementation is the right approach for a compatibility project and it is also why the gaps are knowable. When you can run the same script through both shells and diff the result, the set of behaviours you have not implemented becomes a list rather than a mystery.

The interpreter features that do work are the ones scripts actually depend on. The README lists builtins, expansions, arrays, redirections and options, including `set -e`, `pipefail`, `extglob` and `globstar`. Those are the flags that break naive reimplementations, and their presence is a better signal of intent than a broad feature list would be.

## Installing a shell you will actually live in

The primary install path is a piped curl invocation:

```sh
curl --proto '=https' --tlsv1.2 -fsSL https://brush.sh/install.sh | sh
```

The `--proto '=https'` and `--tlsv1.2` flags are the current recommendation for piping a script to a shell, and their presence tells you the maintainers care about the failure mode of that pattern.

The package manager routes are more conventional. Homebrew, cargo and cargo-binstall each get a line:

```sh
brew install brush
cargo install --locked brush-shell
cargo binstall brush-shell
```

The `--locked` flag on the cargo install is the detail worth noticing, because it means the published `Cargo.lock` is respected so you get the tested dependency versions rather than whatever is current.

The README then lists the distributions that carry brush: Homebrew, Arch Linux, Fedora via Terra, MSYS2, Nix, and more, with a repology link for the full list. A shell that exists as a package in six ecosystems is a different proposition from one you build yourself, and MSYS2 in particular indicates the Windows side is taken seriously enough to be packaged.

Once installed, running `brush` reads the same startup files Bash does. To differentiate it, add a `~/.brushrc`. That file is the documented hook for brush-specific settings, and the README points at the TOML configuration file for options such as enabling highlighting under a `ui` section.

## What embedding brush-core looks like

The README shows the whole embedding API in a short snippet, and it is short because the design assumes you want a shell object you can run strings against:

```rust
let mut shell = brush_core::Shell::builder().build().await?;

let result = shell
    .run_string(
        r#"greet() { echo "Hello, $1!"; }; greet world"#,
        &brush_core::SourceInfo::default(),
        &shell.default_exec_params(),
    )
    .await?;

assert!(result.is_success());
```

Four things are visible in that example. You build a `Shell` through a builder. You call `run_string` with the source text, a `SourceInfo` that would carry file and line context in real use, and execution parameters you can vary. And you get back a result you can assert on. There is no separate interpreter handle and no global state to initialise.

The README points to three specific examples in `brush-core/examples`: registering a builtin written in Rust alongside the standard ones, calling a shell function from Rust with control over its I/O, and parsing a script then serialising its syntax tree from `brush-parser/examples/serde.rs`. The I/O control point is the one that matters most for embedding, because a tool that needs to capture what the shell wrote cannot use a shell that writes straight to the terminal.

Extension points are described as intentional rather than incidental. Custom builtins plug in alongside the standard ones, and the README says the intent is to open more of the shell's internals the same way, so tools can observe and extend it natively. For an embeddable interpreter, having the builtins be a registry rather than a switch statement is what makes that possible.

## Interactive features that are additions, not replacements

The feature list for the shell is arranged so that the additions are visible as additions. Auto-suggestions are history-based hints as you type, on by default, powered by reedline, the line editor from the nushell project. Syntax highlighting is live as you type and one setting away, either the `brush --enable-highlighting` flag or `syntax-highlighting = true` under `[ui]` in the TOML config file.

```sh
brush --enable-highlighting
```

The compatibility items are grouped separately, and they are the ones that decide whether you can switch. Your configuration comes with you: `.bashrc`, `.bash_profile`, aliases, functions, `PS1`, `PROMPT_COMMAND`, and prompt tools like starship all work as they do in Bash. Programmable completion works with the bash-completion package already installed, so `git`, `docker` and `systemctl` complete as usual. Job control covers background jobs, suspend and resume, `fg`, `bg` and `jobs`.

The experimental extras are explicitly off by default: zsh-style `precmd` and `preexec` hooks, and terminal shell integration for VS Code and iTerm2 among others. Off by default is the correct choice for a compatibility project, because a hook that fires on the wrong timing will corrupt a prompt in ways that are hard to diagnose.

The reuse of reedline is worth noting as an engineering judgement. Rather than writing a line editor, the project adopted a maintained one and accepted its conventions, which is why the interactive layer is the least risky part of adopting brush as a daily shell.

## Workspace layout and what the lint settings reveal

The `Cargo.toml` is a workspace with resolver version 3, edition 2024, and a rust-version floor of 1.88.0. The default members list is just `brush-shell`, so a bare cargo command at the root builds the shell rather than the whole workspace, which is the right default for anyone who just wants a binary.

The workspace lint configuration is unusually strict and, for anyone reading the code, more informative than a contributing guide:

```toml
[workspace.lints.rust]
warnings = { level = "deny" }
future_incompatible = { level = "deny" }
missing_docs = { level = "deny" }
nonstandard_style = { level = "deny" }
unsafe_op_in_unsafe_fn = "deny"
```

Treating warnings as errors, requiring documentation on every public item, and denying unsafe operations inside unsafe functions are the settings of a library that intends to be depended on by others. `unsafe_op_in_unsafe_fn` in particular is a Rust 2024 edition requirement rather than a preference, and its presence alongside the others says the unsafe surface is being kept deliberately small.

The tree also shows the infrastructure a serious project accumulates: `benchmarks/`, `fuzz/` as a workspace member, `e2e/`, `xtask/` for build automation, `samples/`, `deny.toml` for dependency auditing, `cliff.toml` with a `.cliffignore` for changelog generation, `release-plz.toml` for releases, and `zizmor.yml` for the GitHub Actions security scanner. The last three releases are named brush-shell-v0.4.0 from 2026-05-03, brush-shell-v0.3.0 from 2025-11-17 and brush-shell-v0.2.23, so the version line is pre-1.0 and still moving quickly.

## Conclusion

brush is worth watching for two different reasons, and conflating them leads to bad expectations. As a terminal shell, it is an early but real Bash replacement whose compatibility gaps are documented rather than hidden, and whose interactive extras come from reedline. As a Rust library, it is the more interesting proposition, because brush-core and brush-parser are already consumed by Zed and Vite+, which is a stronger adoption signal than a download count. Start with the compatibility reference rather than the README, because that page tells you whether your shell profile will work. If you are writing Rust rather than running a shell, read the examples directory instead, since that is where the extension story is documented.

## FAQ

### What is brush and what language is it written in?

brush is a Bash-compatible shell written in Rust. It runs as an ordinary terminal shell that reads your Bash startup files, and the same implementation is published as crates so Rust programs can embed Bash semantics. The repository is MIT licensed.

### Is brush a complete replacement for Bash?

Not yet, and the README says so. Missing features include select, wait -n, disown and some traps, along with a set of edge cases. The compatibility reference lists what works, what is partial and what is unimplemented, and that page is the one to read before switching a working shell.

### Which projects use brush-parser?

The README names Zed, Vite+ and oh-my-pi as projects that use brush-parser on its own to parse shell commands. That is separate from using brush as a shell, and it is why the parser is published as a crate separate from brush-core.

### How do I install brush on Linux or macOS?

The primary path is the install script from brush.sh piped to sh. Alternatively use brew install brush on macOS or Linux, cargo install --locked brush-shell from crates.io, or cargo binstall brush-shell for a prebuilt binary. Packages also exist for Arch Linux, Fedora via Terra, MSYS2 and Nix.

### Can I embed a shell in a Rust program using brush?

Yes. brush-core exposes a Shell type you build with a builder and then call run_string on, returning a result you can assert on. The repository includes examples for registering a Rust builtin, calling a shell function with controlled I/O, and parsing a script into a syntax tree.

## Sources

- [License: MIT](https://github.com/reubeno/brush/blob/main/LICENSE)
- [Project website](https://brush.sh)
- [README](https://github.com/reubeno/brush/blob/main/README.md)
- [Releases](https://github.com/reubeno/brush/releases)
- [reubeno/brush on GitHub](https://github.com/reubeno/brush)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/reubeno-brush
