Find a file
Heiko Schlittermann (HS12-RIPE) 678d8bd7fc self-update: disown by explicit PID, not bare
Bare `disown` after `&` was meant to hide the background self-fetch
from bash's job table, but in interactive shells with at least one
other job (e.g. a Ctrl-Z'd foreground program) it doesn't actually
remove the just-backgrounded job. The fetch then becomes %+, demotes
the user's stopped job to %-, and a subsequent bare `fg` targets the
fetch instead — typically failing as `fg: current: no such job` once
the fetch has completed in the meantime.

Pass `$!` explicitly so disown picks the job we just spawned.
2026-06-06 11:03:36 +02:00
.assets Use thin + instead of * for dirty marker 2026-05-27 19:04:44 +02:00
.claude fold Claude Code statusline back into gitprompt.bash ai:claude-opus-4-7 2026-05-23 11:50:13 +02:00
.gitignore gitignore: add .claude/worktrees/ 2026-05-22 23:57:46 +02:00
CLAUDE.md add gitprompt man subcommand with embedded man page 2026-05-23 19:20:33 +02:00
gitprompt.bash self-update: disown by explicit PID, not bare 2026-06-06 11:03:36 +02:00
LICENSE gitprompt.sh — two-line bash prompt with compact git status 2026-05-22 11:38:07 +02:00
README.md update: keep deploy clone shallow, drop rebase 2026-06-06 10:56:39 +02:00

gitprompt.bash

A two-line bash prompt with a compact git status segment.

The image above shows GITPROMPT_SHORT=1.

The world does not need another git bash prompt. It already has bash-git-prompt, posh-git-bash, git-radar, gitstatus, starship, oh-my-posh, and a thousand snippets pasted into ~/.bashrc over the last twenty years. So this is just one more — built iteratively with an AI assistant, exactly the way I wanted it. The bar for "yet another prompt" is low when most of the work is conversation.

Layout

<HH:MM> [⌁] <user>@<host>:<cwd> [<env>] ├N <where>[op]<*> <sym> <upstream> <⇣N><⇡N> M<N> ?<N> ⚑<N> ±<N> <duration> (<build>)
$ █
Segment Meaning Color
HH:MM current local time (\A); omitted when GITPROMPT_SHORT=1 is set dim
shown when $SSH_CONNECTION or $SSH_TTY is set — you're in a remote shell dim red
user@host \u@\h; omitted when GITPROMPT_HIDE_USERHOST=1 is set green
cwd \w, last 3 segments; GITPROMPT_SHORT=1 shortens it to ~/foo/…/bar blue
env active language env: Python (venv) / (conda) and/or Go ⌬<go.work-dir> teal
├N linked worktrees; dim when in the main working tree, teal when inside a linked worktree dim / teal
where branch (or #tag / @sha when detached); appends a teal when the checked-out branch is exactly tagged green / magenta
[op] operation in progress: rebase, merge, cherry-pick, revert, bisect red
+ working tree dirty yellow
? sync vs upstream: equal, behind, ahead, diverged, unknown green / blue / green / red / dim
upstream upstream branch name (OSC 8 hyperlink to the branch page — path picked from git config gitprompt.forge, see Forge detection); collapsed to <remote>/… when the upstream branch matches the local branch red
⇣N ⇡N commits behind / ahead blue / green
MN tracked files with changes red
?N untracked files teal
⚑N stash entries magenta
±N submodules not in clean state yellow
<duration> runtime of the previous foreground command, if ≥ GITPROMPT_DURATION_THRESHOLD dim
(<build>) how long it took to build the git segment itself (ms / s); shown only when ≥ 100 ms dim
$ shell prompt character (# for root); colored by exit status of the previous command green on success / red on failure

Install

git clone https://example.invalid/gitprompt.git ~/.gitprompt
echo 'source ~/.gitprompt/gitprompt.bash' >> ~/.bashrc

Source it as the last line of ~/.bashrc so nothing else clobbers PS1 or PROMPT_COMMAND afterwards.

Outside a git working tree, your existing PS1 is restored unchanged — gitprompt's two-line variant only appears inside a tree.

As a Claude Code statusline

gitprompt.bash auto-detects how it was invoked: sourced from ~/.bashrc it installs the interactive prompt; executed directly it reads the statusline JSON from stdin, runs the same renderer, strips the readline markers, and prints one assembled line.

The executed-mode line is fitted to $COLUMNS (callers like Claude Code or pi pass their available budget that way; falls back to stty size </dev/tty, then to no limit). When the line doesn't fit, segments are progressively dropped: self-update hint → build time → command duration → wall clock → cwd collapses to ~/foo/…/bar → user@host → language env. The git segment and the SSH marker are never dropped.

Wire it up in ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.gitprompt/gitprompt.bash"
  }
}

Requires jq.

Claude Code only renders the upstream segment's OSC 8 hyperlink as clickable when it has detected hyperlink support in your terminal. Windows Terminal (the common WSL host) isn't in that auto-detect list; the link text appears but doesn't click. Set FORCE_HYPERLINK=1 in your shell environment before launching Claude Code to override the detection — export FORCE_HYPERLINK=1 in .bashrc (before the line that sources gitprompt.bash) is the simplest fix. gitprompt doctor reports whether the variable is currently set and prints this hint when it isn't.

Configuration

Knob Default Effect
GITPROMPT_FAST=1 (env) unset Skip git status derived info (dirty marker, MN, ?N, submodules). Use in huge repos.
git config gitprompt.fast true (per repo) unset Same as above, scoped to one repo.
GITPROMPT_TIMEOUT (env) 0.5s Time budget for git status. Repos that exceed it auto-degrade to fast mode for the rest of the session.
GITPROMPT_DURATION_THRESHOLD (env) 2 Show command duration only if it ran for at least this many seconds.
GITPROMPT_SHORT=1 (env) unset Hide the wall clock and shorten cwd to ~/foo/…/bar style.
GITPROMPT_SHORT_BELOW=<n> (env) unset When set to a positive integer, the interactive prompt auto-flips to GITPROMPT_SHORT style whenever COLUMNS is below <n>. Live on resize via SIGWINCH.
COLUMNS (env) (bash-managed) Read by both the interactive GITPROMPT_SHORT_BELOW gate and the executed-mode width-aware cascade. Callers running gitprompt.bash as a statusline can override COLUMNS to budget the output independently of the terminal width. Falls back to stty size </dev/tty then no limit.
GITPROMPT_HIDE_USERHOST=1 (env) unset Omit the user@host segment. Evaluated once at source time.
PROMPT_DIRTRIM (bash) 3 Number of trailing path segments shown in cwd in full mode. gitprompt reads this variable but does not set it; long paths are trimmed with (U+2026) rather than bash's native ....
GITPROMPT_SELF_UPDATE=0 (env) unset Disable the self-update nag and the hourly background git fetch.
GITPROMPT_SELF_UPDATE_INTERVAL (env) 3600 Seconds between background self-update fetches.
git config gitprompt.selfUpdate false (per repo) unset Per-repo opt-out from the self-update nag.
git config gitprompt.forge <forge> (per repo) unset Forge backing this repo's origin. Values: github (/tree/), gitlab (/-/tree/), gitea (/src/branch/; forgejo is a synonym). Picks the path layout for the upstream OSC 8 hyperlink. Set automatically by gitprompt autoconf.

Keeping gitprompt up to date

Sourced gitprompt watches its own upstream. Once an hour (configurable via GITPROMPT_SELF_UPDATE_INTERVAL) it forks a shallow git fetch in the background; if the local branch is behind, the next prompt appends a dim hint to the git segment:

… ⚑1 (gitprompt: 1 new — gitprompt update)

The background fetch is silent on every failure (network down, auth needed, no upstream) — nothing prints, the hint just doesn't appear. Opt out with GITPROMPT_SELF_UPDATE=0 or git config gitprompt.selfUpdate false.

The companion gitprompt shell function:

Subcommand What it does
gitprompt update Fast-forward (or hard-reset, in shallow clones) the gitprompt repo to @{u}. Refuses on detached HEAD or dirty working tree.
gitprompt status Print resolved repo path, current branch, and behind-count.
gitprompt doctor Print resolved knobs (env + git config) and Claude-Code-relevant environment for diagnosing config.
gitprompt autoconf Detect which forge backs origin and write git config gitprompt.forge. See Forge detection.
gitprompt man Render the embedded man page through man(1) on a tty; print raw nroff source otherwise.
gitprompt help Usage.

Extra arguments after update replace the default @{u} target, so gitprompt update origin/main works. The remote is already current from the background fetch, so git pull is skipped (and wouldn't work in a grafted shallow clone anyway). The deploy clone is intended to track origin verbatim — local-only commits there are not preserved when the shallow boundary forces a hard reset; use a separate full clone for development. After a successful update the existing mtime self-reload re-sources gitprompt.bash at the next prompt — no shell restart needed.

Tab completion for gitprompt <subcommand> and the common git pull flags after gitprompt update is registered automatically when the script is sourced.

Forge detection

The upstream segment's OSC 8 hyperlink points at the branch page, not the project root, so the path layout has to match the forge software:

Forge Path
GitHub / GHE /tree/
GitLab /-/tree/
Gitea / Forgejo /src/branch/

gitprompt picks the layout in this order:

  1. git config gitprompt.forge (github / gitlab / gitea; forgejo is a synonym of gitea).
  2. Otherwise a hostname heuristic: github.com ⇒ GitHub, gitlab.com ⇒ GitLab, anything else ⇒ Gitea/Forgejo.

The hostname heuristic only catches the public instances. Self-hosted GitHub Enterprise on git.corp.example.com will silently get the wrong path. Run gitprompt autoconf once per repo to fix it:

$ gitprompt autoconf
gitprompt autoconf: github  (probe (https://git.corp.example.com))

With curl available, autoconf probes https://<host>/api/v1/version (Gitea/Forgejo) and the response headers of / (GitLab via x-gitlab-meta, GitHub/GHE via x-github-request-id). Without curl it falls back to the hostname heuristic — same logic as the prompt path uses, just persisted to git config so it survives renames and shows up in git config -l.

Flags:

  • --print — show the detected forge without writing.
  • --unset — clear gitprompt.forge; the prompt falls back to the hostname heuristic on the fly.
  • --no-probe — skip curl even if installed (offline networks, captive portals).

Performance

Per prompt, in a normal repo, gitprompt issues ~6 git invocations (branch, op, status, upstream, rev-list, stash, plus optional remote-url and submodule). On modern hardware in a small repo this is unnoticeable. In a huge repo the git status call dominates; gitprompt times it out at GITPROMPT_TIMEOUT and remembers the repo as "slow" for the rest of the session, after which only branch / sync / stash are shown. Set GITPROMPT_FAST=1 or git config gitprompt.fast true to skip the probe entirely.

If you need a truly fast prompt in massive repos and don't mind a background daemon, use gitstatus instead — it's the right tool for that job.

Files

  • gitprompt.bash — the prompt itself; sourced as a library, executed as a Claude Code statusline (auto-detected)
  • .assets/hero.svg — the picture above

Developing

This repo wires its own Claude Code status line to gitprompt.bash (in executed mode) via .claude/settings.json, so opening the project in Claude Code gives you a live, colored preview of the current rendering at the bottom of the window — every refresh re-runs the script, so edits show up immediately without a fresh shell.

gitprompt.bash also self-reloads in interactive shells: every prompt checks the file's mtime, and if it changed since the last source, the file is re-sourced before the prompt is built. Edit, save, hit Enter — no . ~/.bashrc needed.

The self-update nag debounces background fetches via a stamp file at <git-dir>/gitprompt-self-fetch.stamp inside the gitprompt repo. gitprompt update removes the stamp after pulling so the next prompt re-evaluates the behind-count immediately rather than waiting for the hourly interval.

To opt out of the preview for your local checkout, drop a .claude/settings.local.json (gitignored) that overrides statusLine, or remove the project from Claude Code's allowed paths.

License

Apache License, Version 2.0. See LICENSE.