Skip to content

Instantly share code, notes, and snippets.

@ericclemmons
Last active June 25, 2026 03:32
Show Gist options
  • Select an option

  • Save ericclemmons/5b6cad84d3c7c496a1dfba3ca7214dda to your computer and use it in GitHub Desktop.

Select an option

Save ericclemmons/5b6cad84d3c7c496a1dfba3ca7214dda to your computer and use it in GitHub Desktop.
durable: tmux-backed local persistent sessions

durable

A tiny tmux wrapper for local, durable terminal sessions.

It creates or resumes a tmux session based on the current folder or git worktree, git branch when available, and the command role. The goal is to make local shells and long-running tools feel more like SSH sessions: close the terminal tab, reopen later, and resume the same process.

New sessions are named:

durable_<folder>_<branch>_<role>_<YYYYMMDD-HHMMSS>

Install

mkdir -p ~/.local/bin
cp durable ~/.local/bin/durable
chmod +x ~/.local/bin/durable
cp tmux.conf ~/.tmux.conf

Make sure ~/.local/bin is on your PATH.

Zsh Completion

mkdir -p ~/.zsh/completions
cp _durable ~/.zsh/completions/_durable

Make sure your .zshrc has:

if [[ ":$FPATH:" != *":$HOME/.zsh/completions:"* ]]; then
  export FPATH="$HOME/.zsh/completions:$FPATH"
fi

autoload -Uz compinit
compinit

Restart your shell, or run:

autoload -Uz compinit && compinit

Completion order for durable <TAB>:

  1. Durable sessions for the current folder/branch.
  2. Other durable sessions.
  3. Normal command completion.

Usage

Durable shell for the current context:

durable

Durable command for the current context:

durable opencode
durable opencode -c

Rejoin an exact session:

durable durable_Projects_nogit_opencode_20260624-222814

List sessions:

durable --list

Show the computed session name:

durable --name
durable --name opencode

Kill the durable session for the current context:

durable --kill
durable --kill opencode

Inside tmux

Detach and leave the process running:

Ctrl-b then d

Exit the shell/session:

exit

Ctrl-C still goes to the foreground process, such as opencode.

