Bash PROMPT_COMMAND Hooks: Preserve Status and Keep Prompts Fast
Use Bash PROMPT_COMMAND for interactive-shell hooks without losing the previous command status, overwriting other hooks, or slowing every prompt.
Bash’s PROMPT_COMMAND runs immediately before the primary prompt is displayed. It is an attractive place to update a terminal title, record the previous command’s status, refresh a small status indicator, or notify a terminal multiplexer that the working directory changed. It is also easy to make a prompt slow or to overwrite a hook installed by another part of a user’s shell configuration.
Treat PROMPT_COMMAND as an interactive event hook with a strict latency budget. Preserve $? before running any other command, keep work bounded, avoid network calls, and choose deliberately between Bash’s array form and its scalar compatibility form. A prompt hook runs repeatedly, so a small mistake can become a constant cost.
The hook runs before each primary prompt
In interactive Bash, the values in the PROMPT_COMMAND array are executed before Bash displays the primary prompt. When the variable is set but is not an array, its value is used as a command. Bash then expands and displays PS1 and reads the next command. This order means a hook can inspect the status of the user’s previous command, but only if it captures that status immediately.
The first command in the hook changes $?. A function can save the incoming status on entry:
__prompt_record_status() {
local previous_status=$?
PROMPT_LAST_STATUS=$previous_status
}
The local assignment uses the function’s incoming $? value before it runs another command. A second function can read PROMPT_LAST_STATUS to render a status marker in PS1. Keep the value in a shell variable rather than launching an external command merely to pass it around.
Do not put a command substitution directly at the beginning of PROMPT_COMMAND and then assume $? still describes the previous user command. Save it first, then perform status capture, notification, or prompt decoration.
Prefer a deliberate hook array on current Bash
The array form, available starting with Bash 5.1, makes independent actions visible and preserves their order:
__prompt_record_status() {
local previous_status=$?
PROMPT_LAST_STATUS=$previous_status
}
__prompt_update_title() {
printf '\033]0;%s\007' "${PWD##*/}"
}
PROMPT_COMMAND=(__prompt_record_status __prompt_update_title)
This assignment intentionally replaces the existing array. If a framework or another startup file already installed hooks, replacing the array can silently remove them. Inspect the current variable and compose a single documented hook registry rather than independently overwriting the same global from several files. The manual supports both an array and a scalar value; older configurations and shell frameworks may rely on the scalar form. Test the installed Bash version and existing configuration before converting it.
Appending is only safe when PROMPT_COMMAND is already an array. PROMPT_COMMAND+=(new_hook) applied to a scalar is not a general way to build a portable hook list. Avoid compatibility code that guesses at the variable’s declaration using fragile string parsing; if the configuration must support multiple historical Bash versions, define one owner function and make the supported version range explicit.
For a single owner, the scalar form is straightforward:
PROMPT_COMMAND='__prompt_record_status; __prompt_update_title'
Here too, the assignment replaces a previous scalar hook. Compose intentionally and keep commands separated by semicolons only when later hooks should still run after an earlier nonzero status. Prompt hooks should generally report their own failures without changing the status that the user sees for the command they just ran.
Keep hook failures from corrupting command status
After capturing the previous status, a prompt hook should avoid leaving its own failure code as the shell’s visible $?. If the status is displayed in PS1, preserve the intended value:
__prompt_record_status() {
local previous_status=$?
PROMPT_LAST_STATUS=$previous_status
return 0
}
If several commands in the hook run, later commands can still fail. Wrap best-effort work so that a missing optional utility, a terminal write failure, or a stale cache does not replace the previous command’s result. Do not hide meaningful application errors; the prompt is not the place to run the application in the first place.
Also consider shell options such as errexit. Hooks are executed as ordinary shell commands in the interactive shell, not in a sandbox. A failing hook can interact with options and traps configured elsewhere. Keep hook functions small, make expected failures explicit, and test them in a disposable interactive shell launched without the user’s normal startup files.
Protect the prompt’s latency budget
The prompt is on the critical path of every command the user types. Avoid invoking a version-control command on every redraw if it can block on a filesystem, run hooks that execute arbitrary programs, inspect a remote mount, or call a network API. Prefer a cached status updated by events or bounded local checks. If a check can time out, define a short timeout and a fallback value.
Measure startup separately from per-prompt work. A prompt that is quick at startup can still pause after each command because the hook invokes a costly tool. Instrument the hook temporarily with monotonic timing, test in a large repository and on a slow or disconnected filesystem, and remove the instrumentation when done. Do not leave timing commands or verbose output in routine shell initialization.
Hooks should not print ordinary text before the prompt. Such output shifts the visual layout and can interfere with terminal integrations. If a hook emits terminal control sequences, ensure they are properly delimited and escaped; arbitrary directory names or branch names must not be treated as trusted terminal control data.
Keep startup configuration safe and testable
PROMPT_COMMAND is executable shell code stored as a variable. Do not source remote content or evaluate data merely to register a hook. Keep function definitions in a reviewed startup file, guard interactive-only behavior, and do not export a prompt hook to child processes.
To test hook composition, start a clean interactive Bash with --noprofile --norc, define a harmless command that exits nonzero, and verify that the next prompt still displays that status. Test multiple registered hooks, a missing optional dependency, a directory name with shell-special characters, and a hook that emits a terminal escape sequence. Also test the exact Bash version distributed with the operating system: shell configuration portability is often constrained by the system’s Bash, not by the latest manual available online.
Keep the hook list small
Use PROMPT_COMMAND for quick, interactive, local work that genuinely needs to happen before each primary prompt. Preserve $? first, coordinate ownership of the hook variable, return a controlled status, and keep the work bounded. Put expensive indexing, remote polling, and one-time environment setup elsewhere.
The best prompt integration is usually invisible: it does not delay typing, leak output, overwrite another tool’s hook, or change the exit status the user is trying to inspect.
Related:
- How Shell Prompt Customization Actually Works
- Bash Startup Files: Login, Interactive, and Non-Interactive Shells
Sources: