Tmux scripts¶
Three scripts handle tmux session lifecycle: one for creating a fresh session per repo (or per
worktree), one for fuzzy-picking an existing session, and one for tearing down agent worktree
sessions and the worktrees behind them. The worktree create/remove work itself is delegated
to start-worktree / end-worktree, which are also wired in as
Claude Code's WorktreeCreate/WorktreeRemove hooks so
the naming convention stays consistent regardless of who created the worktree.
start-tmux-session¶
sts # alias for start-tmux-session
sts <query> # pre-fill the fzf query
sts . # operate on $PWD instead of $REPOS_DIR
sts <query> <worktree-name> # create/attach a session inside a per-worktree checkout
What it does:
- Walks
$REPOS_DIR(default$HOME/Repos) up to 4 levels deep looking for directories that contain a.git/entry. - Pipes the list into
fzffor selection (--select-1auto-picks if there's only one). - Sanitises the repo's basename to derive the bare-repo session name (see the shared sanitizer below).
- If a second
<worktree-name>argument is supplied, hands off tostart-worktree, which creates~/.cache/agent/worktrees/<repo>-<worktree>on branchagent/<repo>-<worktree>viagit worktree add(reusing the path or branch if either already exists), then prints the worktree path back. The tmux session is named after the worktree directory's basename (<repo>-<worktree>) so the Snacks sessions picker can nest it under the bare-repo parent by name prefix. -
If a session of that name doesn't exist, creates one. The
nvimwindow is created first; every other window is created detached and inserted with-a(immediately after thenvimwindow), so the windows end up in reverse creation order. The creation order isopencode→claude→zsh, which yields this layout:- Window 1 (
nvim) — nvim in the top pane (90%), shell in a small pane below (10%). - Window 2 (
zsh) — a plain login shell in the repo root. - Window 3 (
claude) — runsclaude(Claude Code) in the repo root when it is available onPATH. - Window 4 (
opencode) — runsopencodein the repo root when it is available onPATH.
The two agent windows are skipped entirely when their CLI isn't on
PATH, so the trailing indices shift down accordingly — butzshalways lands at window 2, immediately after the editor. - Window 1 (
-
Sets the terminal window/tab title to the session name via an
OSC 0escape (printf '\033]0;%s\007'), so the tab reads e.g.dotfilesinstead of the launching commandsts dotfiles. tmux leaves this alone becauseset-titlesis off. - Attaches to the session.
claude_bin=$(command -v claude 2> /dev/null || true)
opencode_bin=$(command -v opencode 2> /dev/null || true)
editor_pane=$(tmux -u new-session -d -P -F '#{pane_id}' -s "${name}" -n ' nvim' -c "${selected}" \
-x - -y - "${EDITOR}" .)
editor_window=$(tmux display-message -p -t "${editor_pane}" '#{window_id}')
tmux split-window -t "${editor_pane}" -v -l '10%' -c "${selected}"
tmux select-pane -t "${editor_pane}"
[ -n "${opencode_bin}" ] && tmux new-window -a -d -t "${editor_window}" -c "${selected}" \
-n ' opencode' "${opencode_bin}"
[ -n "${claude_bin}" ] && tmux new-window -a -d -t "${editor_window}" -c "${selected}" \
-n ' claude' "${claude_bin}"
tmux new-window -a -d -t "${editor_window}" -n ' zsh' -c "${selected}"
The shared sanitizer¶
Both start-tmux-session and start-worktree run names
through the same sanitize helper, so fix/stow symlinks becomes fix-stow-symlinks. It
collapses any character outside A-Za-z0-9_- to -, with special handling for .: tmux
3.5+ rejects . in session names (it's the session/window/pane separator), so dots are
encoded rather than dropped — a leading . becomes dot-, a trailing . becomes -dot,
and an interior . becomes -dot-. So next.js becomes next-dot-js and .config
becomes dot-config, keeping each name unique and tmux-safe.
attach-tmux-session¶
Simpler: lists tmux list-session -F '#S', fzf-picks, and either attaches (if running
outside tmux) or switches client (if inside).
The Snacks picker in nvim (++leader++ F S) does the same thing without leaving the editor — see neovim/plugins.
end-tmux-session¶
ets # alias for end-tmux-session — fzf multi-select over agent worktrees
ets <worktree-name>... # remove specific worktrees by name (or absolute path)
ets -f <worktree-name>... # skip the confirmation prompt when worktrees are dirty
What it does:
- Builds a selection list from positional args, or interactively via
fzf -mover~/.cache/agent/worktrees/*(tab to mark, enter to confirm). - Inspect pass — for each selection, prints status and flags concerns:
uncommitted— count of working-tree changes (git status --porcelain).unpushed— count of commits reachable fromHEADbut absent from any remote ref (git rev-list --count HEAD --not --remotes). This catches both "no upstream set" and "upstream set but ahead".
- If any selection had warnings and
--forcewasn't passed, prompts once before continuing. - Destroy pass — hands each selected path to
end-worktree, which:- Resolves the parent repo via
git rev-parse --git-common-dir. - Kills the matching tmux session (
<repo>-<worktree>— the worktree dir basename) if present. git worktree remove --force <path>from the parent repo.git branch -D <branch>if the branch is in theagent/*namespace (matches the convention used bystart-worktree).
- Resolves the parent repo via
Remote branches are never touched — push before removing if you want to keep the work. The matching PR (if any) keeps working off the remote branch even after the local one is gone.
agent-tmux-status¶
agent-tmux-status waiting # turn finished — your turn (calm)
agent-tmux-status attention # the agent needs you now — permission / notification (urgent)
agent-tmux-status clear # lower the indicator (also the default with no/unknown arg)
A no-op-safe leaf script shared by two coding agents so the indicator tracks whether either is waiting on you:
- Claude Code drives it through five
hooks —
Stopcalls it withwaiting,Notificationwithattention, andPostToolUse/UserPromptSubmit/SessionEndwithclear(PostToolUseclears the red after you approve a permission and the tool runs). - opencode drives it through the
agent-tmux-statusplugin —session.idle→waiting,permission.updated→attention, a new user message →clear.
It branches on $TMUX:
- Inside tmux — stores the state token (
waitingorattention) in a per-window user option on the pane the agent runs in (tmux set-window-option -t "$TMUX_PANE" @agent_status …, unset on clear). tmux swallowsOSCtitle sequences from inside a session (set-titlesis off), so the option is the reliable channel.theme.confmaps the token to a colour and glyph in thewindow-statusformat — calm peach●forwaiting, bold redforattention— and only that window changes, since user options resolve per-window. - Outside tmux — falls back to an
OSC 0terminal title written to/dev/tty(the same trickstart-tmux-sessionuses, since the agent may capture the caller's stdout), prefixing the cwd basename with●(waiting) or(attention) and dropping it on clear.
Every tmux/printf call is guarded with || true and the script never exits non-zero, so a
missing tmux server or detached tty can't fail an agent turn. The fallback glyphs live in the
waiting_glyph / attention_glyph lines at the top of the script; the in-tmux colours and
glyphs live in theme.conf.