Building your own tmux helpers via shell functions, designing lightweight status widgets with external scripts, splitting the config into source-file modules, and keeping everything under version control.

In episode 16 we built a plugin foundation with TPM — declaring plugins, installing via prefix + I, updating via prefix + U, and choosing must-have plugins for clipboard, persistence, and status widgets. But there are times when plugins feel like buying pre-made furniture: fine for general needs, but not quite right for your own specific requirements.
In this episode we flip the perspective. Instead of always adding plugins, we learn to build our own: shell functions to automate tmux, lightweight status widgets written in a few lines, and a config structure split into modules so it's easy to maintain and version. This is the skill that separates ordinary tmux users from engineers who truly control their workflow.
All examples refer to tmux 3.7b as the latest stable version. The final target is simple: a config that is thin, clear, and portable to any machine without drama.
Before writing the first line, it's important to understand when a plugin is worth using and when your own script makes more sense. The two aren't enemies — they're tools with different trade-offs.
| Factor | Plugin (via TPM) | Your Own Script / Function |
|---|---|---|
| Maintenance | Follows the ecosystem, updates via prefix + U | Maintained by you |
| Cost | Folder + state per plugin | One small file |
| Customization | Fits the plugin author's design | Exactly your needs |
| Portability | Depends on plugin prerequisites | Depends on the tools you choose |
| Use Case | Common problems with large communities | Specific needs / team internal |
The analogy is choosing between buying a ready-made tool and building your own in a workshop. For already-common problems — clipboard, persistence — plugins are the right call because the community has tested them millions of times. But when you need a command that only makes sense in your team's own workflow, e.g. "create a session named after the project folder", a 15-line script is far lighter than a plugin supporting a hundred features you'll never use.
The most natural bridge between tmux and the shell is the function. Shell functions can run from anywhere, read the environment, and call tmux commands non-interactively — exactly what we learned from the CLI side in episodes 13 and 14.
The most useful example is a function that creates a per-project session. Save the following function in your shell config, e.g. ~/.zshrc or ~/.bashrc:
mkt() {
local name="${1:-$(basename "$PWD")}"
tmux has-session -t "$name" 2>/dev/null || tmux new-session -d -s "$name"
tmux switch-client -t "$name"
}Let's break down each line:
mkt backend, the name used is backend; if no argument is given, the name comes from basename "$PWD" — so in the folder /home/devnull/work/api-gateway, the session is automatically named api-gateway.tmux has-session -t "$name" checks whether the session already exists. If it doesn't (marked by ||), a new detached session is created with tmux new-session -d -s "$name".tmux switch-client -t "$name".The key to this pattern is the -d flag on new-session. Without -d, tmux will try to attach the client to the new session directly, which creates a conflict inside an already running tmux. Creating a detached session then explicitly moving the client is a safe pattern in both contexts: from inside tmux and from a plain shell.
Tip
The has-session || new-session pattern is the most important idiom in tmux automation. You'll see it repeatedly in tmuxp scripts, sessionizers, and various other tools. Once you understand this idiom, nearly all session automation opens up to you.
The tmux status bar supports two ways to display dynamic data: format variables #{...} evaluated by the server, and shell commands #(...) that execute a command. The two are often confused, yet their behavior differs.
Format variables evaluate fast because they're computed internally by the server. Shell commands #() spawn a new process each time they're evaluated — flexible, but there's a cost. The evaluation frequency is controlled by the status-interval option.
The simplest widget example is showing the git branch in the status bar:
set -g status-interval 5
set -g status-right '#[fg=green]#(git rev-parse --abbrev-ref HEAD 2>/dev/null)#[default] %H:%M'Note the use of 2>/dev/null — when you're in a directory that isn't a git repo, git rev-parse outputs an error to stderr that can break the status bar. Discarding it makes the widget stay quiet when not relevant.
When a single #(...) line gets too long or needs logic, move it to an external script. A healthy convention is to keep all widgets in one directory, e.g. ~/.config/tmux/widgets.
#!/usr/bin/env bash
if [[ -r /proc/loadavg ]]; then
read -r load _ < /proc/loadavg
else
load="$(uptime | awk -F'load average: ' '{print $2}' | cut -d, -f1)"
fi
echo "#[fg=yellow]load ${load}#[default]"This script reads the load average with one portability trick: on Linux, the data is read directly from /proc/loadavg; on platforms without that file, the value is taken from uptime output. The result is printed with a color code format the status bar understands directly.
To make it executable, set the execute permission, then reference its path in the config:
chmod +x ~/.config/tmux/widgets/loadavg.sh
set -g status-left-length 40
set -g status-left '#[fg=cyan]#(~/.config/tmux/widgets/loadavg.sh)#[default]'Warning
Every time status-interval elapses, tmux re-runs all #() widgets. Heavy logic like network calls or parsing large files becomes an unnecessary CPU burden. Keep widgets thin — display values, don't do heavy work inside them.
As keybindings, themes, plugins, and widgets accumulate, a single ~/.tmux.conf can swell to hundreds of lines. That's where source-file comes in: a command to load another config file, similar to include in programming languages.
The modular pattern splits the config by concern:
~/.config/tmux/
tmux.conf
keys.conf
plugins.conf
theme.conf
widgets.confIn tmux 3.7b, the config can live at ~/.config/tmux/tmux.conf following the XDG standard — tmux uses it automatically as long as ~/.tmux.conf doesn't exist. The main file then becomes just an entry point that loads other modules:
set -g base-index 1
set -g mouse on
source-file ~/.config/tmux/keys.conf
source-file ~/.config/tmux/theme.conf
source-file ~/.config/tmux/plugins.conf
source-file ~/.config/tmux/widgets.confThe benefit of this split is real in the real world: when one person changes the theme, git diff only touches theme.conf; when adding a keybinding, only keys.conf. Team collaboration is cleaner and merge conflicts are easier to resolve. Remember to reload with prefix + r whenever one of the modules changes.
One of the most commonly separated modules is the theme, because visual changes happen most often. Here's an example theme.conf with the Catppuccin Mocha palette:
set -g status-style 'bg=#1e1e2e,fg=#cdd6f4'
set -g window-status-current-style 'fg=#89b4fa'
set -g pane-border-style 'fg=#45475a'
set -g pane-active-border-style 'fg=#89b4fa'| Color | Value | Role |
|---|---|---|
#1e1e2e | Mantle | Status bar background |
#cdd6f4 | Text | Default text color |
#89b4fa | Blue | Accent / active color |
#45475a | Surface | Inactive pane border |
Since the theme only contains options, moving it to a separate file doesn't change behavior at all — it only simplifies maintenance. The same pattern applies to keys.conf and widgets.conf.
In episode 14 we already touched tmuxp as a declarative tool for arranging sessions. Now we use it more deeply to complete the modular config: a layout stored as YAML can be re-run anytime and produce an identical structure.
session_name: web
windows:
- window_name: editor
panes:
- shell_command: nvim
- window_name: server
panes:
- shell_command: npm run devThis YAML file describes a session named web with two windows: editor opening nvim, and server running the dev server. Compare that to rewriting lines of tmux new-window and tmux send-keys by hand — a declarative file is far easier to read, review, and version.
tmuxp load tmuxp.yamlBecause tmuxp only uses tmux commands under the hood, the same layout works on any machine as long as tmux is installed — ideal for teams that want to standardize their workspace structure.
All the effort above is wasted without versioning. A config not managed by version control will start to differ between machines, and those differences are the source of the hardest-to-trace bugs.
The most common approach is keeping the config in a dotfiles repo and symlinking it to the location tmux reads:
mkdir -p ~/dotfiles/tmux
git init ~/dotfiles
ln -s ~/dotfiles/tmux/tmux.conf ~/.tmux.confWith a symlink, you only have one source of truth: the file inside the repo. Every change can be committed, reviewed, and rolled back. For a cleaner setup with many tools at once, tools like GNU Stow can manage symlinks automatically — but for a single tmux config, ln -s is enough.
chmod +x on widget scripts. A non-executable script only shows an error or produces nothing in the status bar. Always verify by running the script directly from the terminal before wiring it into the config.#() widgets. Widgets are re-executed every status-interval. Network calls, big loops, or parsing huge files inside a widget will make the status bar slow and burden the CPU.source-file. source-file /home/user/.config/tmux/theme.conf will break on another machine. Use ~ or $HOME so the config stays portable.theme.conf, you still have to press prefix + r for the changes to show.~/.tmux.conf and ~/.config/tmux/tmux.conf at the same time. tmux prioritizes ~/.tmux.conf; the XDG file is only used when the legacy file doesn't exist. These two files will shadow each other and confuse. Pick one.This episode completed your mindset: plugins are great, but the ability to build your own is a long-term asset. You can now write shell functions like mkt() that create per-project sessions, design lightweight widgets with #() and external scripts, split the config into organized source-file modules, describe layouts with tmuxp, and keep everything in sync via versioning.
Key points to take away:
has-session || new-session idiom is the foundation of session automation.#() widgets are flexible but must stay thin; raise status-interval to reduce load.source-file and XDG, not piling hundreds of lines into one file.With a solid local foundation, it's time to bring tmux to the most defining scenario: remote machines. In episode 18 we'll cover remote development and SSH workflows — surviving dropped connections, using mosh for slow networks, traversing jump hosts, and handling nested tmux when the server also runs tmux. See you in episode 18!