.tmux-session files: let each project own its terminal layout

2026-02-19 · #tmux #bash #dotfiles #workflow

I have 63 projects on my machine. Each one needs a different terminal layout.

Some need four windows: editor, AI assistant, shell, server. Some need two. One detects git worktrees and creates separate sessions per branch. They all start the same way, though. I run a script, and the session appears, right?

The trick is a file called .tmux-session.

One file, one idea

Drop a .tmux-session file in any project root. It's a bash script that creates your tmux session.

#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SESSION="${TMUX_SESSION_NAME:-$(basename "$SCRIPT_DIR")}"

if ! tmux has-session -t "=$SESSION" 2>/dev/null; then
    tmux new-session -d -s "$SESSION" -c "$SCRIPT_DIR" -n editor
    tmux new-window -t "$SESSION" -n shell -c "$SCRIPT_DIR"
    tmux send-keys -t "$SESSION:editor" 'nvim .' C-m
fi

tmux attach-session -t "=$SESSION"

Two windows. Editor opens nvim automatically. The last line attaches you to the session.

Make it executable (chmod +x .tmux-session), run it (./.tmux-session), and you get a named session rooted in the project directory. If the session already exists, it skips creation and reattaches. No duplicates.

Every project is different

The minimal example above is a starting point. Real projects get more specific.

Here's one of mine that sets up a custom status bar and opens four windows:

#!/usr/bin/env bash
SESSION="${TMUX_SESSION_NAME:-my-project}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

if ! tmux has-session -t "=$SESSION" 2>/dev/null; then
    tmux new-session -d -s "$SESSION" -c "$SCRIPT_DIR" -n nvim
    tmux new-window -t "$SESSION" -n claude -c "$SCRIPT_DIR"
    tmux new-window -t "$SESSION" -n shell -c "$SCRIPT_DIR"
    tmux new-window -t "$SESSION" -n server -c "$SCRIPT_DIR"

    tmux send-keys -t "$SESSION:nvim" 'nvim .' C-m

    tmux set-option -t "$SESSION" status-right-length 60
    tmux set-option -t "$SESSION" status-right '#(~/.local/bin/claude-usage) | %H:%M %d-%b'
fi

tmux attach-session -t "=$SESSION"

And here's one that gets interesting. My DeckEngine project uses git worktrees, so the session script detects which branch directory it's running from and names the session accordingly:

#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKTREE_NAME="$(basename "$SCRIPT_DIR")"

case "$WORKTREE_NAME" in
    builder|planner|release)
        SESSION="deckengine-${WORKTREE_NAME}"
        ;;
    *)
        SESSION="${TMUX_SESSION_NAME:-deckengine}"
        ;;
esac

if ! tmux has-session -t "=$SESSION" 2>/dev/null; then
    tmux new-session -d -s "$SESSION" -c "$SCRIPT_DIR" -n nvim
    # ... windows, status bar with sprint + CI status
fi

Three worktrees, three separate tmux sessions, all from the same .tmux-session file. Because it's just bash, you can add whatever logic the project needs.

Why not tmuxinator, tmuxp, or the sessionizer?

The idea of scripting per-project tmux sessions came from ThePrimeagen's sessionizer. His approach is one central script: it searches a list of directories with fzf, creates a generic session named after the project, and switches you there. It's fast and elegant for jumping between projects.

My version went a different direction. Instead of one smart script that creates generic sessions, each project has its own .tmux-session file that defines exactly what that project needs. Centralised discovery versus decentralised, project-specific creation. Different trade-off.

Tmuxinator and tmuxp are closer to my approach in that they support per-project configs, but they default to centralised configs (~/.config/tmuxinator/) and add a dependency. Ruby for tmuxinator, Python for tmuxp.

The .tmux-session pattern keeps the file in the project repo, version-controlled alongside the code. Clone the repo, run the script, get the session. No gem to install, no YAML schema to learn. It runs anywhere tmux runs.

I work across WSL, remote servers, and fresh VMs. A bash script needs nothing beyond tmux itself. That portability matters more to me than YAML convenience.

My rule is simple: if bash can do something simply, I do it in bash. A session script runs once when you start working. It creates a few windows and maybe sends a keystroke. That's not a job that needs a framework.

Every dependency you add is a version to track, a package to install on every new machine, and an upstream change that might break your workflow. Save the dependencies for things bash genuinely can't do well, like fuzzy finding across 63 projects. That's where fzf earns its place.

What it doesn't do

It doesn't manage sessions for you. There's no "list all projects" or "switch between sessions" or "kill a session gracefully." You run the script manually, and you kill the session with tmux kill-session -t my-project. For one or two projects that's fine. For 63 it gets tedious.

It also doesn't handle the "am I already inside tmux?" problem. If you run the script from within tmux, tmux attach will try to nest. You need a small helper to detect that and use tmux switch-client instead. I use a 10-line script called tmux-join for this; it's in the gist.

And the scripts are bash. If you're a fish or zsh user they still run fine (they have shebangs), but you'll want to adapt the TMUX_SESSION_NAME variable handling for your shell.

Getting started

Create a .tmux-session file in any project root, make it executable with chmod +x .tmux-session, and run it with ./.tmux-session. Start with the two-window starter and add windows as you need them. The pattern scales from a quick docs project to a multi-worktree monorepo.

Ideas to try

Once you have the basic session working, the script is yours to extend. Some things I've added to different projects:

  • Per-project status bar. My DeckEngine session shows CI pipeline status and current sprint. Another shows Claude API quota. Use #(...) in status-right to run any shell command.
  • Auto-start services. send-keys can kick off dev servers, docker compose, file watchers. Anything you'd type manually after opening the session.
  • Environment loading. Source a .env, activate a virtualenv, or nvm use the right Node version. The script runs once, so setup cost doesn't matter.
  • Git context in the status bar. Current branch, last commit, dirty/clean state. Useful when you have multiple worktrees open.
  • Project-specific key bindings. Bind a key to run tests, restart a server, or tail a log. Different projects, different shortcuts.

The session script is just bash. If you can type it in a terminal, you can automate it here.

This file creates the session. Next I'll cover workon, the function that finds these files across your projects, manages sessions, and adds fzf fuzzy search. But the .tmux-session file is the foundation. Start there.


Part of my series on building reproducible development environments. Previously: GNU Stow for dotfiles and modular .bashrc. Next: workon/workoff, the workflow that manages .tmux-session files across 63 projects.