#compdef durable
_durable() {
# Make `durable <TAB>` prefer existing durable sessions, then behave like
# normal shell completion. `durable opencode <TAB>` behaves like
# `opencode <TAB>`.
local -a original_words
local original_current
original_words=("${words[@]}")
original_current="$CURRENT"
if (( CURRENT > 2 )) && [[ "$words[2]" == "--kill" || "$words[2]" == "kill" ]]; then
local -a durable_sessions
durable_sessions=("${(@f)$(tmux list-sessions -F '#S' 2>/dev/null | awk '$0 ~ /^durable_/')}")
_describe -t durable-sessions 'durable session' durable_sessions
return
fi
if (( CURRENT == 2 )) && [[ "$PREFIX" == -* ]]; then
_values 'durable option' \
'--help[show help]' \
'--list[list tmux sessions]' \
'--name[print computed session name]' \
'--kill[kill durable session for current context]'
return
fi
if (( CURRENT == 2 )); then
local dir branch folder local_prefix
local -a local_sessions other_sessions
dir="$(git rev-parse --show-toplevel 2>/dev/null || pwd -P)"
branch="$(git -C "$dir" branch --show-current 2>/dev/null || true)"
if [[ -z "$branch" ]]; then
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
branch="$(git -C "$dir" rev-parse --short HEAD 2>/dev/null || true)"
branch="${branch:-detached}"
else
branch="nogit"
fi
fi
folder="$(basename "$dir")"
folder="${folder//[^A-Za-z0-9_.-]/_}"
branch="${branch//[^A-Za-z0-9_.-]/_}"
local_prefix="durable_${folder}_${branch}_"
local_sessions=("${(@f)$(tmux list-sessions -F '#S' 2>/dev/null | awk -v prefix="$local_prefix" 'index($0, prefix) == 1')}")
other_sessions=("${(@f)$(tmux list-sessions -F '#S' 2>/dev/null | awk -v prefix="$local_prefix" '$0 ~ /^durable_/ && index($0, prefix) != 1')}")
_describe -t local-durable-sessions 'local durable session' local_sessions
_describe -t durable-sessions 'durable session' other_sessions
fi
words=("${words[@]:1}")
(( CURRENT-- ))
_normal
words=("${original_words[@]}")
CURRENT="$original_current"
}
_durable "$@"
#!/usr/bin/env zsh
set -euo pipefail
usage() {
cat <<'EOF'
durable: tmux-backed durable sessions
Usage:
durable Enter a durable shell for the current context
durable <command...> Enter a durable session for that command
durable <session-name> Rejoin an existing durable tmux session
durable --name Print computed session name
durable --list List tmux sessions
durable --kill Kill the durable session for the current context
durable --kill <role> Kill the durable session for a role in current context
Examples:
durable
durable opencode
durable opencode -c
durable --kill opencode
Detach without killing the session:
Ctrl-b then d
Exit/finish the session:
exit
EOF
}
die() {
print -u2 -- "durable: $*"
exit 1
}
need() {
command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"
}
sanitize() {
local value="$1"
value="${value//[^A-Za-z0-9_.-]/_}"
value="${value##[_-.]}"
value="${value%%[_-.]}"
print -r -- "${value:-x}"
}
display_session_name() {
local durable_role="$1"
local dir branch folder
dir="$(context_dir)"
branch="$(context_branch "$dir")"
folder="$(basename "$dir")"
if [[ "$branch" == "nogit" ]]; then
print -r -- "$(sanitize "$folder") · $(sanitize "$durable_role")"
else
print -r -- "$(sanitize "$folder") · $(sanitize "$branch") · $(sanitize "$durable_role")"
fi
}
context_dir() {
git rev-parse --show-toplevel 2>/dev/null || pwd -P
}
context_branch() {
local dir="$1"
local branch
branch="$(git -C "$dir" branch --show-current 2>/dev/null || true)"
if [[ -n "$branch" ]]; then
print -r -- "$branch"
return
fi
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
branch="$(git -C "$dir" rev-parse --short HEAD 2>/dev/null || true)"
print -r -- "${branch:-detached}"
else
print -r -- "nogit"
fi
}
session_prefix() {
local durable_role="$1"
local dir branch folder
dir="$(context_dir)"
branch="$(context_branch "$dir")"
folder="$(basename "$dir")"
print -r -- "durable_$(sanitize "$folder")_$(sanitize "$branch")_$(sanitize "$durable_role")"
}
timestamp_suffix() {
date '+%Y%m%d-%H%M%S'
}
session_name() {
local durable_role="$1"
print -r -- "$(session_prefix "$durable_role")_$(timestamp_suffix)"
}
durable_sessions() {
tmux list-sessions -F '#{session_created} #{session_name}' 2>/dev/null | awk '$2 ~ /^durable_/ { print $0 }'
}
matching_sessions_for_prefix() {
local durable_prefix="$1"
durable_sessions | awk -v prefix="$durable_prefix" 'index($2, prefix "_") == 1 { print $0 }' | sort -rn | awk '{ print $2 }'
}
existing_session_for() {
local durable_role="$1"
local dir="$2"
local durable_prefix candidate fallback context
durable_prefix="$(session_prefix "$durable_role")"
fallback=""
while IFS= read -r candidate; do
[[ -n "$candidate" ]] || continue
if [[ -z "$fallback" ]]; then
fallback="$candidate"
fi
context="$(tmux show-option -qv -t "$candidate" @durable_context_dir 2>/dev/null || true)"
if [[ "$context" == "$dir" ]]; then
print -r -- "$candidate"
return 0
fi
done < <(matching_sessions_for_prefix "$durable_prefix")
if [[ -n "$fallback" ]]; then
print -r -- "$fallback"
fi
}
role_for_args() {
if [[ "$#" -eq 0 ]]; then
print -r -- "shell"
else
print -r -- "$1"
fi
}
startup_command_for_args() {
if [[ "$#" -eq 0 ]]; then
return 0
else
local quoted
quoted="${(j: :)${(q)@}}"
print -r -- "$quoted"
fi
}
attach_or_switch() {
local durable_session_name="$1"
if [[ -n "${TMUX:-}" ]]; then
tmux switch-client -t "$durable_session_name"
else
tmux attach-session -t "$durable_session_name"
fi
}
main() {
need tmux
case "${1:-}" in
-h|--help)
usage
return 0
;;
--list|list|ls)
tmux list-sessions
return 0
;;
--name|name)
shift
local name_role name_existing
name_role="$(role_for_args "$@")"
name_existing="$(existing_session_for "$name_role" "$(context_dir)")"
print -r -- "${name_existing:-$(session_name "$name_role")}"
return 0
;;
--kill|kill)
shift
local kill_role kill_session
if [[ "${1:-}" == durable_* ]]; then
kill_session="$1"
else
kill_role="$(role_for_args "$@")"
kill_session="$(existing_session_for "$kill_role" "$(context_dir)")"
fi
[[ -n "$kill_session" ]] || die "no matching durable session"
tmux kill-session -t "$kill_session"
return 0
;;
esac
local dir durable_role durable_session_name startup_command
dir="$(context_dir)"
if [[ "${1:-}" == durable_* ]] && tmux has-session -t "$1" 2>/dev/null; then
attach_or_switch "$1"
return 0
fi
if [[ "${1:-}" == durable_* ]]; then
die "no such durable session: $1"
fi
durable_role="$(role_for_args "$@")"
durable_session_name="$(existing_session_for "$durable_role" "$dir")"
if [[ -z "$durable_session_name" ]]; then
durable_session_name="$(session_name "$durable_role")"
fi
startup_command="$(startup_command_for_args "$@")"
if ! tmux has-session -t "$durable_session_name" 2>/dev/null; then
local shell_path tmux_command display_name window_name startup_script
shell_path="${SHELL:-/bin/zsh}"
if [[ -n "$startup_command" ]]; then
startup_script="${startup_command}"$'\n''exec "${SHELL:-/bin/zsh}" -l'
tmux_command="${(q)shell_path} -lc ${(q)startup_script}"
else
tmux_command="${(q)shell_path} -l"
fi
display_name="$(display_session_name "$durable_role")"
window_name="$(sanitize "$durable_role")"
tmux new-session -d -s "$durable_session_name" -n "$window_name" -c "$dir" "$tmux_command"
tmux set-option -t "$durable_session_name" @durable_display_name "$display_name"
tmux set-option -t "$durable_session_name" @durable_context_dir "$dir"
fi
attach_or_switch "$durable_session_name"
}
main "$@"
set -g mouse on
# Quiet status bar for durable sessions.
set -g status on
set -g status-position bottom
set -g status-interval 5
set -g status-style "bg=#111318,fg=#7d8590"
set -g status-left-length 80
set -g status-right-length 40
set -g status-left "#[fg=#d0d7de,bold] #{E:@durable_display_name} #[fg=#5c6370]#S"
set -g status-right "#[fg=#5c6370]%H:%M"
set -g window-status-format " #[fg=#7d8590]#I:#W "
set -g window-status-current-format " #[fg=#d0d7de,bold]#W "
set -g window-status-separator ""
set -g set-titles on
set -g set-titles-string "#{?pane_title,#{pane_title},#{E:@durable_display_name}}"
set -g pane-border-style "fg=#30363d"
set -g pane-active-border-style "fg=#58a6ff"
set -g message-style "bg=#1f242c,fg=#d0d7de"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment