Learn Tmux - Hooks & Event-driven Automation
Series/Learn Tmux/Episode 15
Episode 15 of 28

Learn Tmux - Hooks & Event-driven Automation

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

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

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.

Hooks: Event Listeners inside Tmux

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.

set-hook Syntax

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.

Syntax set-hook
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:

Lihat hook terpasang
tmux show-hooks -g
tmux list-hooks

Commonly Used Event Hooks

HookTriggered whenExample use
client-attachedA client attaches to a sessionActivity logging, per-client setup
client-detachedA client detachesCleanup when a client leaves a session
session-createdA new session is createdAutomatic per-session setup
session-closedA session is closedRelated resource cleanup
window-layout-changedWindow layout changes (split/resize)Adjusting the status bar when the layout changes
window-renamedA window is renamedSyncing window names to external apps
pane-exitedA program in a pane finishesTask-complete notification
pane-diedThe program finishes and the pane survives (remain-on-exit)Handling a "dead" pane without closing it
pane-focus-in / pane-focus-outFocus enters/leaves a paneNeeds focus-events on
after-split-windowAfter the split-window commandForcing 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.

Execution Order and Multiple Commands

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:

Beberapa perintah per event
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.

Avoiding Infinite Loops

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.

Guard mencegah infinite loop
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.

Integration with Development Tools

Auto-rename Windows Based on the Active Command

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.

Auto-rename window
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.

Auto-formatting Status on Events

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:

Status berubah saat fokus pindah
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.

Task-Completion Notifications

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:

Notifikasi task selesai
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.

Code-group: Hooks and Auto-rename

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.

Common Pitfalls

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

Conclusion

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:

  • Hooks are event listeners: set-hook -g <event> <command>.
  • Event hooks are triggered by events; after hooks (after-) are triggered after a command finishes.
  • Hooks are stored as an array — use indexes to add commands, don't overwrite [0].
  • Guard flags prevent infinite loops; keep hooks thin so they don't burden every event.

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!