nucleic-brush
A fork of brush — the Bo(u)rn(e) RUsty SHell by reuben olinsky — carrying the patches Nucleic needs to build nash, its agent shell.
Pinned at upstream tag brush-shell-v0.4.0 (commit 96a26d0).
Looking for the shell itself? Go upstream. This repository is not a distribution of brush and publishes no binaries or crates — it is a vendored fork whose only consumer is Nucleic's
shell/workspace. Everything here that isn't marked as a fork change is upstream's work, under upstream's MIT license.
What the fork adds
Nucleic runs agent-authored shell commands and needs to see what they do — the commands, their exits, and the data crossing pipes and redirections — from inside the shell rather than by wrapping it. That is what these patches provide. Everything else is upstream brush, unmodified.
Every divergence carries a // nash: comment, so the complete diff against upstream is
one grep away:
$ grep -rn "nash:" --include="*.rs" --include="*.yaml" .
1. The observation gate
A process-global, verdict-shaped hook at the shell's command choke point
(brush-core/src/gate.rs). The default implementation allows everything and observes
nothing, so an unconfigured shell behaves exactly like upstream brush — including
the fast paths: pipe teeing is only wired up when a real gate is installed.
| Surface | What it reports |
|---|---|
Gate::on_exec / on_exit |
every simple command (builtin, function, external): argv, cwd, redirections, pipeline slot; then its exit code (128+signal for signal deaths) |
Gate::on_pipe |
the bytes crossing each a | b link once it drains, with both stages rendered from their pre-expansion AST text |
Gate::on_cmdsub |
the trimmed output of $(…) / backtick substitutions |
on_exec returns a Verdict, so the same seam supports enforcement (Deny) later
without further changes to this crate. Today Nucleic only observes.
The plumbing behind those hooks:
| File | Change |
|---|---|
brush-core/src/gate.rs (new), lib.rs |
the Gate trait, event types, and process-global install/allow-all default |
brush-core/src/commands.rs |
SimpleCommand::execute split into a gate wrapper + execute_inner; on_exec runs before dispatch and correlates completion across all three spawn-result variants |
brush-core/src/processes.rs |
ChildProcess.gate_token and on_exit reporting from both wait() and poll(), exactly once |
brush-core/src/interp.rs |
redirection recording (>, >>, <, 2>, &>, <>, heredocs, here-strings) for post-run read-back; a tee interposed on each pipe link when observation is active — a pooled copier thread mirrors a bounded prefix while preserving backpressure and SIGPIPE, with a splice passthrough after the cap |
brush-core/src/expansion.rs |
command-substitution arm reports its output to the gate |
brush-shell/src/entry.rs |
set_exit_hook seam so an embedder can flush buffered events before process::exit |
2. Upstream-candidate fixes
Bugs found by running real-world scripts through brush. These aren't Nucleic-specific and belong upstream:
| File | Fix |
|---|---|
brush-core/src/expansion.rs |
literal unquoted word text is no longer field-split (bash splits only the results of expansions) while keeping its glob characters active. Parameter-expansion substitutions convert back to splittable at the expansion boundary; ExpanderOptions.field_split_literal_text lets data-string callers opt in. Repairs 3 previously known_failure IFS tests. |
brush-core/src/completion.rs |
compgen -W opts into field_split_literal_text — the -W string is data, not source text |
brush-builtins/src/dot.rs |
. / source searches PATH for slashless operands when sourcepath is set, as bash and POSIX require. Fixes git-subtree, which sources git-sh-setup this way. |
3. Branding
The binary this workspace builds is still named brush, but identifies itself as nash
so an agent-facing shell doesn't misrepresent what it is:
| File | Change |
|---|---|
brush-shell/src/productinfo.rs |
PRODUCT_NAME → nash; display string reads nash (Nucleic agent shell, brush fork) … |
brush-shell/src/args.rs |
usage and version strings rebranded |
brush-core/src/prompt.rs |
\$ renders the atom sign ⚛ (U+269B) for non-root; root still shows # |
NUCLEIC_FORK.md tracks the same list at file granularity, with the
current verification status.
How it's consumed
Nucleic's shell/ workspace depends on brush-shell and brush-parser by path. The
nash binary is a thin wrapper that loads a root-owned operator policy, installs its
Gate implementation (nash-observe), flushes events at exit, and otherwise defers
entirely to brush's bash-compatible CLI. None of the fork's crates are published; the
brush binary built from here is a development artifact.
Building and testing
Standard upstream workflow — a recent Rust toolchain (see rust-version in
Cargo.toml), then:
$ cargo build --release
$ cargo test --workspace
$ cargo test -p brush-shell --test brush-compat-tests # the bash-oracle compat suite
The compat suite is the fork's regression gate: it must stay level with a pristine
build of the pinned upstream tag. As last recorded in
NUCLEIC_FORK.md, it does — the only failures are environmental
ones a baseline run reproduces, plus the 3 IFS tests the expansion fix repairs.
Staying in sync with upstream
- Diff this tree against the pinned upstream tag.
- Re-vendor the new tag.
- Re-apply the fork patches —
grep -rn "nash:"on the old tree enumerates all of them. - Re-run the brush compat suite and Nucleic's transcript-replay corpus.
- Update the pin and the patch table in
NUCLEIC_FORK.md.
Fixes that aren't Nucleic-specific should go to upstream rather than accumulate here — a smaller diff is a cheaper rebase.
Upstream documentation
Retained as vendored, and still accurate for everything the fork doesn't touch:
- Compatibility reference — what does and doesn't work versus bash
- Configuration
- Building from source · Running tests
- Full docs index
Credits and license
brush is written by reuben olinsky and its contributors: https://github.com/reubeno/brush. It is an excellent piece of work, and the reason Nucleic could get an observable shell by patching rather than by writing one.
This fork is distributed under the same MIT license as upstream; the copyright notice is unchanged. Upstream's community guidelines and contribution guidelines are vendored here and describe upstream's process — follow them there, not in this repository.