# zsh-style hooks (`precmd` and `preexec`) An [experimental feature](experimental.md). Enable it persistently with `zsh-hooks = true` under `[experimental]` in `config.toml`, or per-invocation with `brush --enable-zsh-hooks`. zsh defines these hooks, and prompt frameworks, command timers, and history tools (atuin, starship, and friends) are built on them. brush provides a bash-native adaptation: the hooks live in the shell itself, with bash semantics, rather than in the `DEBUG`/`PROMPT_COMMAND` plumbing stock bash needs to approximate them. [bash-preexec](https://github.com/rcaloras/bash-preexec) is the adaptation of the same hooks the ecosystem already builds on, so it is what settles the details zsh leaves open for a bash shell, and what brush interoperates with — see [below](#relationship-to-bash-preexec). ## The hooks | Registry | When it runs | Arguments | |----------|--------------|-----------| | `precmd_functions` | before each prompt is displayed, ahead of `PROMPT_COMMAND` | none | | `preexec_functions` | after a command line is read, before it runs | the command line, as `$1` | The two arrays start out as `(precmd)` and `(preexec)`, so defining a function by either name is enough. Register more by appending — `preexec_functions+=(my_fn)` — and they run in array order. Each array is read once per dispatch, so a hook that edits one is heeded from the next dispatch on. Every entry names a **shell function**. The name is looked up as a single, already-expanded word: never re-split, glob-expanded, or resolved against `PATH`. An entry that isn't a shell function is skipped quietly, so a hook may be registered before it is defined. Hooks are active only in interactive shells, in the `$-` sense that also decides whether rc files are loaded. `brush -s < script` does not qualify and claims none of the bash-preexec state below; `brush -i -s < script` does, so a hook that reads standard input there consumes the script's own lines. `preexec` fires only for a line that runs a command: blank lines, comment-only lines, and syntax errors dispatch nothing, matching the `DEBUG` trap bash-preexec dispatches from. Only lines the user typed dispatch it — a command run from a key binding does not. ## What a hook sees - `$?`, `PIPESTATUS`, and `$_` hold what the last command left behind. All three are restored before every hook and again afterwards, so nothing a hook runs is visible to the next hook, to `PROMPT_COMMAND`, or to the next command. - A hook that fails has its error reported; the remaining hooks still run. Under `set -e` a non-zero return exits the shell instead, exactly as it does from `PROMPT_COMMAND`. - A hook that calls `exit` exits the shell. Nothing after it runs: no later hook, no `PROMPT_COMMAND`, and not the command line `preexec` was dispatched for. ## Relationship to bash-preexec The two must not both be live: bash-preexec's `DEBUG` trap would dispatch the same hooks a second time. So brush claims bash-preexec's interlock as well as its behavior — the `bash_preexec_imported` and legacy `__bp_imported` inclusion guards, along with the two arrays, are set before profile and rc files load. A copy of bash-preexec sourced afterwards — including the one tools such as atuin inline into their `init` output — finds itself already loaded and returns, leaving the hooks to brush rather than installing its `DEBUG`-trap emulation. [ble.sh](https://github.com/akinomyoga/ble.sh) takes the same approach. All four are set the way `name=value` sets a variable: nothing is exported, so a child bash still gets the real implementation, but one that already existed keeps its attributes. A guard or registry inherited from the environment therefore stays exported. Because brush seeds the arrays before rc files rather than appending after them, an rc file that appends gets `(precmd mine)`, where sourcing bash-preexec after the same append would give `(mine precmd)`. Clearing the guards is not a supported way to hand the hooks back. brush keeps dispatching, so a bash-preexec sourced after they are cleared installs itself over the same registries and every hook runs twice. To use the real bash-preexec, leave this feature off. The claim covers bash-preexec's documented surface and no more: the two hook names, the two arrays, and the inclusion guards. None of its `__bp_*` internals exist, and `precmd` hooks are not reachable through `PROMPT_COMMAND` the way its dispatcher is. ### `BP_PIPESTATUS` is not provided bash-preexec offers `BP_PIPESTATUS` because its hooks run from `PROMPT_COMMAND`, by which point the real `PIPESTATUS` is gone. brush restores `PIPESTATUS` itself, so a hook reads it directly. There is no copy, and none is planned. A hook that must work under both can fall back in one expansion: ```bash statuses=("${BP_PIPESTATUS[@]:-${PIPESTATUS[@]}}") ``` ## Differences | | brush | zsh | bash-preexec | |---|---|---|---| | `preexec` `$1` | the line as typed, interior newlines included (the trailing one is trimmed) | the same | what `history 1` reports, which under `cmdhist` joins the command onto one line | | `preexec` `$2` and `$3` | not passed | single-line and fully-expanded forms of the command | not passed | | entries that aren't shell functions | skipped | skipped | run, when `type -t` finds a builtin or a program | | `PIPESTATUS` inside a hook | the real one | n/a | gone; `BP_PIPESTATUS` holds a copy | brush matches zsh on `$1`: zsh passes the string the user typed, verbatim (verified against zsh 5.9). A hook that needs the command on one line can read `history 1` itself — it is already in the history when `preexec` runs. Only functions run because that is what zsh does and what bash-preexec's own usage notes tell you to register. Admitting programs would let a misspelled or not-yet-defined function name fall through to a same-named program on `PATH` and run it before every prompt. Wrap such a command in a function (`log_cmd() { logger "$1"; }`) and register that.