.tmux-session files: let each project own its terminal layout
2026-02-19 · #tmux #bash #dotfiles #workflow
I have 63 projects on my machine and many of them need separate terminal layouts.
Some need four windows; an editor, an AI assistant, a shell and a server. Some only need two. Sometimes I need a session that detects git worktrees and creates separate sessions per branch. However, they all start the same by running a script and a session appears.
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" That script will give you two windows with the editor opening nvim automatically. The last line in the script attaches your terminal to the session.
Make this script executable and run it and it will create a named session in your project directory and if it already exists it will re-attach without creating duplicate sessions.
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, it supports 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, without a gem to install or a 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 bars are a good starting point. 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-starting services works too. send-keys can kick off dev servers, docker compose, file watchers. Anything you'd type manually after opening the session.
Environment loading is another one. 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 is useful when you have multiple worktrees open. Current branch, last commit, dirty/clean state.
And 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.