2026-08-10 22:12:29 -07:00
# nucleic-brush
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
A fork of [**brush** ](https://github.com/reubeno/brush ) — the Bo(u)rn(e) RUsty SHell by
2026-08-11 20:11:26 -07:00
reuben olinsky — carrying the patches Nucleic needs to build **hash** , its agent shell.
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
Pinned at upstream tag ** `brush-shell-v0.4.0` ** (commit `96a26d0` ).
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
> **Looking for the shell itself?** Go [upstream](https://github.com/reubeno/brush).
> 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](LICENSE).
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
## What the fork adds
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
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.
2026-07-18 05:19:31 -07:00
2026-08-11 20:11:26 -07:00
Every divergence carries a `// hydrashell:` comment, so the complete diff against upstream is
2026-08-10 22:12:29 -07:00
one grep away:
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
```console
2026-08-11 20:11:26 -07:00
$ grep -rn "hydrashell:" --include= "*.rs" --include= "*.yaml" .
2026-07-18 05:19:31 -07:00
```
2026-08-10 22:12:29 -07:00
### 1. The observation gate
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
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.
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
| 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 |
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
`on_exec` returns a `Verdict` , so the same seam supports enforcement (`Deny` ) later
without further changes to this crate. Today Nucleic only observes.
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
The plumbing behind those hooks:
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
| 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` |
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
### 2. Upstream-candidate fixes
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
Bugs found by running real-world scripts through brush. These aren't Nucleic-specific
and belong upstream:
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
| 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. |
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
### 3. Branding
2026-07-18 05:19:31 -07:00
2026-08-11 20:11:26 -07:00
The binary this workspace builds is still named `brush` , but identifies itself as hash
2026-08-10 22:12:29 -07:00
so an agent-facing shell doesn't misrepresent what it is:
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
| File | Change |
| --- | --- |
2026-08-11 20:11:26 -07:00
| `brush-shell/src/productinfo.rs` | `PRODUCT_NAME` → `hydrashell` ; display string reads `hydrashell (hash — Hydrangea agent shell, brush fork) …` |
2026-08-10 22:12:29 -07:00
| `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 `#` |
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
[`NUCLEIC_FORK.md` ](NUCLEIC_FORK.md ) tracks the same list at file granularity, with the
current verification status.
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
## How it's consumed
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
Nucleic's `shell/` workspace depends on `brush-shell` and `brush-parser` by path. The
2026-08-11 20:11:26 -07:00
`hydrashell` binary is a thin wrapper that loads a root-owned operator policy, installs its
`Gate` implementation (`hydrashell-observe` ), flushes events at exit, and otherwise defers
2026-08-10 22:12:29 -07:00
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.
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
## Building and testing
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
Standard upstream workflow — a recent Rust toolchain (see `rust-version` in
[`Cargo.toml` ](Cargo.toml )), then:
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
```console
$ cargo build --release
$ cargo test --workspace
$ cargo test -p brush-shell --test brush-compat-tests # the bash-oracle compat suite
2026-07-18 05:19:31 -07:00
```
2026-08-10 22:12:29 -07:00
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` ](NUCLEIC_FORK.md ), it does — the only failures are environmental
ones a baseline run reproduces, plus the 3 IFS tests the expansion fix repairs.
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
## Staying in sync with upstream
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
1. Diff this tree against the pinned upstream tag.
2. Re-vendor the new tag.
2026-08-11 20:11:26 -07:00
3. Re-apply the fork patches — `grep -rn "hydrashell:"` on the old tree enumerates all of
2026-08-10 22:12:29 -07:00
them.
4. Re-run the brush compat suite and Nucleic's transcript-replay corpus.
5. Update the pin and the patch table in [`NUCLEIC_FORK.md` ](NUCLEIC_FORK.md ).
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
Fixes that aren't Nucleic-specific should go to
[upstream ](https://github.com/reubeno/brush/issues ) rather than accumulate here — a
smaller diff is a cheaper rebase.
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
## Upstream documentation
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
Retained as vendored, and still accurate for everything the fork doesn't touch:
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
* [Compatibility reference ](docs/reference/compatibility.md ) — what does and doesn't work versus bash
* [Configuration ](docs/reference/configuration.md )
* [Building from source ](docs/how-to/build.md ) · [Running tests ](docs/how-to/run-tests.md )
* [Full docs index ](docs/README.md )
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
## Credits and license
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
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.
2026-07-18 05:19:31 -07:00
2026-08-10 22:12:29 -07:00
This fork is distributed under the same [MIT license ](LICENSE ) as upstream; the
copyright notice is unchanged. Upstream's [community
guidelines ](CODE_OF_CONDUCT.md ) and [contribution
guidelines ](CONTRIBUTING.md ) are vendored here and describe *upstream's* process —
follow them there, not in this repository.