
tsm is a terminal session manager. It keeps shell sessions alive as background daemons, lets you detach and reattach later, and restores terminal state without turning the project into a tmux clone.
It is built around a simple model:
tsm mux (cmux, kitty, Ghostty, and WezTerm backends)tsm wtFor full-screen terminal apps like Neovim, the preferred build uses Ghostty's VT engine so reattach can restore the visible screen instead of only restoring terminal modes.
Full TUI:

Simplified session palette:

Compact help layout:

tsm is for people who want persistent terminal sessions without adopting tmux's window and pane model.
Use it when you want:
For splits and workspaces, use tsm mux to orchestrate your terminal's native split system:
tsm mux open dev opens a workspace with splits and sessions from a TOML manifesttsm mux)tsm mux lets you define workspaces as TOML manifests and open them with native terminal splits. Unlike tmux, it delegates layout to your terminal emulator's own split system — no server wrapping the terminal.
Create ~/.config/tsm/workspaces/dev.toml:
name = "dev"
version = 1
[[surface]]
name = "editor"
session = "editor"
cwd = "~/Developer/myproject"
command = "nvim ."
[[surface.split]]
name = "shell"
session = "shell"
direction = "right"
cwd = "~/Developer/myproject"
tsm mux open dev
This creates the sessions, opens native splits in your terminal, attaches each session, and runs the startup commands.
tsm mux open <workspace> Open workspace from manifest
tsm mux split <left|right|up|down> <s> Split focused pane with session
tsm mux tab new <session> New tab with session
tsm mux save <workspace> Save workspace manifest
tsm mux restore <workspace> Restore workspace from manifest
tsm mux doctor <workspace> Diagnose workspace health
tsm mux sidebar sync <workspace> Sync agent state to cmux sidebar
tsm mux last Focus previous pane
tsm mux next Focus next pane
tsm mux workspace [name] List or switch workspaces
tsm mux setup kitty Configure kitty for remote control
tsm mux status Show terminal, backend, workspace info
| Terminal | Backend | Detection | Splits | Tabs | Sidebar |
|---|---|---|---|---|---|
| cmux | cmux | CMUX_SOCKET_PATH |
yes | yes | yes |
| kitty | kitty | KITTY_PID |
yes | yes | no |
| Ghostty | ghostty | GHOSTTY_RESOURCES_DIR |
yes | yes | no |
| WezTerm | wezterm | WEZTERM_UNIX_SOCKET |
yes | yes | no |
Override with TSM_MUX_BACKEND=cmux (or kitty, ghostty, wezterm).
Press w in the TUI to see available workspaces and open one with Enter.
tsm wt)tsm wt manages git worktrees and their sessions together. It auto-detects worktrees, creates sessions with repo@branch naming, and lets you open full workspace layouts with splits.
When you create a session inside a linked git worktree, tsm automatically names it repo@branch:
cd ~/Developer/myapp-feature-login
tsm attach
# → creates session "myapp@feature-login"
Regular (non-worktree) repos keep using the directory name. When a session is created in a repo with worktrees, tsm auto-creates sessions for all sibling worktrees.
The TUI groups sessions by git repository with collapsible headers:
▾ myapp (3)
▸ myapp@main ●1 2h
myapp@feature-login ○0 30m
myapp@bugfix-42 ○0 5m
nvim ○0 10m
/) matches branch and repo namesCtrl+] opens the session picker from inside any app (neovim, claude, etc.)tsm wt List worktrees and session status
tsm wt <branch> Attach to (or create) session for a branch
tsm wt open [--split <cmd>...] Open worktrees as workspace with splits
tsm wt tui Open TUI filtered to this repo's sessions
tsm wt add <branch...> Create new git worktrees + sessions
tsm wt rm <branch...> [-f] Remove worktrees and kill sessions
tsm wt move <branch> <path> Move a worktree to a new path
tsm wt prune Prune stale worktrees and orphaned sessions
tsm wt --create Create sessions for all existing worktrees
Open all worktrees as tabs with splits for your tools:
# Each worktree gets a tab with claude + nvim
tsm wt open --split "claude" --split "nvim"
# Only specific branches
tsm wt open main feature-login --split "claude" --split "nvim"
The first --split command runs in the main pane, each additional one creates a right-split. Requires a mux backend (cmux, kitty, ghostty, or wezterm).
# Create worktrees and sessions in one step
tsm wt add feature-login bugfix-42 hotfix-99
# Remove worktrees and kill their sessions
tsm wt rm feature-login bugfix-42
# Force remove (uncommitted changes)
tsm wt rm hotfix-99 -f
# Clean up stale worktrees and orphaned sessions
tsm wt prune
There are two supported install tracks.
| Install path | Backend | Best for |
|---|---|---|
| Release archive | libghostty-vt |
Best restore quality, recommended for most users |
| Homebrew | libghostty-vt |
Managed installs and upgrades |
This is the recommended install path.
Release archives are self-contained. They bundle:
tsmlibghostty-vtUsers do not need to install Ghostty separately.
Supported bundled release targets:
amd64arm64amd64arm64See docs/COMPATIBILITY.md for the current OS, shell, terminal, and agent-support contract. See docs/KNOWN_LIMITATIONS.md for the current product boundaries and caveats.
Download the matching archive from GitHub Releases, extract it, and place tsm on your PATH.
tsm is distributed through a custom tap, not homebrew/core, so it will not show up in a plain brew search tsm.
Install it with the tap-qualified formula name:
brew tap adibhanna/tsm
brew install adibhanna/tsm/tsm
Remove it with:
brew uninstall adibhanna/tsm/tsm
The supported Homebrew path is the self-contained release archive formula published by the release workflow. A source-backed formula is brittle because Ghostty's Zig build fetches external dependencies during the build, which Homebrew may block in its sandbox. Until tagged release archives are published, prefer the source install.
Prerequisites:
git clone https://github.com/adibhanna/tsm.git
cd tsm
make setup
make build
make setup verifies prerequisites, clones Ghostty into ./ghostty, and builds libghostty-vt into ./.ghostty-prefix. After that, make build, make test, and make lint all work.
Install under a user prefix:
make install
That installs:
tsm into ~/.local/binlibghostty-vt into ~/.local/lib/tsmRemove it cleanly:
make uninstall
Override the install root if needed:
make install PREFIX=/opt/homebrew
make uninstall PREFIX=/opt/homebrew
System-wide install:
sudo make install PREFIX=/usr/local
sudo make uninstall PREFIX=/usr/local
Create or attach a session:
tsm attach
That command is intentionally smart:
Create a specific session and start a command inside it:
tsm new api bash -lc 'npm run dev'
Open Neovim in a session:
tsm attach editor
nvim
Detach interactively:
Ctrl+\
Reattach later:
tsm attach editor
List sessions:
tsm ls
Run diagnostics:
tsm doctor
Each session is a long-lived daemon with:
zsh, bash, fish, nvim, or a custom commandThe session keeps running after you detach. You only lose it if you explicitly kill it or the process exits on its own.
Use this if you prefer to stay in the shell.
tsm
tsm tui [--simplified] [--keymap default|palette]
tsm palette
tsm attach [name]
tsm detach [name]
tsm new <name> [cmd...]
tsm ls
tsm wt
tsm wt <branch>
tsm wt open [--split <cmd>...]
tsm wt add <branch...>
tsm wt rm <branch...> [-f]
tsm wt move <branch> <path>
tsm wt prune
tsm wt tui
tsm mux open <workspace>
tsm mux split <dir> <session>
tsm mux tab new <session>
tsm mux save <workspace>
tsm mux restore <workspace>
tsm mux doctor <workspace>
tsm mux status
tsm doctor
tsm doctor clean-stale
tsm debug session <name>
tsm rename <old> <new>
tsm kill [name...]
tsm claude-statusline
tsm config install [--force]
tsm version
tsm attach with no name:
tsm attach <name>:
tsm build, warn so you know to recreate the session if behavior looks staleAnything after the session name becomes the command started inside the session instead of your default login shell.
Examples:
tsm new work
tsm new logs tail -f /var/log/system.log
tsm new editor nvim
tsm new api bash -lc 'npm run dev'
tsm detach with no name uses $TSM_SESSION, so it detaches the current session when run inside an attached shell.
tsm detach <name> detaches all attached clients from that named session without killing the daemon.
tsm kill with no name uses $TSM_SESSION, so it kills the current session when run inside an attached shell.
tsm kill <name>... kills one or more named sessions.
Examples:
tsm detach
tsm detach work
tsm kill
tsm kill api worker repl
Use tsm doctor when install, runtime-linking, or socket issues are unclear.
It reports:
pkg-config and libghostty-vt availabilityIf tsm doctor reports stale sockets or orphaned sidecars, clean them up with:
tsm doctor clean-stale
Use tsm debug session <name> when one specific session is acting strangely.
It reports:
tsm and tsm tui open the full TUI.
The full TUI is the best workflow when you want:
| Key | Action |
|---|---|
↑ ↓ |
Navigate sessions (including group headers) |
← |
Collapse repo group / scroll preview |
→ |
Expand repo group / scroll preview |
enter |
Attach (or toggle collapse on group header) |
space |
Toggle selection |
ctrl+a |
Select or deselect all |
d |
Detach selected session(s) |
n |
New session |
k |
Kill selected session(s) |
r |
Rename session |
c |
Copy attach command |