Controlling tmux through hooks: triggering automatic commands on events like pane-exited and client-attached, integrating with development tools, and preventing hidden infinite loops.

In episode 14 we built workspaces imperatively and declaratively — non-interactive commands like new -d and send-keys, idempotent bootstrap scripts, and layouts with tmuxp and tmuxinator. All of that makes tmux act when we command it. In this episode we flip the direction: tmux acts when something happens, via hooks.
Hooks are tmux's event-driven mechanism — commands run automatically when certain events occur, from a client attaching to a program in a pane finishing. In the real world, this eliminates boring monitoring work: no more peeking at the log window waiting for a build to finish, because tmux itself tells you. No more manually renaming windows, because tmux does it based on the active program.
The concept of hooks isn't new — you know event listeners in JavaScript or signal handlers in the shell. tmux applies it the same way: every event that happens inside the tmux server can trigger one or more commands. There are two big groups of hooks:
Event hooks — triggered by direct events like pane-exited, client-attached, or session-created. After hooks — triggered after a tmux command finishes, named with the after- prefix, e.g. after-split-window which runs every time a window is split.
The fitting analogy is a smartphone notification system: apps don't need constant monitoring, because the system tells you when something happens. Without hooks, you do manual polling — repeatedly opening the log window. With hooks, tmux does the watching, and you act when notified.
All hooks are managed with the set-hook command. Its basic form: set-hook -g <event> <command>. The -g flag sets the hook globally — applying to all sessions, windows, and panes.
set-hook -g pane-exited 'display-message "pane exited"'
set-hook -g client-attached 'display-message "client #{client_name} attached"'
set-hook -g session-created 'run-shell "notify-send tmux session-started"'The first line shows a message in the status bar every time a program in a pane finishes. The second notifies you when a client attaches. The third runs a desktop notification via run-shell every time a new session is born. The command inside a hook executes in the context of the session, window, or pane where the event occurred — so format variables like #{client_name} automatically hold the correct value.
To inspect installed hooks, use show-hooks or list-hooks:
tmux show-hooks -g
tmux list-hooks| Hook | Triggered when | Example use |
|---|---|---|
client-attached | A client attaches to a session | Activity logging, per-client setup |
client-detached | A client detaches | Cleanup when a client leaves a session |
session-created | A new session is created | Automatic per-session setup |
session-closed | A session is closed | Related resource cleanup |
window-layout-changed | Window layout changes (split/resize) | Adjusting the status bar when the layout changes |
window-renamed | A window is renamed | Syncing window names to external apps |
pane-exited | A program in a pane finishes | Task-complete notification |
pane-died | The program finishes and the pane survives (remain-on-exit) | Handling a "dead" pane without closing it |
pane-focus-in / pane-focus-out | Focus enters/leaves a pane | Needs focus-events on |
after-split-window | After the split-window command | Forcing a certain layout after every split |
Note the important difference between pane-exited and pane-died: the first signals the pane has been closed, while the second signals the program finished but the pane stays open because of remain-on-exit on. The choice between them determines whether the hook runs before the pane disappears — e.g. to capture its last output.
Hooks are stored as an array. When an event occurs, all array members execute in order by index. After writing set-hook -g pane-exited '...', array member [0] is filled. To add a second, third, and further command on the same event, use explicit indexes:
set-hook -g pane-exited 'display-message "pane exited"'
set-hook -g pane-exited[1] 'run-shell "notify-send tmux task-done"'
set-hook -g pane-exited[2] 'set -g status-style bg=green'Writing set-hook -g pane-exited 'perintah-baru' without an index clears the array and replaces [0] — this is the trap that most often makes a config "lose" old hooks. A healthy habit: name indexes explicitly on every hook line you write, so the execution order is readable straight from the config file.
tmux automatically doesn't run after-hooks for commands executed from inside a hook — this prevents most recursion. But event hooks aren't auto-protected, and you can easily create a loop: a window-layout-changed hook that runs select-layout will trigger itself forever, because every select-layout changes the layout and raises the same event.
The most common protection pattern is a guard flag: a user option checked at the start of the hook and reset at the end. This flag ensures hook work runs only once per cycle.
set -g @layout-guard 0
set-hook -g window-layout-changed 'if-shell "test #{E:layout-guard} = 0" "run-shell \"tmux set -g @layout-guard 1; tmux select-layout even-horizontal; tmux set -g @layout-guard 0\""'With this pattern, when the hook is triggered by the select-layout it ran itself, the guard is already 1 so the if-shell branch doesn't execute anything. The general rule to hold: don't run commands from a hook that trigger the same event the hook itself listens to, unless you deliberately use a guard.
Warning
Besides loops, watch the cost too. A hook containing heavy logic will slow every event that triggers it — splitting a window feels slow if the after-split-window hook runs a big parse. Keep hooks thin: delegate heavy work to run-shell running in the background, rather than piling logic directly into the hook.
The built-in tmux feature closest to hooks is automatic rename: a window renames itself to follow the program active in the pane. Enable it with automatic-rename on, and the name format is set via automatic-rename-format, whose default is #{pane_current_command} — so a window running nvim automatically becomes named nvim.
setw -g automatic-rename on
setw -g automatic-rename-format '#{pane_current_command}'The format can be modified to carry more context, e.g. the project folder name plus the active program. Important note: once a window is renamed manually, automatic rename for that window is disabled automatically — tmux assumes you want full control.
Hooks can also change the status bar appearance dynamically. With pane-focus-in and pane-focus-out, you can mark which pane is active by changing the status color — very helpful when monitoring several processes at once. Remember, both events need the focus-events on option:
set -g focus-events on
set-hook -g pane-focus-in 'set -g status-style bg=blue'
set-hook -g pane-focus-out 'set -g status-style bg=default'When you move focus to another pane, the status bar changes color instantly — no polling, no scripting, purely a reaction to events. This shows the power of hooks: the display becomes a mirror of runtime conditions, not the result of periodic computation.
The most practical daily scenario: tell you when a long command finishes. The pane-exited hook fires exactly when the program exits, and display-popup shows a message without disturbing your work focus:
set-hook -g pane-exited 'display-popup -E "echo Task selesai di pane #{pane_index}; sleep 3"'
set-hook -g pane-exited[1] 'run-shell "notify-send tmux task-done"'The first line opens a small popup showing "Task done" for three seconds then closes itself. The second line, if your machine has notify-send (the libnotify package on Linux) or osascript on macOS, raises a desktop notification that stays visible even when you switch apps. The combination means you'll never miss the moment a build finishes — even when the tmux window isn't visible.
set-hook -g client-attached 'display-message "client #{client_name} attached"'
set-hook -g session-created 'run-shell "notify-send tmux session-started"'
set-hook -g pane-exited 'display-popup -E "echo Task selesai; sleep 3"'
set-hook -g after-split-window 'select-layout even-horizontal'The first block shows hooks reacting to events; the second combines automatic rename with focus-based status changes. These two patterns complement each other: the first makes tmux inform you, the second makes tmux present context without you asking.
Writing a hook that triggers the same event. A window-layout-changed hook running select-layout without a guard will loop endlessly. Always use a guard flag, or don't call commands that raise that event itself.
Forgetting to enable focus-events on. pane-focus-in and pane-focus-out will never fire while this option is off. Check with tmux show-options -g focus-events before blaming the hook.
Overwriting an existing hook without an index. A second set-hook -g pane-exited 'cmd' replaces the previous hook. To add, use indexes [1], [2], and so on.
Relying on notify-send on a machine without libnotify. run-shell "notify-send ..." fails silently in environments without that binary. Verify by running notify-send directly from the terminal, or use the display-popup fallback.
Putting heavy logic directly in hooks. Every event pays the hook execution cost. Delegate long work to run-shell, and make sure it doesn't trigger the event that started it when done.
Ignoring the side effects of global hooks. A hook with -g applies to all sessions, including production sessions from other people attaching to the same machine. For per-session behavior, install the hook without -g targeting a specific target.
This episode turned tmux into a proactive system. You understand the two hook groups — event hooks and after hooks — mastered the set-hook -g syntax, know the most useful events like client-attached, session-created, window-layout-changed, and pane-exited, manage execution order with array indexes, and protect the configuration from infinite loops. You can also integrate hooks with development tools: auto-renaming windows from pane_current_command, a status bar that changes on focus, and task-completion notifications via display-popup.
Key points to take away:
set-hook -g <event> <command>.after-) are triggered after a command finishes.[0].Writing your own hooks opens the door to patterns the community has already solved. In episode 16 we'll cover the plugin ecosystem with TPM — the Tmux Plugin Manager, installing and updating plugins, and must-have plugins like tmux-sensible, tmux-yank, and the resurrect-continuum combination for a production-ready workflow. See you in episode 16!