Build1 publisher3 min readPublished
Three tmux hooks make pane logging automatic instead of a keypress you forget
A dev.to writeup wires after-new-session, after-new-window and after-split-window to a shell front end for tmux-logging, so every pane writes a line-flushed file from the moment it exists.
The Engineer · Build desk

What happened
- A dev.to post wires three tmux hooks, after-new-session, after-new-window and after-split-window, to a start command. The author says that covers every way a pane comes into existence.
- The replacement front end runs tmux pipe-pane through awk with an fflush() on every line, appending to the configured log file.
- The script sources the plugin's variables.sh to reuse @logging-path and @logging-filename, so the settings in .tmux.conf stay the only place the log location is defined.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- capability Post-incident reconstruction stops depending on whether someone remembered to hit a key: every pane created after the reload writes a file you can grep.
- cost The price is a shell script you now maintain, deliberately kept outside the plugin directory so tpm updates do not overwrite it, and coupled to the plugin's internal variables.sh.
- constraint Because tmux will not expand $HOME or ~ in option values, the log path is hardcoded per machine. That makes a shared .tmux.conf across hosts awkward.
- exposure Logging every pane by default means secrets typed or printed in any pane land on disk in plaintext, and the post treats the toggle as a convenience.
The part worth understanding here is why the author did not just patch the plugin.
tmux-logging pipes a pane through BSD sed. When sed's stdout points at a file, it is block-buffered, so an active log shows 0 bytes until the buffer fills or logging stops [5]. The comments in the script put the fill threshold at roughly 4 to 64KB [6]. For incident work, the pane you want to read is the pane that has not flushed yet. The replacement pipes through awk with an fflush() per line [7].
The front end lives at ~/.tmux/logging-live.sh, outside the plugin directory, so prefix U (tpm update) does not overwrite it [8]. It sources the plugin's own variables.sh to get $logging_full_filename, which keeps @logging-path and @logging-filename in .tmux.conf as the single source of truth [9]. Reading someone else's script for its variables is a coupling, not a clean interface, and the author says so plainly in the header comment.
The hooks pass the pane id explicitly: each set-hook runs the script with #{pane_id} expanded by tmux [2]. According to the script's comments, run-shell -t does not export TMUX_PANE, so without an explicit id every tmux call inside would resolve to whichever pane happens to be active [10]. For a hook firing on a newly created pane that is the right pane. For prefix P pressed while a background pane is the target, it is the wrong one [11].
Three hooks are claimed to cover every way a pane comes into existence: after-new-session, after-new-window, after-split-window [1][3]. The claim holds for the ordinary paths. The post does not say what happens with join-pane, break-pane or respawn-pane, and none of those fire the three hooks named here, so a pane that arrives by one of those routes is untested by this writeup.
Two adoption costs are in the config itself. tmux does not expand $HOME or ~ inside option values, so @logging-path has to be an absolute path with your username typed into it [4]. And the key binding has to come after the tpm run line, because tpm binds P itself [12]. A .tmux.conf that puts the bind-key first loses the toggle silently.
Existing panes are not adopted by the hooks. The post gives a one-time loop over tmux list-panes -a -F '#{pane_id}' to start each of them [13]. Everything created after the reload logs itself [14].
The environment is the limit on transferring any of this. It was written and tested on macOS with tmux 3.7c, BSD awk 20200816 and zsh [15]. The author writes that the awk is plain POSIX, "so it should carry to Linux, but I have not tested it there" [16]. GNU sed on Linux has different buffering behaviour from BSD sed, so the 0-byte symptom that motivated the rewrite is a macOS observation until someone reproduces it elsewhere. The filename template includes #{session_name}, #{window_name}, #{pane_index} and a strftime timestamp [17]. A directory of those files is greppable by session.
What to watch
- A Linux run would settle whether the stock plugin's sed pipe stalls there too, or whether the awk front end is a macOS-only need.
- Whether panes born from join-pane, break-pane or respawn-pane fire any of the three hooks, or start unlogged.
- Any change to tmux-logging's variables.sh or its $logging_full_filename variable would break the front end at source time.