A modern, modular, git-aware bash prompt. Two-line layout, tiered glyphs that do not break on a non-Nerd-Font terminal, OSC 133 semantic prompt marks, transient prompt, async rendering. Pure bash 4.4+, zero dependencies, ~500 lines of source split across 12 modules.
What this is. A
.bashrcthat surfaces the context you actually need (where am I, which branch, dirty?, ahead/behind, runtime+version, last command duration, exit code) without cluttering every prompt, without tofu on terminals that lack fancy glyphs, and without slowing the shell down on a large repo. It is also a small, readable codebase: a SPEC, six ADRs, a render-pipeline diagram, tests, and CI. The whole thing fits in a ten-minute read.
Status: v2 release candidates. Tagged v2.0.0-rc.1 (transient), v2.0.0-rc.2
(async), v2.0.0-rc.3 (demo + install.sh + README rewrite). v2.0.0 lands
with the CI matrix and a GitHub Release (M7).
Documentation · Decisions · Changelog · Repo
- What you see
- Install
- Prompt anatomy
- Configuration
- Presets
- Module reference
- Compatibility
- Performance
- Customization
- Troubleshooting
- Repository layout
- Contributing
- License
The animated demo above shows a typical sequence: enter a clean repo, touch
a file (dirty marker appears), git stash (stash count), a slow command
(timer), a failed command (red prompt + exit code), leave the repo (simple
prompt). The same flow in ASCII, for terminals where the GIF is not
inlined:
~/projects/bash-gitaware on main@74fd6cf
↳ feat(install): add install.sh with --replace / --append modes
❯ git status
On branch main
nothing to commit, working tree clean
~/projects/bash-gitaware on main@74fd6cf
↳ feat(install): add install.sh with --replace / --append modes
❯ printf 'x' > note.txt
~/projects/bash-gitaware on main@74fd6cf ●
↳ feat(install): add install.sh with --replace / --append modes
❯ git stash
~/projects/bash-gitaware on main@74fd6cf ≡1
↳ feat(install): add install.sh with --replace / --append modes
❯ sleep 3; false
took 3s ✘1
↳ feat(install): add install.sh with --replace / --append modes
❯
Outside a git tree, the prompt collapses to its essentials:
~/Documents
❯
Inside a Node / Python / Rust / Go project, the toolchain shows up next to the git block:
~/projects/myapp on main@a1b2c3d via node v20.11.0
↳ chore: bump deps
❯
A non-zero exit colors the prompt symbol red and adds ✘N:
~/projects/myapp on main@a1b2c3d
↳ chore: bump deps
took 4s ✘2
↳ chore: bump deps
❯
After Enter, the previous prompt collapses to a one-line form in the
scrollback:
❯ git status
On branch main
...
❯ printf 'x' > note.txt
❯ git stash
Saved working directory and index state ...
❯
git clone https://github.com/SzematPro/bash-gitaware ~/.bash-gitaware
cd ~/.bash-gitaware
./install.sh # replaces ~/.bashrc (creates a timestamped backup first)
exec bash # or: open a new terminalIf you have an existing ~/.bashrc you want to keep, append a source line
instead:
./install.sh --append # idempotent; safe to run twice
exec bashThe script:
- Warns (does not refuse) if your
bashis older than 4.4. macOS ships bash 3.2 by default; install a modern bash withbrew install bash && chsh -s "$(brew --prefix)/bin/bash". - Backs up
~/.bashrcto~/.bashrc.bak-<unix-ts>before--replace. Skip with--no-backup. - Accepts
--target FILEfor testing (write somewhere other than$HOME/.bashrc).
See ./install.sh --help for all flags.
bash --rcfile new.bashrc -iA throwaway interactive shell with the prompt loaded; nothing in your dotfiles is touched.
The prompt is two lines by default, with an optional middle line for the last commit subject:
[chroot] [container] [user@host ] path[ on branch[@hash][ STATE][ ↑ahead][ ↓behind][ ●][ ≡stash][ via runtime ...][ took duration][ ✘exit]]
↳ last commit subject
❯
Every element is conditional. Most prompts use only a small subset of it.
| Element | When | Notes |
|---|---|---|
[chroot] |
$debian_chroot is set |
Amber tag |
[container] |
Running in Docker / Podman / LXC | Detected once at shell startup via /.dockerenv, /run/.containerenv, /proc/1/cgroup |
user@host |
SSH session OR running as root (or always if BASHGITAWARE_SHOW_HOST=always) |
Host in bold amber over SSH |
path |
Always | Repo-relative inside a git work tree (repo-name + path-within-repo); ~-relative under $HOME; trimmed to the last BASHGITAWARE_PATH_MAXDEPTH components (default 3) with a leading … |
on branch |
Inside a git work tree | Branch label from .git/HEAD (no git status needed on the cheap path) |
@hash |
Branch is a real branch (not detached) | First 7 chars of the commit oid |
STATE |
Mid-operation | REBASE n/m, AM n/m, MERGING, CHERRY-PICK, REVERTING, BISECTING — filesystem checks only |
↑N / ↓N |
Local has unpushed commits / remote has commits we lack | Async — appears on the next prompt cycle on a large repo |
● |
Working tree has changes (tracked or untracked) | Async — same |
≡N |
git stash has N entries |
Sync, gated by .git/refs/stash existing |
via runtime |
Node / Python / Rust / Go project marker in $PWD |
Cached per (PWD, VIRTUAL_ENV, CONDA_DEFAULT_ENV) |
took Ns |
Last command took ≥ BASHGITAWARE_TIMER_THRESHOLD seconds (default 2) |
Yellow, after the git/runtime block |
✘N |
Last command failed | Bold red, last on the line |
↳ subject |
Optional commit-line; BASHGITAWARE_COMMIT_LINE=0 to hide |
Truncated to $COLUMNS - 3 with the configured ellipsis |
❯ |
Always (the prompt symbol) | Bold green on success, bold red on failure; > on the ascii tier; # for root |
While the async path is computing on a large repo, a faint … (or ... on
ascii) trails the branch info to signal "still working"; it clears on the
next prompt cycle.
After Enter, the previous prompt collapses to ❯ <typed-command> (the
"transient prompt"). Disable with BASHGITAWARE_TRANSIENT=0.
All settings are environment variables. Export them before ~/.bashrc is
sourced (e.g. in /etc/profile, ~/.profile, or near the top of the file
itself).
| Variable | Default | Effect |
|---|---|---|
BASHGITAWARE_PRESET |
default |
Coherent bundle of defaults — minimal, default, powerline, full. See Presets. |
BASHGITAWARE_GLYPHS |
auto | Force a glyph tier: nerd, unicode, ascii. Auto picks unicode on a UTF-8 locale, else ascii. Never auto-picks nerd — a UTF-8 locale does not imply the font has Nerd glyphs. |
BASHGITAWARE_NERD_FONT |
0 |
Shorthand for BASHGITAWARE_GLYPHS=nerd. |
BASHGITAWARE_COMMIT_LINE |
1 |
Show the ↳ subject line below the context line. 0 to hide. |
BASHGITAWARE_RUNTIME |
1 |
Show via node / via python / via rust / via go in language projects. 0 to hide. |
BASHGITAWARE_OSC |
1 |
Emit OSC 133 semantic marks (A/B/C/D) and OSC 7 cwd reports. 0 to suppress. |
BASHGITAWARE_TRANSIENT |
1 |
Collapse the previous prompt to ❯ <command> on submit. 0 to disable. |
BASHGITAWARE_ASYNC |
1 |
Compute the expensive part of git status in the background and surface it on the next prompt. 0 to render fully synchronous. |
BASHGITAWARE_TIMER_THRESHOLD |
2 |
Show took Ns only when the command took at least N seconds. |
BASHGITAWARE_PATH_MAXDEPTH |
3 |
Trim the path to the last N components, with a leading …. 0 for unlimited. |
BASHGITAWARE_SHOW_HOST |
auto |
always / auto / never. Auto = SSH session or root. |
NO_COLOR |
unset | Standard NO_COLOR (https://no-color.org). When set, the prompt renders in plain text. |
A preset sets a coherent set of defaults; individual BASHGITAWARE_*
variables still override anything a preset sets (:= parameter expansion,
not =).
| Preset | Glyphs | Commit line | Runtime modules | User@host | Notes |
|---|---|---|---|---|---|
minimal |
ascii |
off | off | auto | The cleanest look: ASCII-only, no commit line, no runtime modules. |
default |
auto | on | on | auto | Current behavior with no preset. UTF-8 unicode glyphs on a UTF-8 locale. |
powerline |
nerd |
on | on | auto | Nerd Font glyphs on by default. Falls back gracefully if the font lacks them. The arrow-segment render style is planned for a future release; in v2 the preset is a glyph default. |
full |
auto | on | on | always | Everything. user@host is shown even on a local session. |
export BASHGITAWARE_PRESET=minimal
exec bashThe single-file new.bashrc is generated from twelve modules under lib/.
Edit the modules, run make build, commit the regenerated artifact. CI
verifies the artifact is current.
| Module | Responsibility |
|---|---|
lib/00-options.bash |
Read BASHGITAWARE_* knobs; apply presets via ${var:=value} so user overrides win. |
lib/10-detect.bash |
Detect SSH session, container, UTF-8 locale, terminal title support, root, color capability — once at startup, not per prompt. |
lib/20-palette.bash |
256-color palette + tiered glyph selection (nerd > unicode > ascii). |
lib/30-git.bash |
Cheap (sync) git info: rev-parse + filesystem state + branch label from .git/HEAD. Expensive (async) info compute function for the background job. |
lib/40-path.bash |
Repo-relative path inside a git work tree; ~-relative outside; trim to BASHGITAWARE_PATH_MAXDEPTH. |
lib/50-runtime.bash |
Node / Python / Rust / Go detection + version, cached per (PWD, VIRTUAL_ENV, CONDA_DEFAULT_ENV). |
lib/60-render.bash |
__bga_prompt: assemble PS1 from parts; emit OSC marks; record the transient state for lib/80-transient.bash. |
lib/70-osc.bash |
OSC 133 A/B/C/D semantic marks, OSC 7 cwd, OSC 0/2 window title. BASHGITAWARE_OSC=0 short-circuits all of them. |
lib/80-transient.bash |
Collapse the previous prompt to ❯ <command> on Enter, via bind -x + a readline macro on \C-m. |
lib/85-async.bash |
Spawn the background git-status subshell, parse its cache file defensively, install the EXIT trap. |
lib/90-hooks.bash |
Wire PROMPT_COMMAND, the DEBUG trap (command timer), and __bga_async_init. |
lib/95-shell.bash |
The non-prompt .bashrc bits: history, ls colors, aliases, completion, PATH. |
The build pipeline is bin/build.sh: it concatenates lib/[0-9][0-9]-*.bash
(stable sort) with a header line, writes new.bashrc, and CI fails the build
if the artifact in git differs from a fresh rebuild.
| Platform | Bash | Status |
|---|---|---|
| Linux (any distro with a current bash) | 4.4, 5.0, 5.1, 5.2, latest | Supported. CI runs on ubuntu-latest today; full version matrix lands with v2.0.0 (M7). |
| macOS with a modern bash (Homebrew, MacPorts) | 4.4+ | Supported. macOS ships bash 3.2 by default; install a current bash with brew install bash, then chsh -s "$(brew --prefix)/bin/bash". |
| macOS with stock bash 3.2 | 3.2 | Not supported. Several features (PS0, parameter transformations) require 4.4+. See ADR-0006. |
| zsh / fish / dash / ksh | — | Not supported. bash-gitaware is bash-native by design (PS0, bind -x, PROMPT_COMMAND shape, parameter transformations). |
OSC integration (BASHGITAWARE_OSC=1, default) targets modern terminals that
recognize OSC 133 / OSC 7 well-formed sequences: WezTerm, Kitty, VS Code's
integrated terminal, iTerm2, Ghostty, Konsole, Windows Terminal, Warp, foot,
Alacritty. Terminals that do not recognize the sequences ignore them — they
are well-formed OSC and never corrupt the screen.
The synchronous render path costs under 30 ms on a CI runner for a small repo with warm caches (measured: ~8 ms on the ubuntu-latest runner across 50 renders, well under the 80 ms budget).
The async path makes the perceived prompt time independent of repo size:
the cheap info renders immediately, the expensive git status --porcelain=v2
runs in a background subshell, and the prompt re-renders with full info on
the next cycle.
Subprocess budget per prompt:
| Where | Count |
|---|---|
| Outside a git tree | 0 git subprocesses |
| Inside a git tree, cheap path (default) | up to 3 (rev-parse + an optional rev-parse HEAD for packed-refs + git log -1 --pretty=%s) |
Inside a git tree, sync path (BASHGITAWARE_ASYNC=0) |
up to 4 (the cheap three plus the porcelain v2 status) |
| Stash | 1 extra when .git/refs/stash exists |
| Runtime version (Node / Python / Rust / Go) | 1 on the first prompt in a directory; 0 thereafter (per-(PWD, VIRTUAL_ENV, CONDA_DEFAULT_ENV) cache) |
Subprocesses are the expensive part. Filesystem checks (rebase state, container detection, locale detection) cost nothing measurable.
Themes:
- The color palette is in
lib/20-palette.bash, organized by semantic name (_c_path,_c_branch,_c_dirty, ...) rather than raw color codes. Retheme by changing the mappings, not the prompt logic. - The glyph sets are in the same file, in three tiers; add or change glyphs per tier and rebuild.
Layout:
- The two-line layout is built in
lib/60-render.bash. Reorder, drop, or add elements there; rebuild withmake build. - A "powerline arrow-segment" structural render is planned but not yet shipped;
the
powerlinepreset is a glyph default in v2.
To make the build automatic after editing lib/:
make build # regenerate new.bashrc
make test # run the scenario + perf suite
make lint # run shellcheck -x
make check # build-freshness gate (CI uses this)The symbols look broken (tofu, missing-glyph squares). Your terminal
font does not have the Nerd Font / Powerline glyphs. The defaults are safe
(unicode tier on UTF-8 locales, ascii otherwise), so this usually means
you set BASHGITAWARE_NERD_FONT=1 or BASHGITAWARE_PRESET=powerline
without a Nerd Font installed. Fix: export BASHGITAWARE_GLYPHS=unicode
and reload, or install a Nerd Font in your terminal.
The prompt is misaligned after Ctrl-C or a terminal resize. The
transient-prompt collapse uses the recorded line count of the just-shown
prompt; a resize between rendering and submit can put the cursor on the
wrong row. The next prompt re-syncs. Disable with
BASHGITAWARE_TRANSIENT=0 if it bothers you.
Dirty marker / ahead-behind appears one prompt late on a big repo.
That is the async path doing its job. The expensive git status runs in
a background subshell and the full info shows up on the next prompt.
Disable with BASHGITAWARE_ASYNC=0 for fully synchronous rendering.
OSC sequences appear as literal text in my terminal. Your terminal does
not recognize OSC 133. Disable with BASHGITAWARE_OSC=0 (or upgrade to a
terminal that supports it; the list is in Compatibility).
Colors look wrong. Set NO_COLOR=1 to disable all color. If the issue
is a specific element, the semantic palette is in lib/20-palette.bash.
make check fails: "new.bashrc is out of date". You edited lib/
without running make build. Run it, then commit the regenerated
new.bashrc.
bash-gitaware/
├── new.bashrc the generated single-file artifact (committed)
├── install.sh install to ~/.bashrc (--replace or --append)
├── bin/build.sh concatenate lib/*.bash into new.bashrc
├── Makefile build / test / lint / check / demo
├── lib/ canonical source, twelve modules
│ ├── 00-options.bash
│ ├── 10-detect.bash
│ ├── 20-palette.bash
│ ├── 30-git.bash
│ ├── 40-path.bash
│ ├── 50-runtime.bash
│ ├── 60-render.bash
│ ├── 70-osc.bash
│ ├── 80-transient.bash
│ ├── 85-async.bash
│ ├── 90-hooks.bash
│ └── 95-shell.bash
├── docs/
│ ├── SPEC.md what v2 is (and is not)
│ ├── PLAN.md milestones (M0..M7)
│ ├── adr/ six MADR architecture decisions
│ └── diagrams/ render-pipeline (Mermaid)
├── tests/
│ ├── run.sh orchestrator
│ ├── lib.sh assertion helpers
│ ├── scenarios.sh rendering scenarios (69 asserts)
│ ├── perf.sh 80 ms render budget
│ └── MANUAL.md interactive checklist
├── demo/ vhs tape + generated GIF (`make demo` to regenerate)
└── .github/workflows/ci.yml
See CONTRIBUTING.md. Workflow:
- Edit
lib/[0-9][0-9]-*.bash. make build— regeneratenew.bashrc.make test— scenario + perf + freshness suite.make lint— shellcheck-x.- Conventional Commits for messages; signed off with the project's identity
(
SzematPro).
For substantive design changes, add an ADR in docs/adr/ following the MADR
template (docs/adr/template.md).
Security issues: SECURITY.md. The repo has a low-attack-surface threat model — branch names, commit subjects, and paths are inserted as data, never re-evaluated.
MIT.
Author: Waldemar Szemat — waldemar@szemat.pro ·
github.com/SzematPro
