Sesh, the smart tmux session manager
Sesh is a CLI that helps you create and manage tmux sessions quickly and easily using zoxide.
- Smart session creation - automatically names sessions based on git repo, git remote, or directory
- Zoxide integration - jump to your most-used projects instantly
- Session configuration - define startup commands, windows, and preview commands per project in
sesh.toml - Wildcard configs - apply settings to all projects matching a glob pattern
- Built-in picker - interactive session selector, or integrate with fzf, television, or gum
- Clone and connect - clone a git repo and start a session in one step
- Mkdir and connect - create a new directory (relative or absolute path) and start a session in one step
- Last session switching - seamlessly bounce between your two most recent sessions
- Root session navigation - jump to the root of a git worktree or repository
- Nerd Font icons - display session type icons in your picker
- Shell completions - tab completion for Bash, Zsh, Fish, and PowerShell
[!NOTE]
⭐ If you find sesh useful, please star the repo — it helps the project grow and reach more developers!
[!TIP]
Want to see more features and help sesh mature? Consider sponsoring the project to support ongoing development.
Table of Contents
Videos
Intro from Josh Medeski (sesh's creator) |
Review from DevOps Toolbox |
How to install
Homebrew
To install sesh, run the following homebrew command:
brew install sesh
Arch Linux AUR
To install sesh, run the following yay command:
yay -S sesh-bin
Fedora (Copr)
Sesh is available from the buckaroogeek/Tmux_sesh Copr repository (Fedora and EPEL 10):
sudo dnf copr enable buckaroogeek/Tmux_sesh
sudo dnf install tmux-sesh
Go
Alternatively, you can install Sesh using Go's go install command:
go install github.com/joshmedeski/sesh/v2@latest
This will download and install the latest version of Sesh. Make sure that your Go environment is properly set up.
Conda
To install sesh, run one of the following commands, depending on your setup:
- Conda/(micro)mamba users
# Replace with mamba/micromamba if required
conda -c conda-forge install sesh
- Pixi users
pixi global install sesh
Nix
See the nix package directory for instructions on how to install sesh through the nix platform.
Note: Do you want this on another package manager? Create an issue and let me know!
Shell Completion
Sesh supports shell completion (tab completion) for Bash, Zsh, Fish, and PowerShell. This helps you discover commands, flags, and arguments by pressing the Tab key.
Bash
# Generate completion script
sesh completion bash > sesh-completion.bash
Install system-wide (recommended)
sudo cp sesh-completion.bash /etc/bash_completion.d/
Or install user-only
mkdir -p ~/.local/share/bash-completion/completions
cp sesh-completion.bash ~/.local/share/bash-completion/completions/sesh
Reload your shell
source ~/.bashrc
Zsh
# Generate completion script
sesh completion zsh > _sesh
Install system-wide (recommended)
sudo mkdir -p /usr/local/share/zsh/site-functions
sudo cp _sesh /usr/local/share/zsh/site-functions/
Or install user-only
mkdir -p ~/.zsh/completions
cp _sesh ~/.zsh/completions/
echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc
echo 'autoload -U compinit && compinit' >> ~/.zshrc
Reload your shell
source ~/.zshrc
Fish
# Generate and install completion
sesh completion fish > ~/.config/fish/completions/sesh.fish
Reload fish configuration
source ~/.config/fish/config.fish
PowerShell
# Generate completion script
sesh completion powershell > sesh.ps1
Create PowerShell profile directory if it doesn't exist
mkdir -p (Split-Path $PROFILE)
Add to PowerShell profile
Add-Content $PROFILE ". /path/to/sesh.ps1"
Reload PowerShell
& $PROFILE
After setting up completion, you can press Tab while typing sesh to see available commands, flags, and arguments.
Extensions
Raycast Extension
The sesh companion extension for Raycast makes it easy to use sesh outside of the terminal.
Here are limitations to keep in mind:
- tmux has to be running before you can use the extension
- The extension caches results for a few seconds, so it may not always be up to date
Ulauncher Extension
For Linux users using Ulauncher there are two extensions to use sesh outside the terminal:
Here are limitations to keep in mind for Sesh Session Manager:- tmux has to be running before you can use the extension
Walker launcher usage (Linux)
Create an action directly on $XDG_CONFIG_HOME/config.toml
[[plugins]]
name = "sesh"
prefix = ";s "
src_once = "sesh list -d -c -t -T"
cmd = "sesh connect --switch %RESULT%"
keep_sort = false
recalculate_score = true
show_icon_when_single = true
switcher_only = true
For the dmenu mode you can use:
Fish shell:
set ssession $(sesh l -t -T -d -H | walker -d -f -k -p "Sesh sessions"); sesh cn --switch $ssession
Bash/Zsh:
ssession=$(sesh l -t -T -d -H | walker -d -f -k -p "Sesh sessions"); sesh cn --switch $ssession
##### For dmenu launchers replace walker -dfk with dmenu or rofi)
Agent skill
Sesh ships an agent skill that teaches coding agents to start work through sesh instead of shelling out to tmux new-session, tmux new-window, and tmux send-keys. An agent that has it will pick a session or a window depending on whether the work belongs to something already open, resolve names and directories across tmux, zoxide, config, and tmuxinator, reuse what's already running instead of duplicating it, and leave you where you are when the work is a background step.
Install it with the skills CLI:
# globally, for every project
npx skills add joshmedeski/sesh -g
or into the current project only
npx skills add joshmedeski/sesh
It works with Claude Code, Codex, Cursor, OpenCode, and dozens of other agents — skills add will ask which ones to install to, or take -a claude-code to pick.
The skill lives at skills/sesh/SKILL.md and is versioned with the CLI, so npx skills update sesh picks up changes. To install it by hand instead, copy or symlink that file to ~/.claude/skills/sesh/SKILL.md (or your agent's equivalent skills directory).
How to use
tmux for sessions
tmux is a powerful terminal multiplexer that allows you to create and manage multiple terminal sessions. Sesh is designed to make managing tmux sessions easier.
zoxide for directories
zoxide is a blazing fast alternative to cd that tracks your most used directories. Sesh uses zoxide to manage your projects. You'll have to set up zoxide first, but once you do, you can use it to quickly jump to your most used directories.
Basic usage
Once tmux and zoxide are setup, sesh list will list all your tmux sessions and zoxide results, and sesh connect {session} will connect to a session (automatically creating it if it doesn't exist yet). It is best used by integrating it into your shell and tmux.
fzf
The easiest way to integrate sesh into your workflow is to use fzf. You can use it to select a session to connect to:
sesh connect $(sesh list | fzf)
tmux + fzf
In order to integrate with tmux, you can add a binding to your tmux config (tmux.conf). For example, the following will bind ctrl-a T to open a fzf prompt as a tmux popup (using fzf-tmux) and using different commands to list active sessions (sesh list -t), configured sessions (sesh list -c), zoxide directories (sesh list -z), and find directories (fd...).
bind-key "T" run-shell "sesh connect \"$(
sesh list --icons | fzf-tmux -p 80%,70% \
--no-sort --ansi --border-label ' sesh ' --prompt '⚡ ' \
--header ' ^a all ^t tmux ^g configs ^x zoxide ^d tmux kill ^f find' \
--bind 'tab:down,btab:up' \
--bind 'ctrl-a:change-prompt(⚡ )+reload(sesh list --icons)' \
--bind 'ctrl-t:change-prompt(🪟 )+reload(sesh list -t --icons)' \
--bind 'ctrl-g:change-prompt(⚙️ )+reload(sesh list -c --icons)' \
--bind 'ctrl-x:change-prompt(📁 )+reload(sesh list -z --icons)' \
--bind 'ctrl-f:change-prompt(🔎 )+reload(fd -H -d 2 -t d -E .Trash . ~)' \
--bind 'ctrl-d:execute(tmux kill-session -t {2..})+change-prompt(⚡ )+reload(sesh list --icons)' \
--preview-window 'right:55%' \
--preview 'sesh preview {}'
)\""
You can customize this however you want, see man fzf for more info on the different options.
To continuously refresh previews of active tmux sessions, add --watch to the preview command:
--preview 'sesh preview --watch {}'
Watch mode polls only active tmux sessions; directory and configured-session previews still render once. The default interval is 500 milliseconds and can be changed with --interval 100ms.
Custom list formatting
--format lets external pickers control how each sesh list row is rendered without changing the default output. The following placeholders are available:
| Placeholder | Value |
| --- | --- |
| {name} | The normal display name, including the source icon when --icons is set |
| {session} | The raw session name |
| {source} | Session source (tmux, config, zoxide, etc.) |
| {path} | Session path |
| {active_window_name} | The active tmux window name; empty for non-tmux sessions |
| {active_window_name_prefix} | The active window name followed by a space, or empty when there is none |
For example, used in combination with tmux plugin tmux-nerd-font-window-name
sesh list --icons --format '{active_window_name_prefix}{name}'
neovim-config
sesh
dotfiles
To use the active-window icon instead of the normal tmux source icon while keeping source icons for config/zoxide/etc., exclude only the tmux source icon:
sesh list --icons --icons-exclude tmux --format '{active_window_name_prefix}{name}'
Foreground colors can be scoped directly in the format using {fg:COLOR} and {/fg}. This keeps presentation in the format instead of tying a color to a particular placeholder:
sesh list --icons --icons-exclude tmux \
--format '{fg:yellow}{active_window_name_prefix}{/fg}{name}'
Colors can be applied to any part of the row, for example:
sesh list --format '{fg:gray}{source}{/fg} {fg:bright-cyan}{active_window_name}{/fg} {name}'
Supported colors are black, red, green, yellow, blue, magenta, cyan, white, and their bright-* variants. gray/grey are aliases for bright-black. --no-color removes format color tokens as well as built-in source-icon coloring.
The active window names are fetched live in one tmux call and only when the format uses an active-window-name placeholder. A tmux lookup failure simply leaves the active-window-name placeholders empty. --format cannot be combined with --json.
sesh list --json reports what connecting will actually use: each session's resolved Icon (the same one the picker shows), its Alias, AliasAutoConnect, and Tmuxp, the matching Wildcard pattern, and StartupCommand, PreviewCommand, WindowNames, and WindowConfigs with [[wildcard]] and [default_session] fallbacks applied. tmux sessions include their live TmuxWindows (Name, Index, Path, Active) whether or not [tui] show_windows is set. Sessions under a [[worktree]] root include Worktree (Repo, Number, Title, State), with Title and State read from the issue cache that sesh worktree list fills; it is null for every other session.
Arbitrary formatted rows are display output and are not parsed back by sesh connect. When using fzf, keep the raw session name in a hidden field and show only the formatted field:
selected="$(
sesh list --icons --format $'{session}\t{active_window_name_prefix}{name}' |
fzf --delimiter=$'\t' --with-nth=2
)"
sesh connect "${selected%%$'\t'*}"
tmux + television
If you prefer to use television instead of fzf, you can add a binding to your tmux config that opens the sesh channel in a tmux popup.
bind-key "T" display-popup -E -w 80% -h 70% -d '#{pane_current_path}' -T 'Sesh' tv sesh
Use Ctrl-s to cycle through the sources, and Ctrl-d to kill the highlighted session.
Window management
sesh window (alias w) lets you list, switch to, and create tmux windows within a session — similar to how sesh list and sesh connect work for sessions. It's a group of subcommands:
sesh window list # list the windows of a session
sesh window connect # select a window, creating it if it doesn't exist
List windows
sesh window list # the session you're attached to
sesh window list -t work # another session
sesh window list -j # as json
Switch to an existing window by name
sesh window connect editor
If a window named editor exists in the target session, sesh selects it. If it doesn't, sesh creates it.
Create a new window at a directory
sesh window connect ~/projects/my-app
A directory argument names the window after its basename (my-app) and roots it there. Running it again selects the window the first run created rather than opening a duplicate.
Run a command in the window
sesh window connect claude -t 'second brain' -c 'claude "summarize my notes"'
On a created window the command becomes the window's process, so the window closes when it exits. On an existing window it's typed into whatever is already running there. Pass --new when every invocation should get its own fresh window instead of reusing the name.
Target a specific session
Use --target / -t to manage windows in a session other than the one you're attached to:
sesh window list -t work
sesh window connect ~/projects/my-app -t work
The target is resolved the same way sesh connect resolves a session — across tmux, zoxide, config, and tmuxinator — and started if it isn't running yet, without moving you out of the session you're in. Use -b / --background to create the window without being taken to it, and -s / --switch when triggering sesh from outside the terminal.
[!NOTE]
sesh windowandsesh window -swere replaced bysesh window connectandsesh window list -t.--sessionstill works as a deprecated alias for--target, and-snow means--switchonsesh window connect, consistent withsesh connect.
fzf integration
You can combine the two subcommands with fzf to interactively switch windows:
sesh window connect "$(sesh window list | fzf)"
Or as a tmux keybind:
bind-key "W" run-shell "sesh window connect \"$(sesh window list | fzf-tmux -p 60%,50% --prompt '🪟 ')\""
Create a directory and connect
sesh mkdir (alias md) combines mkdir and sesh connect into a single step: it creates the directory if it doesn't already exist, then connects to it as a session — no need to cd into it first or wait for zoxide to pick it up.
sesh mkdir my-new-project
accepts both relative paths (resolved against your current working directory) and absolute paths, as well as ~ for your home directory:
sesh mkdir my-new-project # relative to the current directory
sesh mkdir ~/projects/my-app # relative to your home directory
sesh mkdir /Users/josh/dev/api # absolute path
gum + tmux
If you prefer to use charmbracelet's gum then you can use the following command to connect to a session:
bind-key "K" display-popup -E -w 40% "sesh connect \"$(
sesh list -i | gum filter --limit 1 --no-sort --fuzzy --placeholder 'Pick a sesh' --height 50 --prompt='⚡'
)\""
Note: There are less features available with gum compared to fzf, but I found its matching algorithm is faster and it has a more modern feel.
[!WARNING]
As of gum v0.15.0 you have to add the --no-strip-ansi in order to display the icons correctly.
See my video, Top 4 Fuzzy CLIs for more inspiration for tooling that can be integrated with sesh.
sesh picker
Sesh has a built-in picker that can be used as a tmux popup:
bind-key "K" display-popup -h 90% -w 50% -E "sesh picker -i"
zsh keybind
If you use zsh, you can add the following keybind to your .zshrc to connect to a session:
function sesh-sessions() {
{
exec </dev/tty
exec <&1
local session
session=$(sesh list -t -c | fzf --height 40% --reverse --border-label ' sesh ' --border --prompt '⚡ ')
zle reset-prompt > /dev/null 2>&1 || true
[[ -z "$session" ]] && return
sesh connect $session
}
}
zle -N sesh-sessions
bindkey -M emacs '\es' sesh-sessions
bindkey -M vicmd '\es' sesh-sessions
bindkey -M viins '\es' sesh-sessions
After adding this to your .zshrc, you can press Alt-s to open a fzf prompt to connect to a session.
Recommended tmux Settings
I recommend you add these settings to your tmux.conf to have a better experience with this plugin.
bind-key x kill-pane # skip "kill-pane 1? (y/n)" prompt
set -g detach-on-destroy off # don't exit from tmux when closing a session
Bonus
Last
The default command will "Switch the attached client back to the last session." However, if you close a session while detach-on-destroy off is set, the last session will not be found. To fix this, I have a sesh last command that will always switch the client to the second to last session that has been attached.
Add the following to your tmux.conf to overwrite the default last-session command:
bind -N "last-session (via sesh) " L run-shell "sesh last"
Enrich session names with the GitHub issue title
sesh can rename a session to include its branch's GitHub issue title, e.g.
400-status → 400-status — warm the status cache. It parses the issue number
from the branch name and looks it up with the gh CLI (which must be installed
and authenticated).
Add an opt-in tmux hook so every new session is enriched in the background:
set-hook -g session-created 'run-shell -b "sesh rename --enrich"'
The command is a no-op when the branch has no resolvable issue (the session keeps its plain name), and it self-heals: switching to a branch without an issue renames the session back to its base name. Reconnecting to the directory reattaches to the enriched session rather than creating a duplicate.
Connect to root
While working in a nested session, you may way to connect to the root session of a git worktree or git repository. To do this, you can use the --root flag with the sesh connect command.
I recommend adding this to your tmux.conf:
bind -N "switch to root session (via sesh) " 9 run-shell "sesh connect --root $(pwd)"
Filter by root
If you want to filter your search by the root of the active project, you can modify your piker by using the sesh root command:
bind-key "R" display-popup -E -w 40% "sesh connect \"$(
sesh list -i -H | gum filter --value \"$(sesh root)\" --limit 1 --fuzzy --no-sort --placeholder 'Pick a sesh' --prompt='⚡'readme
)\""
I have this bound to so I can use an alternative binding.
Note: This will only work if you are in a git worktree or git repository. For now, git worktrees expect a .bare folder.
Configuration
You can configure sesh by creating a sesh.toml file in your $XDG_CONFIG_HOME/sesh or $HOME/.config/sesh directory.
mkdir -p ~/.config/sesh && touch ~/.config/sesh/sesh.toml
Custom Config Path
You can specify a custom config file path using the --config (or -C) flag. This is useful for NixOS wrappers, maintaining separate work/private configurations, or testing.
sesh -C /path/to/custom/sesh.toml list
sesh --config /path/to/custom/sesh.toml connect my-session
The flag works with any subcommand. When specified, the file must exist or sesh will return an error. Without the flag, sesh uses the default config path.
Custom Multiplexer (psmux)
Sesh uses tmux as the default terminal multiplexer, but you can configure it to use any tmux-compatible multiplexer like psmux by setting tmux_command in your sesh.toml:
tmux_command = "psmux"
This replaces the tmux binary in all commands sesh runs (session creation, switching, attaching, etc.). The configured multiplexer must support tmux's CLI interface.
Custom Frecency Backend (fasd, autojump, etc.)
Sesh uses zoxide as its default frecency directory-jumping backend, but you can point it at an alternative such as fasd, fasder, autojump, or memy by overriding the commands in a [frecency] table. This is useful for tools that track files _and_ directories, unlike zoxide which tracks directories only.
[frecency]
list_command = "fasd -d -l -R" # list all tracked entries
query_command = "fasd -d {}" # resolve one input to a path
add_command = "fasd -A {}" # record a path after connecting
remove_command = "fasd -D {}" # remove a path (picker ctrl+x)
- The
{}placeholder is replaced with the query string (query_command) or the path (add_command,remove_command), the same substitution used bypreview_command. list_commandoutput is parsed one path per line, most-frecent first. A leading numeric score is detected automatically when present (as with zoxide's--score); otherwise the score is0.- Any command runs as a single binary (no shell), so pipes/redirects aren't supported.
- Any field you omit falls back to its zoxide default (
zoxide query --list --score,zoxide query {},zoxide add {},zoxide remove {}), so an absent[frecency]table leaves behavior unchanged. - The source label in
sesh listoutput stayszoxide, so existing integrations that read the--jsonoutput keep working.
Schema (Editor Autocomplete)
Sesh provides a JSON Schema for sesh.toml that enables autocomplete, validation, and documentation in your editor. This works with any editor that supports the taplo TOML language server.
Quick Setup
Add this line to the top of your sesh.toml:
#:schema https://github.com/joshmedeski/sesh/raw/main/sesh.schema.json
That's it! Taplo will read the directive and apply the schema automatically.
Neovim
Make sure you have taplo installed:
brew install taplo
If you're using nvim-lspconfig, taplo should work out of the box with the #:schema directive above. Alternatively, you can configure the schema association directly:
require("lspconfig").taplo.setup({
settings = {
taplo = {
schema = {
associations = {
[".*sesh\\.toml$"] = "https://github.com/joshmedeski/sesh/raw/main/sesh.schema.json",
},
},
},
},
})
Blacklist
You may want to blacklist certain tmux sessions from showing up in the results. For example, you may want to exclude your scratch directory from the results.
blacklist = ["scratch"]
To surface blacklisted sessions for management, pass -b (or --blacklisted) to sesh list. It composes with source flags, so sesh list -b -t lists blacklisted tmux sessions only.
Directory Length
Control how many directory components are used for session names. Default is 1 (only the directory basename).
dir_length = 2 # Uses last 2 directories: "projects/sesh" instead of just "sesh"
[!NOTE]
Works great with tmux-floax
Session Name Substitution
Some directories produce long, repetitive session names -- everything under
~/c/dotfiles/.config shows up with that whole prefix. name_substitution
rules rewrite the path a name is derived from so you can shorten or relabel it.
[[name_substitution]]
find = "~/c/dotfiles/.config/"
replace = ""
With that rule, connecting to ~/c/dotfiles/.config/nvim creates a session
named nvim instead of the full path. Rules are matched against the
home-collapsed path (a leading ~) for every path-derived source -- zoxide,
directories, and wildcards. Existing tmux sessions keep their names, and if no
rule matches, naming falls back to the usual git and directory strategies, so
adding rules never changes how anything else is named.
Rules apply in order, each one seeing the result of the previous, so they can
compose. find is matched literally by default; set regex = true to treat it
as a Go regular expression, which lets
replace reference capture groups with $1, $2, and so on:
[[name_substitution]]
find = "./workspace/[0-9]+_(.)"
replace = "ws-$1"
regex = true
[!NOTE]
tmux session names can't contain.or:, and spaces are turned into_,
so those characters in replace are normalized in the final name.
Sorting
If you'd like to change the order of the sessions shown, you can configure sort_order in your sesh.toml file
sort_order = [
"tmuxinator", # show first
"config",
"tmux",
"zoxide", # show last
]
The default order is tmux, config, tmuxinator, and then zoxide.
You can omit session types if you only care about the order of specific ones.
sort_order = [
"config", # resulting order: config, tmux, tmuxinator, zoxide
]
Merging sources into one score-ordered group
By default every source is its own block, so a config session you open daily
still sits below zoxide paths you haven't touched in weeks. Nest sources in
sort_order to merge them into a single block ordered by zoxide score, highest
first:
sort_order = [
"tmux", # live sessions stay pinned on top
["config", "zoxide"], # merged, ordered by zoxide score
]
Sessions from a source that carries no score of its own — a [[session]] block,
for instance — borrow the score zoxide has for their path, so they sort by how
often that directory is actually visited. A path zoxide has never seen scores 0
and trails the group.
Groups still appear in the order they're listed, and a flat sort_order behaves
exactly as it always has: merging is opt-in.
Group separator
Once sources are interleaved, the boundary between live tmux sessions and
everywhere else stops being obvious from position alone. group_separator draws
a faint rule between the sort_order groups in the picker:
[tui]
group_separator = true
The rule is never selectable and the cursor steps straight over it. It is suppressed while you're filtering, where results are reordered by match quality and the groups no longer line up with contiguous ranges.
Cache
[!WARNING]
This feature is experimental and may not work as expected.
Sesh can cache session lists to speed up repeated calls. Caching is opt-in and disabled by default. When enabled, sesh stores results at $XDG_CACHE_HOME/sesh/sessions.gob (default ~/.cache/sesh/sessions.gob) and uses a stale-while-revalidate strategy with a 5-second TTL:
- Cold start: no cache exists, fetches live data and writes the cache
- Fresh hit (within 5s): returns cached data instantly
- Stale hit (after 5s): returns cached data instantly and refreshes the cache in the background
sesh connect.
cache = true
Sessions created or killed outside sesh connect (e.g. plain tmux new-session, closing a session's last window) are missing from the cache until the next stale-hit refresh. To keep the cache current, run sesh cache refresh when those events happen — it fetches live data and rewrites the cache, and does nothing when the cache is disabled. With tmux hooks this covers both events:
set-hook -g session-created 'run-shell -b "sesh cache refresh"'
set-hook -g session-closed 'run-shell -b "sesh cache refresh"'
If you use a kill binding in an fzf picker (like ctrl-d in the example above), refresh the cache before reloading the list so the killed session disappears immediately:
--bind 'ctrl-d:execute(tmux kill-session -t {2..}; sesh cache refresh)+change-prompt(⚡ )+reload(sesh list --icons)' \
Picker TUI
The Picker TUI can be configured with some options that help you customize it's behaviour, this picker is a useful replacement to external fuzzy pickers.
[tui]
prompt = "> "
placeholder = "Filter sessions... "
show_icons = false
show_windows = false
group_separator = false
window_name_format = "#{window_name}"
alias_auto_connect_delay = "150ms"
alias_filter_prefix = "/"
preview = false
preview_width = 60
preview_min_width = 100
preview_border = "line"
With show_windows = true, each row also lists the names of the windows inside that session, dimmed after the session name. Window names that don't fit are summarized as +N:
> sesh editor server logs
dotfiles nvim shell
my-project code server db +2
scratch
Window names are display-only: selecting a row still returns just the session name, and typing a window name does not match its session. The names for live tmux sessions are fetched in a single tmux call, so the option costs the same regardless of how many sessions you have.
window_name_format accepts any tmux format. tmux interprets conditionals, fallback variables, and #() commands. For example, this format uses the pane title when it exists and uses the window name otherwise:
window_name_format = "#{?#{pane_title},#{pane_title},#{window_name}}"
alias_auto_connect_delay and alias_filter_prefix tune aliases — see Session Aliases.
Custom icons
With show_icons = true, each row gets a glyph for where the session came from — tmux, config, zoxide, tmuxinator. That says where it was found, not what it is, so [[session]] and [[wildcard]] blocks can name their own icon instead. It can be any string: a nerd font glyph or an emoji.
[[session]]
name = "sesh"
path = "~/c/sesh"
icon = ""
[[session]]
name = "notes"
path = "~/second-brain"
icon = "📓"
[[wildcard]]
pattern = "~/c/work/*"
icon = "🏠"
> sesh
📓 notes
🏠 work-api
dotfiles
The most specific match wins: an exact [[session]] name, then its path — so the same directory listed by zoxide under a derived name still gets the icon — then a [[wildcard]] pattern. If several patterns match, the first in config order wins, as it does for startup_command. Anything with no icon of its own keeps its source glyph, and icon = "" counts as unset.
Custom icons render unstyled: emoji bring their own color, and nerd font glyphs take the default foreground. The icon column is padded to the widest icon you configured, so a double-width emoji on one row doesn't push its name out of line with the rest.
If one icon still sits a column off, add a trailing space to it:
[[session]]
name = "update"
path = "~/c/update"
icon = "⬆️ " # note the trailing space
Terminals disagree about how wide an emoji is, and nothing in a TUI can ask which way yours went. Emoji written with a variation selector — ⬆️ is U+2B06 plus U+FE0F, as are 🖼️ and 🖥️ — measure as two cells but are drawn in one by WezTerm and others, which leaves that row short. A trailing space is counted into the row but not into the column width, so it fixes the one icon without shifting anything else.
This is picker-only. sesh list --icons keeps the source glyphs, because the scripts and external pickers that parse its output trim a known-width glyph — see #246. Custom icons are also suppressed entirely with show_icons = false.
Starting with a filter
--query (-q) opens the picker with its filter already typed out, which is handy for tmux keybinds that scope the list to one slice of your sessions:
bind-key "P" display-popup -h 90% -w 50% -E "sesh picker -q work/"
The query goes in exactly as if you'd typed it, so the list is narrowed the moment sessions load, and backspacing widens it again. Sigils work too — -q '#' opens straight into number jumping and -q / into alias mode.
A query that matches a single session doesn't connect on its own; the picker opens with that row highlighted and waits for enter. Neither does a query that spells out an alias with alias_auto_connect on — auto-connect stays a reward for actually typing it, so a scripted --query never connects somewhere you didn't look.
This is per-invocation only, with no [tui] equivalent: a default query would silently hide sessions on every launch.
Jumping by number
Typing # as the first character numbers the rows and turns the next digit into a jump — the fastest way to reach one of your first few sessions without reading their names:
filter: #
> 1 sesh
2 dotfiles
3 my-project
Pressing 3 connects to my-project immediately. Only 1–9 jump, and only the first nine rows are numbered.
The numbers follow the visible list, so anything typed after the sigil narrows it first and renumbers what's left — #a then 2 jumps to the second match for a. A digit with no row at that position does nothing rather than filtering.
Only a leading # counts, so feat#123 filters normally. If you configure alias_filter_prefix = "#", alias mode wins and this mode is unreachable.
Removing a row
ctrl+x gets rid of the highlighted row, and what that means depends on where the row came from: a live tmux session is killed, a zoxide entry is pruned from the frecency backend. Either way it happens behind a confirmation.
A directory you deleted or renamed keeps showing up in the picker until zoxide is told about it, so ctrl+x on a zoxide row prunes it where you noticed it:
╭────────────────────────────────────────────────────────╮
│ │
│ Do you want to remove this directory from zoxide? │
│ │
│ ~/c/some-old-project │
│ │
│ Yes No │
│ │
╰────────────────────────────────────────────────────────╯
On a tmux row it runs tmux kill-session instead, which is the built-in launcher's equivalent of the ctrl+d binding in the fzf setup:
╭────────────────────────────────────────────────────────╮
│ │
│ Do you want to kill this tmux session? │
│ │
│ my-project │
│ │
│ Yes No │
│ │
╰────────────────────────────────────────────────────────╯
y or enter confirms, n, q, or esc cancels, and ←/→ or tab move between the buttons. Nothing typed while the dialog is open reaches the filter, so the list is exactly as you left it either way.
Only tmux and zoxide rows can be removed — a [[session]] block or a tmuxinator config describes a session that might exist, and getting rid of either means editing a file, so ctrl+x says so and does nothing. The row disappears only once the command actually succeeded; a backend that refuses it reports the error and leaves the row in place.
Killing the session you are attached to is allowed: tmux moves your client to another session, or exits if that was the last one. The picker goes with it, since it was running there.
The zoxide removal runs zoxide remove {} by default, or whatever remove_command you set — see Custom Frecency Backend. With cache = true, the cache is rewritten behind either action so the next launch doesn't list what just went away.
Preview pane
With preview = true, the highlighted session is previewed beside the list, using exactly the same output as sesh preview — live tmux panes via capture-pane, your preview_command for configured sessions, and a directory listing otherwise:
> sesh │ $ eza --icons
dotfiles │ README.md main.go
my-project │ picker/ previewer/
The pane is off by default, and ctrl+o toggles it at any time.
preview_width is the share of the terminal, in percent, guaranteed to the pane. The session list is capped at 60 columns, so on a wide terminal everything past that cap goes to the preview on top of this share; on a narrow one the list is never squeezed below 40 columns.
preview_border picks the divider between the two panes: line (default), thick, double, or none for no divider at all. Turning it off gives the divider's column to the preview text:
[tui]
preview_border = "none"
> sesh $ eza --icons
dotfiles README.md main.go
my-project picker/ previewer/
preview_min_width (default 100) is the narrowest terminal that gets a split at all. Below it the picker renders the list only, and the pane comes back on its own once the window has room — handy in small tmux popups. Preview commands run in the background as the cursor moves, so a slow one never blocks the list, and a failing one is reported in the pane instead of taking the picker down.
Default Session
The default session can be configured to run a command when connecting to a session. This is useful for running a dev server or starting a tmux plugin.
Additionally, you can define a preview command that runs when previewing the session's directory. This can be handy for displaying files with tools like eza or lsd.
Note: The {} will be automatically replaced with the session's path.
[default_session]
startup_command = "nvim -c ':Telescope find_files'"
preview_command = "eza --all --git --icons --color=always {}"
If you want to disable the default start command on a specific session, you can set disable_startup_command = true.
Session Configuration
A startup command is a command that is run when a session is created. It is useful for setting up your environment for a given project. For example, you may want to run npm run dev to automatically start a dev server.
Note: If you use the --command/-c flag, then the startup script will not be run.
I like to use a command that opens nvim on session startup.
You can also define a preview command to display the contents of a specific file using bat or any another file previewer of your choice.
[[session]]
name = "Downloads 📥"
path = "~/Downloads"
startup_command = "ls"
[[session]]
name = "tmux config"
path = "~/c/dotfiles/.config/tmux"
startup_command = "nvim tmux.conf"
preview_command = "bat --color=always ~/c/dotfiles/.config/tmux/tmux.conf"
A session can also set icon to replace the source glyph it gets in the picker — see Custom icons.
Session Aliases
Fuzzy matching is great for discovery, but the top result for wp shifts as sessions come and go, so muscle memory never quite forms. An alias gives a session a short, fixed name you can always count on:
[[session]]
name = "wallpaper"
path = "~/c/wallpaper"
alias = "wp"
alias_auto_connect = true
[[session]]
name = "dotfiles"
path = "~/.config"
alias = "dot"
Aliases must be unique (case-insensitively) — sesh reports an error at startup if two sessions share one.
On the command line, sesh connect wp behaves exactly like sesh connect wallpaper. In the picker, aliased sessions are tagged with a chip so they stay discoverable:
> wp wallpaper
dot dotfiles
my-project
Typing an alias exactly resolves to that session and nothing else, regardless of how fuzzy matching would have ranked it — the whole point being that the result never shifts:
filter: wp
> wp wallpaper
Anything short of an exact alias is fuzzy-matched as usual, so w and wpx behave like any other query. An alias resolves even when its session isn't in the current list — say you're running sesh picker --tmux and it isn't started yet.
With alias_auto_connect = true, typing the full alias connects immediately — no Enter. It is opt-in per session because it is only worth it for the handful you jump to constantly.
Aliases that share a prefix (w and wp) are allowed. [tui] alias_auto_connect_delay (default 150ms) is the grace period before auto-connect fires, which leaves room to finish typing the longer one. Raise it if you type slowly, or lower it to "0s" to fire the instant the alias is complete.
To keep typing past an alias in a one-off invocation, run the picker with --no-alias-auto.
Browsing aliases
Typing / as the first character narrows the picker to aliased sessions only, which is how you go from "I know I set up a shortcut for this" to the session without remembering the shortcut:
filter: /
> wp wallpaper
dot dotfiles
tc tmux config
What you type next narrows further, matching aliases by prefix (/t finds tc) and then falling back to session names (/config also finds tc, since aliases come first and names are matched anywhere). Aliases whose sessions aren't running still show up.
Completing an alias in this mode connects immediately, whether or not it set alias_auto_connect — reaching for / says the next thing you type is a shortcut to jump to. alias_auto_connect_delay still applies, so /w leaves room to become /wp, and --no-alias-auto still holds it back.
Only a leading / counts, so code/app filters normally. But a query that starts with a path — /Users/you/code — would enter alias mode instead, so pick a different sigil if you filter that way:
[tui]
alias_filter_prefix = "@" # or "" to turn the mode off
Path substitution
If you want to use the path of the selected session in your startup or preview command, you can use the {} placeholder.
This will be replaced with the session's path when the command is run.
An example of this in use is the following, where the tmuxinator default_project uses the path as key/value pair using ERB syntax:
[default_session]
startup_command = "tmuxinator start default_project path={}"
preview_command = "eza --all --git --icons --color=always {}"
Multiple windows
If you want your session to have multiple windows you can define windows in your configuration. You can then use these window layouts in your sessions. These windows can be reused as many times as you want and you can add as many windows to each session as you want.
Note: If you do not specify a path in the window, it will use the session's path.
[[session]]
name = "Downloads 📥"
path = "~/Downloads"
startup_command = "ls"
[[session]]
name = "tmux config"
path = "~/c/dotfiles/.config/tmux"
startup_command = "nvim tmux.conf"
preview_command = "bat --color=always ~/c/dotfiles/.config/tmux/tmux.conf"
windows = [ "git" ]
[[window]]
name = "git"
startup_script = "git pull"
Wildcard Configuration
Wildcard configs let you define session settings that apply to any directory matching a glob pattern, instead of creating a [[session]] entry for each project. This is useful when you have a directory full of projects that should all use the same startup command.
[[wildcard]]
pattern = "~/projects/*"
startup_command = "nvim"
[[wildcard]]
pattern = "~/work/*"
startup_command = "make dev"
preview_command = "ls -la"
[[wildcard]]
pattern = "~/repos/**"
startup_command = "git status"
When you run sesh connect ~/projects/myapp, sesh matches the path against your wildcard patterns and automatically creates a session named from the directory (using git remote or folder name), runs the configured startup command, and adds the path to zoxide.
Available fields:
| Field | Description |
|-------|-------------|
| pattern | Glob pattern to match directories (e.g. ~/projects/*) |
| startup_command | Command to run on session creation (supports {} for path) |
| preview_command | Command to run when previewing the session |
| disable_startup_command | Set to true to suppress the startup command |
| windows | Window layout to use (array of window names from [[window]] configs) |
| icon | Icon shown for matching sessions in the picker — see Custom icons |
Note: Patterns use Go's filepath.Match syntax which supports * (any sequence), ? (single character), and [...] (character classes). You can also use / at the end of a pattern for recursive matching -- ~/projects/ matches ~/projects/foo, `
... (README truncated for length)