Skip to content

shellmark: every command you type, with how it ended

The problem

Shell history keeps the text of a command and nothing else. It cannot tell you which commands failed this week, which one eats your afternoons, or what you ran on that branch. The answers exist for a moment, in $? and the clock, and the next prompt throws them away.

Prompt hooks can catch them, but the obvious hook gets three things wrong:

  • bash logs the wrong command. bash has no "before a command" hook, so it is faked with a DEBUG trap that also fires for whatever PROMPT_COMMAND runs. macOS Terminal sets PROMPT_COMMAND=update_terminal_cwd; tried live with bash 3.2, a hook that arms after its own prompt function logged update_terminal_cwd in place of all four commands typed.
  • Pipelines hide failures. $? of make test | tee out.log is tee's.
  • $( ) is a subshell, in zsh too. Five per prompt measured at about 12 ms per command.

The method

  • bash: arm once, and only for a new history line. PROMPT_COMMAND becomes record; <what was there>; arm. The trap takes a command only when history 1 shows a new number, so other prompt code, which adds no history line, is ignored wherever it sits, and the whole line is logged, not its first word.
  • Every pipeline status ($pipestatus, PIPESTATUS) goes in a "pipe" field. bash resets PIPESTATUS when the trap runs, so the trap saves it first.
  • No subshells: results come back in variables. The clock is zsh's $epochtime or bash 5's $EPOCHREALTIME, split on any non-digit because the decimal mark follows the locale. 1,000 commands: 0.7 s, against 12.7 s for the $( ) version.
  • Branch from files: walk up to .git; when it is a file (worktree, submodule), follow gitdir:. Detached HEAD shows 12 characters.
  • A line typed with a leading space is never logged; SHELLMARK=off pauses.
echo 'source /path/to/methods/shellmark/shellmark.sh' >> ~/.zshrc

$ shellmark fails
x  2 01 Jan 08:02    9.1s  /src/app (main)        make test
|    01 Jan 08:04    9.0s  /src/app (main)        make test | tee out.log

| marks a failure inside a pipeline that exited 0. Also recent, slow, top, dirs, find REGEX, stats, csv. The log is plain NDJSON:

{"ts":1767225600,"cmd":"make test | tee out.log","cwd":"/src/app","branch":"main","ms":9000,"exit":0,"pipe":[2,0],"sid":"4182.2291830114"}

Limits

  • bash spends one subshell per command reading history 1; zsh none.
  • bash logs what history keeps: an ignoredups repeat is skipped.
  • bash 3 and 4 time in whole seconds.
  • An existing bash DEBUG trap wins; on bash 3.2 shellmark then records nothing, silently, because bash hides the outer trap from a sourced file.
  • Typed secrets land in the log unless the line starts with a space.

Code

methods/shellmark/ in the methods repository: the hook (shellmark.sh, zsh and bash 3.2+), the query tool (Python standard library), and a 52-check test that types at real interactive bash and zsh on a pseudo-terminal.