tmux
How it works
tmux evaluates #(command) format strings by forking a shell every time the status bar refreshes. With a status-interval of 5 seconds and a handful of queries, that is a steady stream of subprocesses — each one potentially spawning git, running a battery command, or querying the network.
beachcomber changes the economics. The daemon pre-caches values in the background; each comb g call reads from that cache in roughly 34 microseconds. The subprocesses still happen, but they return almost instantly instead of blocking on I/O. Across a session with multiple windows and a short refresh interval this adds up to a meaningful difference in shell and CPU overhead.
Each comb g call also signals demand to the daemon, which keeps the relevant provider warm automatically.
Prerequisites
- The beachcomber daemon is running (
comb sshould return successfully) - tmux is installed and you have an active session
If the daemon is not running, see the Getting Started guide.
Plain tmux setup
Where to add the config
tmux reads ~/.tmux.conf on startup. Status bar format strings go in set -g status-left and set -g status-right directives. You can add the lines below anywhere in that file; most people group all status bar configuration together near the bottom.
Reload without restarting
After editing ~/.tmux.conf, apply the changes to a running session:
tmux source-file ~/.tmux.conf
Or from inside tmux, press the prefix key then : and type source-file ~/.tmux.conf.
The comb syntax
comb g <key> [path] # g = get, text is the default format
comb get <key> [path] # equivalent long form
g is the short form used throughout this guide — g is get, text is the default output format. See the CLI reference for all shorthands.
Global providers (battery, load, hostname, uptime, network) do not require a path. Path-scoped providers like git require a directory to query against.
Important: #() commands in the tmux status bar run in the tmux server process, not inside any pane. The server has no concept of the current pane's working directory. For path-scoped providers you must pass the path explicitly using tmux's own format variables:
#(comb g git.branch "#{pane_current_path}")
tmux expands #{pane_current_path} before passing the string to the shell, so the daemon receives the actual path of the active pane.
Provider examples
Battery percentage:
#(comb g battery.percent)%%
The %% is tmux's way of printing a literal %.
Load average (1-minute):
#(comb g load.one)
Network SSID (macOS):
#(comb g network.ssid)
Uptime (days and hours, via template format):
#(comb g.f '{days}d {hours}h' uptime)
Hostname:
#(comb g hostname.short)
Git branch for the active pane's directory:
#(comb g git.branch "#{pane_current_path}")
Kubernetes context:
#(comb g kubecontext.context)
Complete status bar example
# ~/.tmux.conf
# Refresh every 5 seconds — cheap with beachcomber
set -g status-interval 5
# Left: session name and hostname
set -g status-left '[#S] #(comb g hostname.short) | '
# Right: git branch, load, battery, kubernetes context
set -g status-right '#(comb g git.branch "#{pane_current_path}") | load:#(comb g load.one) | bat:#(comb g battery.percent)%% | #(comb g kubecontext.context)'
# Optional: increase the space available for the right status
set -g status-right-length 120
Expected output
With the example above the right side of the status bar will look roughly like:
main | load:0.42 | bat:87% | prod-cluster
Fields that return no data (for example, git.branch when the pane is not inside a git repository) will be empty strings. You can wrap them in tmux conditionals or simply accept the extra | separator.
oh-my-tmux
oh-my-tmux is a popular tmux configuration framework. It ships with its own opinionated status bar and theme system. There are two levels of integration — a simple approach that adds beachcomber queries alongside the existing oh-my-tmux features, and a deep integration that replaces oh-my-tmux's built-in data sources entirely.
Background: how oh-my-tmux works internally
Before choosing an approach, it helps to understand what oh-my-tmux does under the hood. The .tmux.conf file is both a tmux config and an embedded shell script — all lines prefixed with # are actually shell code. When tmux evaluates a format string like #{battery_percentage}, oh-my-tmux has already rewritten it during theme application into something like:
#(cut -c3- "$TMUX_CONF" | sh -s _battery_info)
This strips the # prefix from the entire .tmux.conf file, pipes the result into sh, and calls the named function. Every format string evaluation forks a new shell, and every one of those shells unconditionally runs _uname_s=$(uname -s) to detect the platform — even if the function being called doesn't need it.
With a status-interval of 30 seconds and several panes, oh-my-tmux's built-in variables like #{battery_status}, #{battery_bar}, #{battery_percentage}, #{username}, and #{hostname} collectively spawn hundreds of subprocesses per minute. Each battery query forks pmset on macOS. Each username/hostname query walks the pane's process tree.
beachcomber eliminates the data-gathering cost: the daemon pre-caches battery, hostname, and user data in the background, and each comb g call returns from cache in microseconds.
How oh-my-tmux handles customization
oh-my-tmux is designed so that you never edit .tmux.conf directly. All user customization goes into .tmux.conf.local. oh-my-tmux sources this file after applying its own config, so anything you set there overrides the defaults without touching the framework files.
Simple approach: add beachcomber queries
The quickest integration is to add comb g calls directly into the oh-my-tmux status variables. Find the tmux_conf_theme_status_left and tmux_conf_theme_status_right lines in your .tmux.conf.local and add beachcomber queries using #() format strings:
# ~/.tmux.conf.local
tmux_conf_theme_status_left=' #S | #(comb g hostname.short) '
tmux_conf_theme_status_right=' #(comb g git.branch "#{pane_current_path}") | load #(comb g load.one) | bat #(comb g battery.percent)%% | #(comb g kubecontext.context) | %R '
This works alongside oh-my-tmux's built-in variables. The beachcomber queries return from cache nearly instantly; the built-in variables continue to fork as before.
Deep integration: replace oh-my-tmux's data sources
The deep integration replaces oh-my-tmux's built-in #{battery_*}, #{username}, and #{hostname} variables with custom functions that read from beachcomber. This eliminates the subprocess storm at its source.
oh-my-tmux supports custom variables through POSIX shell functions defined in the # EOF section of .tmux.conf.local. A function named foo is referenced as #{foo} in the status string. oh-my-tmux's theme engine rewrites #{foo} into #(cut -c3- "$TMUX_CONF_LOCAL" | sh -s foo) — a shell fork that calls your function.
Step 1: Replace battery
oh-my-tmux's battery display uses three built-in variables: #{battery_status} (charging arrow), #{battery_bar} (gradient bar), and #{battery_percentage}. Behind the scenes, _battery_info runs a background polling loop that calls pmset -g batt on macOS, and _battery_status runs on every status refresh with another pmset call.
The key insight: oh-my-tmux's battery code path only activates if #{battery_*} tokens appear in the status string. Remove them and the entire battery subprocess machinery is disabled. Replace them with a single custom function that queries beachcomber and delegates rendering to oh-my-tmux's own _bar() function:
# ~/.tmux.conf.local — status_right
# Comment out the original and add the beachcomber version:
# original:
# tmux_conf_theme_status_right=" #{prefix}#{mouse}#{pairing}#{synchronized}#{?battery_status,#{battery_status},}#{?battery_bar, #{battery_bar},}#{?battery_percentage, #{battery_percentage},} , %R , %d %b | #{username}#{root} | #{hostname} "
# beachcomber battery:
tmux_conf_theme_status_right=" #{prefix}#{mouse}#{pairing}#{synchronized}#{comb_battery} , %R , %d %b | #{username}#{root} | #{hostname} "
Then define the comb_battery function in the # EOF section at the bottom of .tmux.conf.local:
# # /!\ do not remove the following line
# EOF
#
# comb_battery() {
# percent=$(comb g battery.percent 2>/dev/null) || return
# [ -z "$percent" ] && return
# charging=$(comb g battery.charging 2>/dev/null)
#
# # status symbol
# if [ "$charging" = "true" ]; then
# status="↑"
# else
# status="↓"
# fi
#
# # compute charge as 0-1 float for oh-my-tmux's _bar renderer
# charge=$(awk "BEGIN { printf \"%.2f\", $percent / 100 }")
#
# # render the gradient bar using oh-my-tmux's built-in _bar() function
# # args: palette, empty_symbol, full_symbol, length, charge, client_width
# bar=$(cut -c3- "$TMUX_CONF" | sh -s _bar \
# "gradient" "◻" "◼" "auto" "$charge" \
# "$("$TMUX_PROGRAM" display -p '#{client_width}')")
#
# printf '%s %s %s%%' "$status" "$bar" "$percent"
# }
#
# "$@"
# # /!\ do not remove the previous line
The comb_battery function calls comb g twice (battery percent and charging status), both returning from cache in microseconds. It then calls _bar() through oh-my-tmux's own shell payload to render the gradient bar identically to the original. The result is visually identical, but no pmset subprocess is ever forked.
Note the _bar() call: cut -c3- "$TMUX_CONF" | sh -s _bar ... invokes the function from the main .tmux.conf. The _bar function is a pure renderer — it takes a charge float and outputs tmux colour escape sequences. It does not query any system state.
If you want a simpler display without the gradient bar, you can skip the _bar() call entirely:
# printf '%s %s%%' "$status" "$percent"
Step 2: Replace username and hostname
oh-my-tmux's #{username} and #{hostname} are not simple lookups — they walk the pane's process tree to detect SSH sessions and display the remote username and hostname when you are SSHed into another machine. This is genuinely useful and beachcomber cannot replicate it because the daemon only knows about the local system.
The solution is a hybrid approach: use beachcomber for local panes and fall back to oh-my-tmux's SSH-aware resolver for remote sessions. A pgrep check on the pane's child processes detects SSH sessions cheaply.
Update the status string to use custom functions instead of the built-in variables:
# beachcomber battery + username + hostname:
tmux_conf_theme_status_right=" #{prefix}#{mouse}#{pairing}#{synchronized}#{comb_battery} , %R , %d %b | #{comb_username #{pane_pid} #{b:pane_tty} #D}#{root} | #{comb_hostname #{pane_pid} #{b:pane_tty} #h #D} "
The #{pane_pid}, #{b:pane_tty}, #h, and #D tokens are expanded by tmux before the function runs. They provide the pane's process ID, TTY, short hostname, and unique pane identifier — the same arguments oh-my-tmux passes to its own _username and _hostname functions.
Add the functions in the # EOF section:
# comb_username() {
# # args: pane_pid, pane_tty, pane_id
# if pgrep -P "$1" -q ssh mosh mosh-client 2>/dev/null; then
# # SSH session — fall back to oh-my-tmux's process tree resolver
# cut -c3- "$TMUX_CONF" | sh -s _username "$1" "$2" false "$3"
# else
# # Local session — read from beachcomber cache
# comb g user.name 2>/dev/null || \
# cut -c3- "$TMUX_CONF" | sh -s _username "$1" "$2" false "$3"
# fi
# }
#
# comb_hostname() {
# # args: pane_pid, pane_tty, h_or_H, pane_id
# if pgrep -P "$1" -q ssh mosh mosh-client 2>/dev/null; then
# # SSH session — fall back to oh-my-tmux's process tree resolver
# cut -c3- "$TMUX_CONF" | sh -s _hostname "$1" "$2" false false "$3" "$4"
# else
# # Local session — read from beachcomber cache
# comb g hostname.short 2>/dev/null || \
# cut -c3- "$TMUX_CONF" | sh -s _hostname "$1" "$2" false false "$3" "$4"
# fi
# }
For local panes, the comb g call returns from cache and the function exits immediately — no process tree walking, no uname side effect. For SSH panes, the full oh-my-tmux resolver runs as before, correctly displaying the remote username and hostname.
Step 3: Reload
After all changes, reload the configuration:
tmux source-file ~/.tmux.conf
You must source the main .tmux.conf, not just .tmux.conf.local — oh-my-tmux's _apply_configuration function is what processes custom variables and wires them into the status bar format strings.
What this eliminates
With the deep integration, a typical oh-my-tmux setup with 10 panes and a 30-second status interval goes from forking roughly 500+ subprocesses per minute down to a handful of lightweight comb g calls that return from cache. The battery provider polling (pmset -g batt) and the platform detection (uname -s) are the most expensive operations eliminated.
The #{root} variable is left unchanged — it is a simple check that oh-my-tmux handles cheaply.
Reverting
All changes are in .tmux.conf.local. To revert to the original oh-my-tmux behaviour:
- Uncomment the original
tmux_conf_theme_status_rightline - Comment out or delete the beachcomber version
- The custom functions in the
# EOFsection are inert when not referenced — you can leave them in place
No changes to .tmux.conf are required at any point.
Troubleshooting
- Status bar not updating: check
tmux show -g status-interval. If set to a large value or 0, reduce it:tmux set -g status-interval 5. - Path-scoped providers empty in tmux:
#()runs in the tmux server process, not a pane. Verify tmux is expanding the path variable:tmux display-message -p '#{pane_current_path}'. The output should be the pane's working directory. - oh-my-tmux changes not taking effect: after editing
.tmux.conf.local, runtmux source-file ~/.tmux.conf— oh-my-tmux re-reads the local file as part of sourcing the main config. Sourcing only.tmux.conf.localwill not trigger the custom variable wiring. - Custom functions not rendering: oh-my-tmux's custom variable parser rejects function names starting with
_. Use names likecomb_battery, not_comb_battery. - Battery bar not rendering: the
_bar()call requires$TMUX_CONFto be set (oh-my-tmux sets this automatically). If you see the status and percentage but no bar, check that thecut -c3-path is correct. - SSH username/hostname showing local values: verify
pgrep -P <pane_pid> sshdetects the SSH process. If your SSH session is nested deeper in the process tree, thepgrep -Pcheck may not find it and the function will use the local cache. The fallback (|| cut -c3- ...) ensures graceful degradation.
See the Troubleshooting guide for general diagnostics.