config.toml, every setting
The file as it ships, docs/config.example.toml.
# tmux-companion configuration.## You do not need this file. Every setting below is already the default, and a# machine with no config file behaves exactly as the binary did before the file# existed — which is a test, not a promise.## Where it goes, first one found wins:## 1. --config <path># 2. $TMUX_COMPANION_CONFIG# 3. $XDG_CONFIG_HOME/tmux-companion/config.toml# (~/.config/tmux-companion/config.toml when XDG_CONFIG_HOME is unset)# 4. ~/tmux-companion.toml## tmux-companion config path which file is being read# tmux-companion config check parse it and say what is wrong# tmux-companion config dump every setting with its default## A key the parser does not recognise is an error naming the key, the line and# the key you probably meant. A silently ignored setting is worse: it costs an# evening wondering why nothing changed.
[general]# Where the daemon's stderr goes. Unset means `daemon.log` in the state# directory, `~/.local/state/tmux-companion/daemon.log` unless XDG_STATE_HOME# says otherwise. The daemon writes one line when it starts and one per# failure, so this is where "autosave failed" and a config that would not# parse end up. `tmux-companion doctor` prints the last line. Rotated to# `daemon.log.1` past 1 MiB. A `~` here is expanded, as in every path in# this file.# log = "~/.local/state/tmux-companion/daemon.log"
# Path-to-label overrides for the window segment, replacing the abbreviated# path with a name of your choosing.## This replaces ~/.yrl/lib/dir-aliases, which was a path on one laptop. That# file is still read when this table is empty, so upgrading drops nothing.[dirs.aliases]# "/Users/you/work/some-very-long-project" = "proj"
[git]# How long a parsed `git status` stays fresh, in seconds. 0 disables the cache,# which costs a fork on every refresh. At the default, a one-second status bar# is served from memory four times out of five.ttl_secs = 5.0
# How long "this path is inside a work tree" is trusted, in seconds.## A directory's repo-ness effectively never changes, so this could be forever,# except that `git init` in a directory already on the bar would then stay# invisible until the daemon restarts.repo_check_ttl_secs = 300.0
# Middle-ellipsize a branch name longer than this many characters. The# `--branch-max-len` flag on `gst` and `status-right` wins when given; this is# what applies without it. The tail the ellipsis keeps is fixed.branch_max_len = 20
# What the segment draws, and in what order.## A part left out is not rendered. Four hundred untracked build artifacts are# not information, and if you never push, "ahead" and "behind" are two counts# you will never read.## sync the in-flight and failed-remote block before the branch name# branch the branch name itself# state the clean, dirty, new-branch or gone-upstream marker# ahead commits you have that the upstream does not# behind commits the upstream has that you do not# conflicts files with merge conflicts# untracked untracked files, counted with unstaged additions as git does# deleted deleted files in the work tree# renamed renamed files in the work tree# copied copied files in the work tree# modified modified files in the work tree# staged everything in the index, as one group# stash the stash count## Order is honoured between groups: branch info (ahead, behind, conflicts),# then the work-tree counts, then staged, then stash. Inside a group the order# is fixed, because a group is one colour run and reordering its counters would# move escape sequences rather than glyphs.parts = ["sync", "branch", "state", "ahead", "behind", "conflicts", "untracked", "deleted", "renamed", "copied", "modified", "staged", "stash"]
# Which branch-name prefixes earn which glyph, tried in the order written.## The prefix is cut off the name the bar draws, so `feat/status-bar` renders as# the feature glyph and `status-bar`. A name no entry claims keeps the plain# branch glyph, which is why main, master and dev need no entry of their own.## Matching is by plain prefix, compared without case. Not a regular expression:# `posts/` is what people actually name branches, and an unanchored pattern# matching in the middle of a branch name is a bug nobody enjoys finding on a# status bar.## `icon` takes `{NAME}` for any glyph in src/tmux/icons.rs — FEATURE, BUGFIX,# HOTFIX, CHORE, RELEASE, TAG, BRANCH, GIT and the rest — so this file stays# readable without a patched font. Anything else is drawn as written, which is# how an emoji, a letter or a codepoint your own font has gets in:## [[git.branch_types]]# icon = "{TAG}"# prefixes = ["post/", "posts/"]## [[git.branch_types]]# icon = "\U000F04DF " # a codepoint, TOML's 8-digit escape# prefixes = ["parked/"]## [[git.branch_types]]# icon = "wip "# prefixes = ["wip/", "spike/"]## The list replaces these six rather than adding to them, so copy what you want# to keep. `tmux-companion config dump` prints the current list in this shape.[[git.branch_types]]icon = "{FEATURE}"prefixes = ["feat/", "feature/", "features/"]
[[git.branch_types]]icon = "{BUGFIX}"prefixes = ["fix/", "fixes/", "bugfix/", "bugfixes/"]
[[git.branch_types]]icon = "{HOTFIX}"prefixes = ["hotfix/"]
[[git.branch_types]]icon = "{CHORE}"prefixes = ["chore/", "chores/"]
[[git.branch_types]]icon = "{RELEASE}"prefixes = ["release/", "releases/"]
[[git.branch_types]]icon = "{TAG}"prefixes = ["tag/", "tags/"]
[git.autofetch]# Fetch the repositories the bar has drawn, on a timer inside the daemon, so# the ahead and behind counts mean something. A status bar reporting a stale# number confidently is worse than one reporting nothing.## Off by default, and deliberately. This is the only part of tmux-companion# that touches the network, and a daemon quietly reaching a remote is not a# surprise anybody should get from a status bar. The plugin this idea comes# from is thepante/tmux-git-autofetch, which is alive and worth installing if# you are not running this.enabled = false# Seconds between passes.interval_secs = 600# How long one repository gets before it is given up on and its git killed. A# fetch that hangs holds the pass open and everything behind it goes unfetched;# the fetch runs with every credential and passphrase prompt disabled, which# stops the usual cause, and this catches the rest.timeout_secs = 20# How long a repository stays on the list after the bar last drew it. Without# it the daemon would keep fetching everything visited since it started.remember_secs = 3600
[network]# Below this many bytes per second the bandwidth segment draws nothing. The# default is 20 KiB/s: a bar that reacts to every background poll is noise.## This setting was read from the config and then ignored: the segment used a# constant of the same value, so changing it here did nothing at all.threshold_bps = 20480
# The block each rate is drawn on. The number is written in the bar's own# background colour on top of these, which is what makes them read as blocks# rather than as coloured text.download_colour = "#5cae36"upload_colour = "#0262a8"
# The unit after the number: `KiB/s` in `20KiB/s`. Empty draws it in the# figure's colour, in italics. The figure itself is written in the bar's# background colour, or in near-white on a block that colour doesn't read on.unit_colour = ""
[battery]# How long a battery reading stays fresh, in seconds. Reading it is expensive# and the number does not move fast enough to matter.ttl_secs = 30.0
[glyphs]# Which glyph set the bar draws with.## nerd-font-v3 the codepoints in src/tmux/icons.rs, which need a patched# font; this is the default# ascii 7-bit, for a terminal whose font nobody controls## If your bar is a row of boxes, you do not have a Nerd Fonts v3 patch, and# this is the line to change rather than the thing to put up with.preset = "nerd-font-v3"
# Replace individual glyphs, by the constant name in src/tmux/icons.rs. These# are applied on top of the preset, so one missing icon is a reason to fix that# icon rather than to drop to a whole preset below.[glyphs.icons]# STAGED = "*"# ARROW_RIGHT = ""
[status.right]# Whether the right-hand side ends with a space. tmux draws it flush to the# terminal edge, and without this the last glyph sits against the border.trailing_space = true
# The segments, in the order they are drawn, each with the tmux markup that# goes in front of it.## `separator_before = ""` means nothing between this segment and the one# before it, which is a real preference and was not expressible at all while# the literals lived in tmux.conf.## `{NAME}` expands to the glyph of that name in src/tmux/icons.rs, so this file# stays readable in an editor with no patched font. A name that does not exist# is left as you wrote it rather than dropped, because a separator rendering# `{ARROW_RIGH}` is a typo you can see.## A separator is drawn whether or not the segment after it rendered anything,# which is what the tmux.conf literals did.
[[status.right.segments]]name = "git"separator_before = ""
[[status.right.segments]]name = "net"separator_before = ""
[[status.right.segments]]name = "battery"separator_before = "#[reverse,fg=color237]{ARROW_RIGHT}#[bg=color237,none]"
# How many coding agents are running and how many have gone quiet, as# `4 agents · 1 waiting`, with the waiting count coloured. Which programs count# and how long quiet is are `[agents]` below. Draws nothing when no agent is# running, so the separator goes with it. Not on the side by default: the# daemon reads the pane list every `[agents] interval_secs` only while this# block is here, and a machine without agents should pay nothing for it.## [[status.right.segments]]# name = "agents"# separator_before = " "# # What a mouse click on it runs, as a tmux command, with `{me}` for this# # binary's path; empty is the segment's own default, the inbox here and the# # brief on `health`. Needs in tmux.conf:# # bind -T root MouseDown1StatusRight run-shell "tmux-companion click '#{mouse_status_range}'"# on_click = ""
# One mark when the daemon knows something needs a look: a timer that failed# in the last hour and hasn't worked since, config.toml edited after the# daemon started, or a newer# binary on disk than the one running. The glyph and the word for the first# reason, `+N` when there are more, and nothing at all when the daemon is# fine. With [online] turned on, the network being gone is a reason too. `tmux-companion doctor` prints every reason in full on its health# line, and `tmux-companion health ack` takes a failure you've read off the# mark. Two stats every five seconds while this block is here.## [[status.right.segments]]# name = "health"# separator_before = " "
# The chunk clock, with [chunk] below turned on: a timer and the focus time# this sitting, `38m`, amber near the budget, and an alert timer and `50m+12`# in red past it; ` · 1h14m` after either is the agent in front, see# [chunk] agent_age. No I/O at draw# time; the daemon's own look every five seconds keeps the number.## [[status.right.segments]]# name = "chunk"# separator_before = " "
# Jobs stopped or running under a pane, drawn by `tmux-companion sh-jobs`.## This was `vim-bg`, which asked one question with one answer baked in: is# there a suspended nvim under this pane. If you suspend vim, or claude, or a# cargo watch, this is the table that makes the segment say so.## The cost is why it is not on the status bar by default: finding the children# of one pid means enumerating the whole process table, which measured 16.25 ms# of server CPU per call. Bind it to a key, or accept the cost knowingly.[sh_jobs]# `stopped` counts only jobs suspended with ctrl-z, which is what vim-bg meant.# `any` also counts jobs running in the background.states = "stopped"
# How many icons a busy pane may put on the bar.max = 3
# The table, in priority order: the first entry whose pattern matches a process# name wins. `match` is a regular expression, so "^vim$" does not match nvim.# An unparseable pattern costs that row its icon and nothing else.## [[sh_jobs.job]]# match = "^claude$"# icon = " "# color = "#d97757"## `icon` may contain `{NAME}` placeholders, which expand to the glyph of that# name in src/tmux/icons.rs. `color` is a tmux colour; leave it out when the# icon carries its own markup, as the default entry does.[[sh_jobs.job]]match = "nvim"icon = "#[fg=#0262a8,bg=colour235,none] n#[fg=#539035]im#[fg=colour235,bg=colour233]"# What to call a window whose active pane is running this, when# [window_names] is on. Leave it out and this row says nothing about names.# window_name = "edit"color = ""
[usage]# Whether to record which key bindings get picked in search. The cheat sheet orders each# box by this, so the keys you actually reach for float to the top of a group.## It does not records what you press or keylogs it, it is your business and not the tool's.# If don't want to even record what you pick in keybinding search turning it off is one line# which is a default and doesn't need to be set but you can.enabled = false
# Where the log lives. Unset means $XDG_STATE_HOME/tmux-companion/.# path = "~/.local/state/tmux-companion/keys-usage.tsv"
# Days without a lookup after which the cheat sheet counts a binding as# learned and drops it. 0 means nothing is ever learned.learned_after_days = 14
[project]# Which directory jumper the project picker and `new-window` list, by name.# One of:## zoxide `zoxide query -l`, most frecent first. The default.# z the `~/.z` database, which rupa/z, zsh-z and z.lua all write.# Read straight from the file, because `z` is a shell function and# cannot be run from here. Ordered by rank. Honours $_Z_DATA.# cdr zsh's own recent directories, `~/.chpwd-recent-dirs`, most recent# first. Nothing to install: it is already there if you have# `chpwd_recent_dirs` on. Honours $ZDOTDIR.# ghq `ghq list -p`, every repository ghq has cloned. Not visits, so the# list is stable and complete rather than ordered by what you use.# none no directory list. Live sessions and whatever you type.## A jumper that is not installed, or a database that has never been written, is# the empty list and not an error: you get live sessions and a typed path.dirs_source = "zoxide"
# A command that prints one absolute path per line. Set, it wins over# dirs_source; this is how every jumper that is not in the list above works.## dirs_command = ["fd", "-td", "-d2", ".", "/Users/you/src"]# dirs_command = ["sh", "-c", "ls -d ~/src/*/"]## autojump, fasd, jump and the rest go here too, with whichever of their flags# lists directories -- check their own manual for it rather than trusting a# line copied from this file, because they do not agree on one.## It is a list of words rather than one string, because splitting a command# line correctly is a parser that does not belong in a config loader. Anything# that needs a pipe, a glob or a filter goes through `sh -c`, as above.dirs_command = []
# The command that records a visit, with the directory appended. Run when you# open a project or a window, so the place you just went floats up the list.## Empty means whatever the source does on its own: `zoxide add` for zoxide, and# nothing for the rest. z and cdr are written by your shell on every `cd`, and# ghq has no notion of a visit.## visit_command = ["myjumper", "add"]visit_command = []
# Deprecated, and going away next release. Use dirs_source = "none".## `zoxide = false` still means no directory list, whatever else is set here, so# a config written before this section had more than one source keeps working.# zoxide = true
# The layout a new project session starts with.layout = "default"
# Paths under which a checkout's own `.tmux-companion.toml` may run commands.# That file travels with the repository: `[[window]]` rows in the shape of# [[layout.window]] below, or `layout = "name"` naming a layout this config# defines. A file in a checkout that can start programs is a file anybody who# can push to the repository can use to start programs on your machine, so# its commands count only under a prefix listed here, `~` for home and a# trailing `*` allowed. Anywhere else the window names still count and every# command is blanked; `project show` says which happened. Empty trusts# nowhere.## trusted = ["~/work/*", "~/src/mine"]trusted = []
# Use a different layout for projects under a path. First match wins.## [[project.override]]# match = "~/work/*"# use_layout = "work"
# Which window the picker previews for a live session. The window worth seeing# is the one you would have switched to in order to answer "what is happening# over there", which with the layout below is the agent.## A session without a window by this name previews whichever window it is# currently on, so every session shows something rather than only the ones that# happen to match. Empty, the default, means never look for a named window;# with the layout below it would be "ai".preview_window = ""
# What a new project session starts with. The first window is selected when the# session opens.## With no [[layout]] at all a project opens as one plain shell, which is the# shipped default and what this file, being the defaults, leaves it at. The# pair below is what one laptop runs, an editor beside an agent; uncomment it# to get that, or write vim and codex, or one window, or five, or a tail -f on# a log and no editor anywhere.## [[layout]]# name = "default"## [[layout.window]]# name = "edit"# command = "nvim"# # Hold the name against the running program. Without this an editor window# # follows whatever is running and an agent window renames itself to its own# # version string, which is how windows end up called `2.1.278`.# hold_name = true## [[layout.window]]# name = "ai"# command = "claude"# hold_name = true
# A window with panes. `command` above is the shorthand for a window that holds# one pane; a [[layout.window.pane]] table takes over when there is more than# one, and the panes are created in the order they are listed here.## [[layout.window]]# name = "work"# # One of tmux's own five names: even-horizontal, even-vertical,# # main-horizontal, main-vertical, tiled. Panes with nothing set here get# # tiled, because the shape the splits leave behind is an accident of the# # order they ran in.# layout = "main-vertical"# # main-pane-width for main-vertical, main-pane-height for main-horizontal,# # ignored by the rest. A percentage needs tmux 3.4; before that, cells.# main_size = "60%"## [[layout.window.pane]]# command = "nvim"# focus = true # the pane selected when the window opens## [[layout.window.pane]]# command = "claude"## [[layout.window.pane]]# cwd = "~/src" # empty means the project directory## `layout` also takes a raw tmux layout string, which is what# `tmux list-windows -F '#{window_layout}'` prints. Nothing parses it, so you# can arrange a window by hand with your usual bindings and paste the result:## layout = "bb62,272x67,0,0{136x67,0,0,1,135x67,137,0,2}"## The cell sizes in that string are absolute, and tmux rescales them# proportionally, so a layout captured on a wide display comes back cramped on# a laptop and a preset name travels better.
[notify]# Say something when a long command finishes in a pane you were not looking at.# The last clause is the whole value: a command that finishes in front of you# needs no announcement, and firing for those is the noise that teaches people# to ignore the ones that matter.## Off by default.## The plugin this comes from is rickstaa/tmux-notify.enabled = false# Seconds between scans. One tmux call each.interval_secs = 2# How long something has to run before finishing is news.threshold_secs = 30# Only announce what finished out of sight. A pane counts as out of sight if it# was hidden at any point while the command ran, not only at the end, because# the usual shape is starting something, switching away, and coming back when# it is already done.only_when_unwatched = true# What to run. Empty means tmux's own display-message, which needs nothing# installed and behaves the same on every platform. A desktop notification is# one line away, and is not the default because shelling out to osascript or# notify-send on a machine that has neither is a failure somebody has to debug:## command = ["notify-send", "{command}", "ran for {duration}"]## {command}, {duration}, {pane} and {message} are substituted in every argument.command = []# How long tmux shows the message, in milliseconds, when command is empty. The# agents' nudges use it too. 0 keeps it up until a key is pressed, for anybody# four seconds isn't long enough to read it in; prefix ~ shows the ones already# gone.message_ms = 4000# Commands never worth announcing. An editor, a pager or an agent runs for# hours and finishing one is not news; without this list every `:q` fires a# notification about a two-hour nvim session.ignore = [ "nvim", "vim", "vi", "emacs", "nano", "less", "more", "man", "top", "htop", "btop", "watch", "ssh", "tmux", "claude", "codex", "gemini", "grok", "agy", "lazygit", "tig", "fzf",]
[quiet]# Quiet hours that come round every day, beside the timer `tmux-companion# quiet 2h` sets: notifications, nudges and the agents count all hold off# inside them. Local 24-hour times, and a window may cross midnight. For# anybody who would otherwise have to remember to turn quiet on.## daily = ["22:00-08:00", "13:00-14:00"]daily = []
[journal]# What happened in each project, written down as it happens: a command that# ran past `min_secs` and finished, with how long it took; an agent that# stopped and what it asked; an agent that worked a turn past `agent_min_secs`# and said it was done, with the last line of its answer; a project opened or# closed. `tmux-companion journal` reads it, newest first, and `journal# --print -t SESSION` is the standup answer for one project. journal.tsv in# the state directory, rotated past a megabyte. Programs in [notify] ignore# are not runs worth a line. One list-panes per interval_secs.## Off by default, only need to be set to true if you want.enabled = falseinterval_secs = 5min_secs = 60# Only an agent that reports through its hooks (`tmux-companion agent hooks`)# ever says done, so this is silent for the rest. Five minutes: a turn under# that is a chat, one over it is work. Zero writes every turn down.agent_min_secs = 300
[online]# Whether the network is there, said by the health mark: `offline` on the# bar, and `offline for 3m: nothing answers at 1.1.1.1:443` from `doctor` and# the brief. Online draws nothing. It needs the `health` segment in# [[status.right.segments]] to be seen on the bar.## Off, because the probe is a TCP connection to an address outside this# machine, opened and dropped every interval_secs, and that's yours to turn# on. `probe` is any host:port that should always answer; something inside# your own network is a fair choice if what you care about is the VPN.# Two probes in a row have to go unanswered before the mark comes up, so a# lost packet doesn't flash it.enabled = falseprobe = "1.1.1.1:443"interval_secs = 30timeout_ms = 2000
[agents]# One list of what counts as a coding agent, read by everything that asks:# `tmux-companion panes --agents`, the `agents` segment on the bar, and the# restore headline that counts them. Matched against pane_current_command,# and a command that is only a version number, `2.1.283`, counts as well:# that is what tmux reports for claude, which names its process after its# version, and nothing else names a process that way.programs = ["claude", "codex", "gemini", "cursor-agent", "aider", "opencode", "grok", "agy"]# Seconds without output after which an agent counts as waiting for you, when# it has said nothing better through `tmux-companion agent` and rung no bell.# tmux has no per-pane activity time, so this is measured on the window the# agent is in: a shell you are typing into beside it keeps it reading as busy.# A working claude redraws its spinner every second and never goes quiet, so# this only ever tells one sitting at its prompt from one that drew a moment# ago.waiting_secs = 10# How often the bar's agents segment and the inbox re-read the pane list, in# seconds. One tmux call each, shared by every attached client.interval_secs = 2# The inbox: the agents that have stopped, with the last lines of each one's# screen captured at the moment it stopped, so `tmux-companion inbox` can# show the question even for a window nobody has looked at. One capture-pane# per stop. Off, and `inbox` has nothing to show.inbox = true# Say so out loud once an agent has waited this many seconds. Zero never# nudges; the bar already counts them. 300 is five minutes.nudge_after_secs = 0# What says it. Empty is tmux's own display-message. `{program}`, `{at}`,# `{waited}` and `{question}` are substituted anywhere they appear:## nudge_command = ["osascript", "-e", "display notification \"{question}\" with title \"{program} at {at} is waiting ({waited})\""]nudge_command = []# How the bar's segment reads. `words` is `2 agents · 1 waiting`; `glyphs` is# the robot and the count, then an arrow, an hourglass and the waiting count,# for a bar that is already full of words. The glyphs are AGENT, AGENT_TO,# WAITING and BUSY under [glyphs.icons] if the font disagrees.style = "words"# Which count follows the total. `waiting` is the agents that have stopped:# one that said `asked` or `done` through its hooks, one that rang the bell,# or one quiet past waiting_secs. `busy` is the agents working, for somebody# who keeps a dozen open and works one at a time and wants to see the one# that is going rather than the eleven that are not. `both` draws both, busy# first. An agent can say which it is: `tmux-companion agent hooks claude`# prints the settings block that makes claude report busy, asked and done as# they happen, and a report wins over the window's quiet time.show = "waiting"# Lines of an agent's screen that are furniture rather than something it# said, as regular expressions. The question an inbox row, a nudge and a# journal line carry is the last line on the screen that matches none of# these. The input box at the foot of the screen, a rule with a prompt under# it and whatever sits below, is cut before these are tried, so they are for# what is left above it. In order: a rule or the edge of a box, an empty# prompt, a spinner, the line a finished turn leaves, tool output, the mode# line, two hints, and a dialog's numbered options. A pattern that does not# parse matches nothing.question_skip = [ '^[\s─━═╭╮╰╯│┃|-]*$', '^\s*[❯>]\s*$', '^\s*[✢✳✶✻✽✺·*]\s+\S+(…|\.\.\.)', '^\s*[✢✳✶✻✽✺]\s+\S+ for \d', '^\s*⎿', '^\s*⏵⏵ ', '^\s*\? for shortcuts', '(?i)^\s*esc to (cancel|interrupt)', '^\s*[❯>]?\s*\d+\.\s',]
[window_names]# Name a window after what is running in its active pane, using the same# [[sh_jobs.job]] table that gives the status bar its icons. A row drives a# name only when you give it a `window_name`, so every row written before that# field existed keeps drawing its icon and saying nothing about names.## Off by default: renaming somebody's windows is visible.## This will not take a name away from somebody who set one. A window with# automatic-rename off was pinned on purpose, by `hold_name` in a layout or by# hand, and this leaves it alone unless it was the thing that pinned it. When a# window stops matching, the name is handed back and tmux renames it again.## The plugins this comes from are ofirgall/tmux-window-name, which is a Python# daemon, and joshmedeski/tmux-nerd-font-window-name for the icon half.enabled = false# Seconds between passes. One tmux call each.interval_secs = 5
[autoreload]# Watch tmux's config and source it when it changes, so an edit takes effect# without you reaching for the reload binding. The daemon already watches this# file, because the rows behind `keys` and `cheatsheet` are rebuilt when it is# newer than they are, so this is a stat it was going to do anyway.## Off by default: reloading somebody's tmux config without being asked is a# thing that happens to their running sessions.## The plugin this comes from is b0o/tmux-autoreload, which watches with entr or# inotifywait where this compares a modification time. It is archived, since# February 2024, and its author named nothing to use instead, which is why this# setting exists rather than a link.enabled = false# Seconds between checks. One stat per file, so this can be small.interval_secs = 2# Which files to watch. Empty means the tmux config the daemon already watches,# which is $TMUX_COMPANION_TMUX_CONF when set, else the first of# $XDG_CONFIG_HOME/tmux/tmux.conf and ~/.tmux.conf that exists.files = []
# The first pass after the daemon starts never reloads: otherwise a daemon# started right after a config edit would source the file at startup, which is# a surprise, and a loop when the config is what starts the daemon.
# Deprecated: `[sessions]` below does this and more. This shells out to a# plugin's save script and keeps one file; that keeps generations of its own# and records what each pane was running, arguments and all. It still works,# and `config check` says so. It is off by default now.[autosave]# Whether the daemon saves the session list on a timer, so a reboot does not# cost the layout.## This is the saving half of tmux-resurrect. Restoring stays on a keybinding on# purpose: an automatic restore would resurrect a stale layout over a session# you have already started working in, which is a worse failure than losing a# layout to a reboot.## Not tmux-continuum, which is the usual answer: continuum drives its timer by# appending #{continuum_save} to status-right, and status-right here is a# single #() into this binary, tuned down from five spawns a second.## Off since `[sessions]` arrived. A config that still asks for this keeps it.enabled = false
# Seconds between saves.interval_secs = 900
# The script that does the saving. Unset means tmux-resurrect's.# script = "~/.config/tmux/plugins/tmux-resurrect/scripts/save.sh"
[sessions]# Snapshots of the whole tmux server, kept in generations, so a reboot does not# cost the sessions you had open.## This is the saving half. Restoring stays a command you run, because an# automatic restore drops a stale layout over a session you have already# started working in, which is a worse failure than losing a layout to a# reboot.## `[autosave]` above is the older, narrower version of this and is going away:# it shells out to a plugin's save script and keeps one file. This keeps# generations of its own and knows what each pane was running.
# When the daemon takes one by itself.## off never; a snapshot is whatever you ask for by hand# interval every interval_secs# cron on the schedule below, for people who want it on the hour## An earlier design had a fourth mode that wrote on every change the daemon# noticed. It is gone, because setting interval_secs = 10 buys the same thing# with the cost written down instead of hidden behind a word.autosave = "off"
# Seconds between snapshots under `interval`. The floor is 10: below that the# writes start overlapping the capture on a busy machine, and a number under it# is an error rather than something quietly rounded up.## What it costs, measured on one laptop with 7 sessions and 14 panes. The# 40-pane column is that rate extrapolated, not measured:## interval_secs 14 panes 40 panes what keep = 20 spans# 900 (default) 0.015% 0.04% 5 hours# 60 0.2% 0.6% 20 minutes# 10 1.3% 3.7% 200 seconds# 10, no history 0.01% 0.03% 200 seconds## That last column is the one nobody expects. Generations are counted, not# timed, so a ten second interval with the default `keep` holds under four# minutes of history, and a crash you notice after lunch has already rolled off# the end of it. Short interval wants a large `keep`, or `keep_days`.interval_secs = 900
# The schedule under `cron`. Five fields, the usual order.cron = "0 * * * *"
# How many generations to keep. Pruning runs after a successful write, oldest# first, and never touches the one the `last` pointer names.keep = 20
# Also keep anything younger than this many days, however many files that turns# out to be. This is how you keep a month of hourly snapshots without setting# `keep` to 700. Zero leaves the count to decide on its own.keep_days = 0
# Whether to capture what was on each pane's screen.## This is the expensive half, at 9.3 ms per pane against 1 ms for the metadata# of a whole server, and it is what makes a short interval costly. Turning it# off leaves the half that carries what each pane was running and where, which# is the half worth having often.## It is also the half that holds secrets. Pane history is the text that was on# your screen, so it contains whatever you printed, including the token you# echoed and the .env you catted. The directory is created 0700 and every file# in it 0600, and this is how you keep it off the disk entirely.pane_history = true
# How many lines of each pane to capture.pane_history_lines = 2000
# Sessions never captured, by name.## On a shutdown this means the session is not saved and does not come back,# since stopping the server takes every session with it either way. The command# says so before it acts rather than leaving you to find out.exclude = []
# How long the restore summary counts down before going ahead.## It opens only when the restore does not know something: a pane no# [[restore.program]] row claims, one a row refused, or a command the capture# had to read off a running process so its arguments are already gone. A# restore where everything is known goes straight through, however many panes# that is, because a confirmation on every restore is one people turn off.## Touch any key and the countdown stops. Zero never draws it, which is `--yes`# made permanent: the panes it would have asked about still open, in the right# directory, at a prompt.confirm_secs = 5# No countdown at all: the summary opens and waits for a key. For anyone a# clock is in the way of: reading slowly, listening through a screen reader,# pressing keys slowly. Wins over confirm_secs, zero included.confirm_wait = false
# What a restore is allowed to run in a pane it is rebuilding.## Default deny. Restoring means executing commands your own machine recorded# weeks ago, and "re-run anything I saw" is one bad afternoon away from# restoring a `curl | sh` that was in a pane six weeks back. A command no row# claims is captured, shown, and left to you.## `match` is a regular expression against the whole saved command, not against# the process name. The process name of an agent is its version string --# `2.1.281` rather than `claude` -- and everything worth matching on is in the# arguments.## `command` is what actually runs. `{command}` is the saved command verbatim and# `{cwd}` the pane's directory.## `run = false` is how you say "never bring this back" without leaving it to# fall through to the unknown pile and be asked about every time.## Writing this table replaces the shipped rows rather than adding to them, so# copy what you want to keep. `tmux-companion config dump` prints them.
[[restore.program]]# An agent keeps which conversation it is in inside its own arguments, so# replaying them verbatim is what brings the conversation back. A bare `claude`# stays bare. `claude --continue` here instead would take the most recent# conversation in that directory, which survives a stale snapshot and picks# wrong when two agents were running in one repository.match = "^claude( |$)"command = "{command}"
[[restore.program]]match = "^(codex|gemini|cursor-agent|aider|opencode|grok|agy)( |$)"command = "{command}"
[[restore.program]]# An editor with a session plugin restores itself from the directory, and one# without it opens empty. Either way the saved arguments are a file list from an# hour ago, so they are dropped. Reopening your buffers is the editor's job:# auto-session and persistence.nvim both do it per directory.match = "^n?vim( |$)"command = "nvim"
[[restore.program]]match = "^(lazygit|tig|gitui)( |$)"command = "{command}"
[[restore.program]]match = "^(htop|top|btop|watch)( |$)"command = "{command}"
[[restore.program]]match = "^(tail|less|journalctl)( |$)"command = "{command}"
[[restore.program]]match = "^ssh( |$)"command = "{command}"
[run]# Where the commands come from: auto, zsh, bash, fish or atuin.## `auto` reads whichever shell $SHELL names, and is the default. It used to be# `zsh`, which meant a bash user's picker read a ~/.zsh_history that was not# there and came up empty with nothing said -- the one failure shape that looks# like the feature having nothing to offer. Name a shell here and it is used# whatever $SHELL says, which is the reason the setting still exists.## atuin is worth naming because anybody using it has no shell history worth# reading: atuin keeps the history in its own database and answers through its# own command.history = "auto"
# The history file, when it is not where the shell usually puts it.# history_file = "~/.zsh_history"
# How wide the pane is, as a percentage of the window. A percentage rather than# a column count, so a narrower terminal still splits sensibly.width_percent = 33
# The pane slides out rather than appearing. tmux has no animation primitive,# so this is a stepped resize-pane, eased so it reads as a slide rather than a# jump.## Every step sends SIGWINCH to the neighbouring pane, whose shell repaints its# prompt, so more steps is smoother here and flickerier next door. Zero opens# the pane at its full width at once.slide_steps = 5slide_ms = 150
# The shell the command runs under. Empty means $SHELL.## This used to be `zsh`, so on a machine without zsh -- most Linux boxes -- the# pane slid out and the command never ran.shell = ""
[earcons]# A short sound when something happens that a sighted person would see on the# bar: an agent that asked, one that finished, a health reason that appeared.# For somebody who hears the screen rather than glancing at it. A tone says# something happened, not what: inbox, brief and read-bar say what. Off by# default, held by quiet hours, and one sound per pass however many agents# arrived in it. `tmux-companion earcon asked` plays one, on or off, so you can# hear it first.enabled = false
# Which events sound. `done` is left out because an agent finishing is not# something waiting on you.on = ["asked", "health"]
# The command for each, as a list of words. Empty plays the platform's own:# macOS's Glass, Pop and Basso through afplay, or the freedesktop theme's# message-new-instant, complete and dialog-warning through paplay.## asked = ["afplay", "/System/Library/Sounds/Ping.aiff"]asked = []done = []health = []# The chunk clock's sound, Hero on macOS. Whether it plays is [chunk] sound,# not `enabled` or `on` here.chunk = []
[chunk]# How long you've been at tmux this sitting, across every session, with# nothing to start: time counts while a client has the OS focus (focus-events# on) and saw a key within break_after. At budget, one sound at the next# boundary, a window switch, a prompt coming back or an agent finishing its# turn, at most boundary_grace late. `tmux-companion chunk snooze` grants one# extension, then one more sound, then only the bar. Off by default.# docs/dev/design-chunk-clock.md is the design.enabled = false
# 45m, 1h, 90s. Nothing proves one length best; 50m is a choice.budget = "50m"
# The share of the budget past which the bar turns amber.warn_at = 0.8
# Away this long, in another app or with no key, is a break: the sitting# ends and the next one starts at 0. A shorter gap counts as work, so reading# an agent's long reply doesn't stop the clock.break_after = "10m"
# How long the sound may wait past the budget for a boundary.boundary_grace = "5m"
# The one extension `chunk snooze` grants.snooze = "5m"
# The sound plays through quiet hours, since it only comes while you're at# the keyboard. The desktop banner is held by them, and off by default.sound = truebanner = false
# After the time, ` · 1h14m`: how long the agent in the pane in front has been# running. It spends no budget. Off saves a `ps` every five seconds while an# agent is in front.agent_age = true
[clipboard]# The command to pipe a copy into. Unset picks one for the platform: pbcopy on# macOS, then wl-copy, then xclip.## This was two if-shell branches on `uname` in tmux.conf. One binary picking# the right command is one less thing the Linux branch has to special-case.# copy = "xclip -selection clipboard"
[theme]# Which theme a session gets before anybody has picked one.## docs/tmux.conf.full.example sets a `session-created` hook that runs# `theme apply` for every new session, and tmux.conf.starter.example ships the# same line commented out under "Later"; with that hook in place this decides# the colour of a session that nothing else claims. A session a project map# already claims keeps its own colour; this is only the answer for the rest.## `theme init` writes six: ember, pine, slate, plum, sand and ink, each with a# lighter and a darker sibling once `theme gen --shades` has run. Name one of# those, or one of your own.## `by-name` is the other answer: every unclaimed session gets one of the six# chosen from its name, the same one every time and on every machine, so a# server full of sessions is told apart at a glance with nothing picked and# nothing written down. The project picker shows a directory in the colour its# session is about to get. A theme picked with `theme pick` still wins.## Empty leaves unclaimed sessions unpainted, which is also what happens when# the theme named here is not on disk: there is nothing to source and nothing# is said about it, because this runs once per session created and a message# here lands on the terminal at the moment somebody opens a session.default = "ink"
# A theme per session-name prefix, where the prefix is everything before the# first `/`, so `w/api` and `w/web` both match `w`. Useful when session names# already carry a namespace; skip it otherwise and every session takes the# default above.## [theme.namespace]# w = "slate"# a = "plum"
[bar]# What your status bar itself looks like. Every segment here ends in a# powerline cap, and a cap is two colours: the segment's, and whatever is# behind it. That second one used to be a compiled-in `colour233`, which is one# person's tmux.conf and nobody else's, so a bar set to anything else got# wedges and outline backgrounds in a colour that appears nowhere on screen.## Set this to whatever `status-style` says. Anything tmux takes works, so# `colour233`, `#121212`, `black` and `default` are all fine; `default` leaves# the terminal's own background showing through, which is what a transparent# bar wants.## `tmux-companion doctor` reads the live `status-style` and says when the two# have drifted apart.background = "color233"
# The background behind the current window in the window list. Only the# `window` segment draws this, and only when that segment is turned on.current_window_background = "color236"# Draw the segments in colour. false keeps every count, glyph and percentage# and takes the colours away, which is also what NO_COLOR in the daemon's# environment does. The one thing colour alone says is that a branch has no# upstream yet; without colour that branch looks clean.colour = true
[picker]# Everything under here is the answer for every picker at once, and every one# of them is commented out because unset is not the same as set to the value# that happens to be the default: unset lets each picker use its own answer,# and writing one here takes that away from all six. The commented lines are# what you get when nobody writes anything.## How every picker is laid out: the key search, the cheat sheet, the project# list, the run history and the theme list all take their shape from here, so# one setting changes all of them rather than five drifting apart.## A calmer screen, for anybody a lot at once is too much: the list and the# line saying what the keys do, and nothing else.## preview = "none"# counter = false# rules = false## Where the preview pane goes: right, left, bottom, top, or none for no# preview at all. A picker whose rows have nothing to preview gets the whole# width for its list whatever this says.## Left out, each picker uses its own answer, and that is the useful default# because what goes in a preview is not the same thing twice:## keys bottom, 30% three lines: the chord and what it runs# project right, 80% a screen of whatever that session is doing# window right, 40% a directory listing# theme right, 70% a card showing what the theme paints, framed,# beside a narrow column of names# run none the command is the row# open bottom, 30% the command the application would run# panes right, 50% the last lines of that pane's screen## Setting it here overrides all seven at once, which is almost never what you# want; set it under `[picker.<name>]` instead.# preview = "right"
# The preview's share of the popup, as a percentage. Clamped to 20-80, because# outside that one half of the split is too narrow to read.## Left out, each picker uses its own share; the table above `preview` has them.## ctrl-p inside a picker cycles this through 30, 50, 70 and off, because there# is no drag-resize to reach for: a row too long for the list column can only be# read by giving the column more room.# preview_percent = 55
# How big the popup itself is belongs in tmux.conf, on the `display-popup -w`# and `-h` of the binding that opens it. Everything below is what the picker# draws inside whatever tmux gives it.
# The line the box is drawn with: none, plain, rounded, double or thick.## `none` is the one to set when the popup already has a border of its own --# `display-popup -B` turns tmux's off, and without that flag two borders end up# nested one inside the other.# border = "rounded"
# Where the picker says which picker it is: hidden, top-left, top-center,# top-right, bottom-left, bottom-center or bottom-right.## The label itself belongs to the picker rather than here, because only the# picker knows whether it is the key search or the theme list. This is where it# sits, and it is a setting because a label on the bottom border is fzf's habit# and a title over the list is skim's, and people arrive with one of the two.# label_position = "bottom-right"
# How many cells of border are left showing beyond the label, the corner# counted among them. Two is what fzf's `--border-label-pos=-3:bottom` draws,# and those cells are the difference between a label sitting on the line and a# label that has eaten the corner.# label_offset = 2
# Which end the line explaining the keys sits at: top, bottom or hidden. The# brief, the restore screen, the run dialog and the cheat sheet follow it too,# so "hidden" takes the line off every popup; a restore that is counting down# still says when it goes ahead. A hint of your own is best written the way the# shipped ones are: enter first, the picker's own keys, ctrl-a, the key that# leaves, then plain words, three spaces apart. Too wide for the popup, groups# drop from the right and the first and the leaving key stay.# hint_position = "top"
# Whether that line is a row across the whole popup, over the list and the# preview both, rather than a line in the list's column.# hint_across = false
# Which end the query sits at: top or bottom. `bottom` is fzf's default layout,# with the list growing up towards it; `top` is what `--reverse` does.# prompt_position = "bottom"
# Which end the first row sits at: top or bottom.## `bottom` is fzf's default, and it is not a cosmetic choice. The best match# ends up nearest the query, which is where your eye already is, and in a list# whose first rows are the sessions you were last in it makes the key, Up and# Enter a toggle between the last two. `top` is fzf's `--reverse-list`, and it# is the default here because a list that fills downwards is what everything# else on a screen does.# list_from = "top"
# Whether the `matched/total` counter is drawn beside the query. This is fzf's# `--no-info` inverted, and it is off here because on a list of fifteen keys the# count is a number nobody reads.# counter = false
# Rules between the hint, the list and the query. Without them the three run# together and the list has no edges of its own.# rules = true
# Drawn in front of the row the cursor is on. A bar rather than fzf's `>`,# because the row is also bold and a second marker made of punctuation reads as# part of the row.# marker = "▌"
# The order a row's columns are drawn in, by the position the picker built them# at. Empty draws them as built.## Whether the key or what it does comes first is the kind of thing people have# an opinion about and no argument for, so it is a list rather than a decision.# The key search builds `[key, what it does]`, so `[1, 0]` turns it round. A# position no row has is skipped, and a position left out is a column that is# not drawn, which is how a picker is made narrower without touching the code# that fills it.# column_order = []
# The fewest columns a list may be left with before a preview beside it is# moved underneath instead.## A percentage-sized popup on a small terminal is a small popup, and a split of# whatever it is given can leave the list too narrow to read a row in. What# decides it is the columns the list keeps, not the width of the popup: the# same popup leaves 37 columns at a 55% preview and 25 at 70%.## Zero turns the rule off, which is what fzf does: it splits whatever it is# given and truncates the rows.# min_list_width = 24
# How much of a border the preview pane gets: none, edge or full.## `edge` draws only the side facing the list, which is fzf's `border-left` and# the rest of that family. `full` draws a box round it. Which one is right# depends on what is in there: a directory listing wants a divider, a theme# card wants a frame.## The line is the one `border` above draws, or a rounded one when that is# `none`, so a picker inside a popup that already has a border can turn its own# off without losing the frame round its preview.# preview_border = "edge"
# Where the preview's label sits. The same seven positions as `label_position`.## A label sits on the nearest border line to the edge it names. With the# preview above or below the list that is the preview's own top or bottom# border. Beside the list, the preview's only border is the one-cell edge# between the two columns, with nowhere on it for words, so the label goes on# the outer box's border instead -- still within the preview's own columns, so# two labels on one bottom line each sit under the half they name.# preview_label_position = "bottom-center"
# How many cells of that line are left showing beyond the preview's label.# preview_label_offset = 2
# One picker's answer where it differs from the rest. Every key above can be# written again under one of these seven, and the preview is the one people# actually want to set per picker: a theme wants a tall pane beside a narrow# list, a key binding wants three lines under a wide one, and a shell history# wants no pane at all.## [picker.keys] the key search# [picker.project] the project and session list# [picker.window] the directory list a new window opens at# [picker.theme] the colour themes# [picker.run] the shell history# [picker.open] the application chooser# [picker.panes] every pane on the server, or the agents among them# [picker.setup] the checklist `setup` shows## Three keys exist only in these tables, because they are the words one picker# says and not a shape every picker shares: `label` is what it calls itself on# its border, `hint` is the line at the top naming the keys, and# `preview_label` is what it calls its preview. Each defaults to the tool's own# wording, and each is settable because a line somebody reads every day should# be in their words rather than in mine. Setting `preview_label` on a picker# that has nothing to preview does not give it a pane.## A key left out here takes whatever `[picker]` says, so these tables hold the# exceptions rather than a second copy of everything:## [picker.keys]# label = "[ Keys ]"# hint = "enter run ctrl-a show all esc cancel"# preview_label = "[ What it runs ]"# preview = "bottom"# preview_percent = 30# preview_label_position = "top-center"# # a keys row is [chord, filed under, what it does]. What it does goes# # last: its column is as wide as the longest one, a bare command included,# # and anything after it is pushed off the edge. A position left out isn't# # drawn, so [1, 0] hides the description.# column_order = [1, 0, 2]## [picker.run]# preview = "none"
[open]# What happens when `open` finds a file rather than a URL: which pane the# editor opens in, and which editor.## `right` puts it beside the pane you were in, `bottom` puts it underneath.# Right by default, because a file and the log you found it in read better# side by side; bottom is better on a narrow terminal, where two columns of# sixty are two columns nobody can read.split = "right"
# How much of the window that pane takes, as a percentage. Zero lets tmux# halve it, which is what it did before this was a setting.size_percent = 0
# The editor, and how it is told to jump to a line and column. `{path}`,# `{line}` and `{column}` are replaced; the path arrives already quoted for# the shell, so do not put quotes around `{path}` yourself.## The quotes around the `+call` matter. tmux runs a split's command through# `sh`, and `nvim +call cursor(2,22) file` is a shell syntax error, so the# pane opened, complained to nobody, and closed again. Opening a file at a# line had never worked until they were added.## Some others:# helix "hx {path}:{line}:{column}"# emacs "emacsclient -nw +{line}:{column} {path}"# vim "vim '+call cursor({line},{column})' {path}"editor = "nvim '+call cursor({line},{column})' {path}"
# What `open --choose` (or `open -i`) offers. Empty means no chooser, and# `--choose` then opens what it would have opened anyway rather than showing a# picker with nothing in it.## `{url}` and `{path}` both take the thing being opened, so a browser template# and an editor template each name whichever reads better and neither has to# know which kind of thing it was handed. `{line}` and `{column}` are zero for# a URL. The command goes through the shell, because a template is where the# quoting lives: an application name with a space in it only reaches `-a`# intact if you wrote the quotes.## `pane` decides whether it opens in a tmux pane beside the one you are in, the# way the editor above does, or is launched and left alone. An editor wants a# pane and a browser does not, and the wrong answer is either a browser holding# a pane open forever or an editor with nowhere to draw.## [[open.application]]# name = "chrome"# command = "open -a 'Google Chrome' {url}"## [[open.application]]# name = "nvim"# command = "nvim '+call cursor({line},{column})' {path}"# pane = true
[keys]# Keys tmux and the apps in its panes both want. `keys discover` reads what# each layer binds on its own, nvim live over its socket; a table here adds# claims for an app nothing can read, matched on the name tmux shows as the# pane's command. `keys collide` shows where they meet.
# Whether `keys route`, the last line of tmux.conf, wraps tmux's root bindings# so a key the app in front wants is held rather than taken. Off by default:# it rewrites your root table. Needs tmux 3.4.route = false
# How long a held key waits, in milliseconds. A second press inside it goes to# the app; when it runs out, or another key comes, tmux runs its binding.hold_ms = 170
# [keys.app.nano]# claims = ["C-o", "C-x", "C-w"]## Which modes of what discovery found count as claims. Unset is normal mode# for nvim and vim, so M-1 mapped in normal mode isn't held in insert mode,# and every mode for anything else.# [keys.app.nvim]# modes = ["n", "x"]
# One table per key, in tmux's spelling. `route` settles a key both sides# claim: "hold" (the default) waits hold_ms for a second press, "app" hands it# to the app at once while it claims it, the way vim-tmux-navigator shares# M-h/j/k/l, and "tmux" never lets the app have it. `hold_ms` overrides the one# above. Any other field names a layer and a word or two its binding's note,# description or command should contain; `keys collide` lists one that# doesn't as drift. `intent` is a label for you.## These tables, `[keys.app.*]` and the two settings above can also live in# keys.toml beside this file, without the "keys." prefix. Both count, and a# name set in both is an error.## [keys.key."M-h"]# route = "app"## [keys.key."M-1"]# intent = "tree"# tmux = "choose-tree"# nvim = "NvimTree"