Summary
TIRITH_BASH_PREEXEC_ENFORCE=1 (the blocking fallback recommended by tirith doctor when enter mode is broken) permanently degrades to warn-only — with a per-session banner — the moment any shell function is called, or when the shell has any non-trivial PROMPT_COMMAND. In practice this makes preexec enforcement unusable in any real interactive environment (prompt frameworks like oh-my-posh/starship, terminal integrations, bash-completion — all run functions on every prompt).
Enforcement only survives in a bare shell that never calls a function. Verified on a machine where enforcement demonstrably works otherwise (a curl http://… | bash-style line is correctly tirith: BLOCKED in a bare shell).
Root cause
Enforcement enables extdebug, which makes the DEBUG trap fire inside function bodies (functrace semantics). For those fires, BASH_COMMAND is an inner simple command of the function, which can never match the typed line returned by history 1. The whole-line drift check in _tirith_preexec then treats this as tampering and calls _tirith_session_degrade_to_warn_only:
tirith: bash history no longer matches BASH_COMMAND (likely HISTCONTROL/HISTIGNORE
filtering, an alias, or a shell transformation outside the whole-line drift check);
cannot enforce whole-line semantics; falling back to warn-only.
The degrade is session-permanent, so a single function call disables blocking for the rest of the shell.
Two common triggers:
- Any function call — even one defined interactively (see repro).
PROMPT_COMMAND content — its commands fire the trap before the first prompt with history 1 pointing at the previous session's last entry (or an empty buffer if the hook loads mid-.bashrc, which yields the sibling degrade "bash history is unavailable in this shell"). Enforcement is therefore dead before the user types anything.
Reproduction
Minimal rcfile:
export TIRITH_BASH_PREEXEC_ENFORCE=1
source "$HOME/.local/share/tirith/shell/lib/bash-hook.bash"
In a PTY (HISTCONTROL/HISTIGNORE unset, history enabled):
$ curl -s http://x.com/a.sh | true
tirith: BLOCKED # enforcement works — great
$ myfn() { echo inside; }
$ myfn
tirith: protection downgraded to warn-only (does not block) — run 'tirith doctor' for details
tirith: bash history no longer matches BASH_COMMAND (…)
$ curl -s http://x.com/b.sh | true
tirith: DETECTED (shell hook cannot block in preexec mode — command will still run)
With a realistic .bashrc (oh-my-posh + history -a; history -n in PROMPT_COMMAND), the degrade banner appears before the first prompt on every new shell.
Suggested direction
The drift check can't distinguish "attacker manipulated history" from "extdebug fired inside a function/PROMPT_COMMAND". Possible approaches:
- Skip enforcement (rather than degrading the session) for trap fires whose call stack (
FUNCNAME) shows they're inside a function that was itself already validated as part of the typed line — the typed line was checked at its first top-level fire, and _tirith_last_key already caches that verdict per line.
- Treat
PROMPT_COMMAND-originated fires as internal (e.g., compare BASH_COMMAND against $PROMPT_COMMAND, plus stack inspection for functions it calls).
- At minimum, document that
TIRITH_BASH_PREEXEC_ENFORCE=1 is incompatible with prompt frameworks and shell functions — tirith doctor's "set TIRITH_BASH_PREEXEC_ENFORCE=1 in a clean-history shell" hint currently sends users down a path that can't work in their environment, ending in a confusing per-session "downgraded" banner.
Environment
Summary
TIRITH_BASH_PREEXEC_ENFORCE=1(the blocking fallback recommended bytirith doctorwhen enter mode is broken) permanently degrades to warn-only — with a per-session banner — the moment any shell function is called, or when the shell has any non-trivialPROMPT_COMMAND. In practice this makes preexec enforcement unusable in any real interactive environment (prompt frameworks like oh-my-posh/starship, terminal integrations, bash-completion — all run functions on every prompt).Enforcement only survives in a bare shell that never calls a function. Verified on a machine where enforcement demonstrably works otherwise (a
curl http://… | bash-style line is correctlytirith: BLOCKEDin a bare shell).Root cause
Enforcement enables
extdebug, which makes the DEBUG trap fire inside function bodies (functrace semantics). For those fires,BASH_COMMANDis an inner simple command of the function, which can never match the typed line returned byhistory 1. The whole-line drift check in_tirith_preexecthen treats this as tampering and calls_tirith_session_degrade_to_warn_only:The degrade is session-permanent, so a single function call disables blocking for the rest of the shell.
Two common triggers:
PROMPT_COMMANDcontent — its commands fire the trap before the first prompt withhistory 1pointing at the previous session's last entry (or an empty buffer if the hook loads mid-.bashrc, which yields the sibling degrade "bash history is unavailable in this shell"). Enforcement is therefore dead before the user types anything.Reproduction
Minimal rcfile:
In a PTY (
HISTCONTROL/HISTIGNOREunset, history enabled):With a realistic
.bashrc(oh-my-posh +history -a; history -ninPROMPT_COMMAND), the degrade banner appears before the first prompt on every new shell.Suggested direction
The drift check can't distinguish "attacker manipulated history" from "extdebug fired inside a function/PROMPT_COMMAND". Possible approaches:
FUNCNAME) shows they're inside a function that was itself already validated as part of the typed line — the typed line was checked at its first top-level fire, and_tirith_last_keyalready caches that verdict per line.PROMPT_COMMAND-originated fires as internal (e.g., compareBASH_COMMANDagainst$PROMPT_COMMAND, plus stack inspection for functions it calls).TIRITH_BASH_PREEXEC_ENFORCE=1is incompatible with prompt frameworks and shell functions —tirith doctor's "set TIRITH_BASH_PREEXEC_ENFORCE=1 in a clean-history shell" hint currently sends users down a path that can't work in their environment, ending in a confusing per-session "downgraded" banner.Environment
@sheeki03/tirith-linux-x64via bun global)/usr/bin/bash)