A couple of years ago I wrote about copying text between tmux panes. At the time I was still figuring out how all the pieces of my terminal setup fit together (a recurring theme, honestly). Since then, tmux has become the first thing I open every morning. I wrote a small bash script that sets up my dev session, and I've tweaked it enough times that I thought it was worth sharing where it ended up.
Where I Started
My first version created a four-pane layout:
+------------------+------------------+
| nvim | rails server |
+------------------+------------------+
| lazygit | claude |
+------------------+------------------+
Neovim for editing, Rails server running bin/dev (which kicks off Overmind with the full Procfile.dev), lazygit for staging and committing, and Claude Code for when I need a second brain. It felt very productive to look at. Four panes! Everything visible! I am a real developer!
And then I actually used it for a while. The Rails server pane just... sat there, taking up screen real estate while I was writing code that didn't need a running server. Lazygit is great, but I reach for git status and git diff in a regular shell just as often. And four panes on a laptop screen? Everything is tiny.
Where I Landed
Two panes. A shell on the left, Claude Code on the right.
+------------------+------------------+
| shell | claude |
+------------------+------------------+
The shell handles whatever I need in the moment: editing, git, spinning up the server, running tests. Turns out I don't need a dedicated pane for each tool. I just need a place to work and a place to think out loud.
The Script
The full script is in my dotfiles, but here's what it does:
#!/bin/bash
# Universal Development Session Script
# Creates a 2-pane tmux layout for any Rails project or worktree
# Usage: ./dev-session [session-name] [directory]
# Configuration
DEFAULT_EDITOR="nvim"
DEFAULT_PORT="3000"
get_current_dir() {
pwd
}
get_project_name() {
basename "$(get_current_dir)"
}
get_branch_name() {
if git rev-parse --git-dir > /dev/null 2>&1; then
git branch --show-current 2>/dev/null || echo "main"
else
echo "no-git"
fi
}
generate_session_name() {
local project_name=$(get_project_name)
local branch_name=$(get_branch_name)
if [[ "$branch_name" != "main" && "$branch_name" != "develop" && "$branch_name" != "no-git" ]]; then
safe_branch=$(echo "$branch_name" | sed 's/[\/:]/-/g')
echo "${project_name}-${safe_branch}"
else
echo "$project_name"
fi
}
SESSION_NAME="${1:-$(generate_session_name)}"
WORK_DIR="${2:-$(get_current_dir)}"
if [[ ! -d "$WORK_DIR" ]]; then
echo "Error: Directory $WORK_DIR does not exist"
exit 1
fi
# Reattach if session already exists
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "Session '$SESSION_NAME' already exists. Attaching..."
tmux attach-session -t "$SESSION_NAME"
exit 0
fi
# Create session and split into two panes
tmux new-session -d -s "$SESSION_NAME" -c "$WORK_DIR"
tmux split-window -h -c "$WORK_DIR"
# Left pane: shell, right pane: Claude Code
tmux send-keys -t 1 "# Ready for commands (server, etc.)" C-m
tmux send-keys -t 2 "claude" C-m
tmux select-pane -t 1
tmux rename-window "dev"
tmux attach-session -t "$SESSION_NAME"
I alias it in my shell config so I can run dev from any project directory:
alias dev="~/Documents/Repos/dev-scripts/dev-session"
The Parts I Actually Like
It names sessions from your git branch. If I'm on main, the session is just the project name (qualify). On a feature branch, it tacks on the branch name (qualify-fix-event-registry). Slashes in branch names get swapped to hyphens so tmux doesn't choke on them.
This is my favorite part, because I usually have multiple sessions going at once, one per feature branch or worktree. tmux list-sessions gives me a quick snapshot of what I've got cooking:
qualify: 1 windows (created Thu Apr 3 09:15:00 2026)
qualify-add-snapshot-tests: 1 windows (created Thu Apr 3 10:30:00 2026)
rubygems: 1 windows (created Wed Apr 2 14:00:00 2026)
It won't create duplicates. If a session with that name already exists, the script just attaches to it. Detach with Ctrl-b d, go make coffee, come back, run dev again, and you're right where you left off. Sessions survive sleep/wake cycles too, which still feels like magic to me.
It works anywhere. The script doesn't assume Rails or even git. I use it for this blog, for Ruby gems, for random one-off scripts. If there's a git repo, it uses the branch name. If not, you just get the directory name. No fuss.
The Claude Code Pane
The right pane launches Claude Code automatically. I like having it right there instead of in a separate terminal window. I can glance over at its output while I'm working, and Ctrl-b z (tmux zoom) is my best friend here, blowing the Claude pane up to full screen when I'm reading a longer response and then popping back to the split.
The other nice thing is that Claude Code's context stays warm. I can ask it something, go write code in the left pane for twenty minutes, come back and ask a follow-up without re-explaining everything. That alone is worth the dedicated pane.
Things I'd Change
My .tmux.conf is still embarrassingly minimal. It's literally just a scrollback buffer increase. The README in my dotfiles repo documents vim-style pane navigation bindings that I haven't actually configured yet. I will get to it. Eventually. (I tell myself this every week.)
The four-pane version (dev-session-fixed) is still sitting in the repo too. I keep it around for when I'm debugging something where I really do need server logs visible the whole time. But I haven't reached for it in weeks, which tells me something.
Try It
Grab the script from my dotfiles, drop it somewhere in your path, and alias it. The only real dependency is tmux itself. If you don't have Claude Code installed, no worries, that pane just becomes a regular shell.
cd your-project
dev
That's it. Two panes, named after your branch, ready to go.
Top comments (0)