Introduction
"TL;DR
- Manage multiple Claude Code instances with dedicated tmux sessions
- Real-time status updates show current tool usage in session names
- Quick navigation between sessions with fzf picker and keyboard shortcuts
- Persistent sessions that survive Claude exits
When working on complex projects, I often find myself running multiple Claude Code sessions simultaneously - one for the frontend, another for the backend, maybe a third for documentation. Managing these sessions became chaotic until I built this tmux integration.
This article walks through a complete setup for managing Claude Code sessions with tmux, featuring real-time status indicators and seamless navigation.
What This Integration Provides
- Dedicated tmux sessions for each Claude Code instance
- Real-time status updates showing current tool usage in the session name
- Quick navigation between multiple Claude sessions
- Persistent sessions that survive Claude exits
Architecture
The integration uses Claude Code's hook system to update tmux session names in real-time:
Hook Event Flow
When you interact with Claude, the hooks fire in sequence to update the session name:
Session Status States
The session name dynamically updates to show Claude's current state:
| Status | Display | Meaning |
|---|---|---|
| Thinking | [Thinking~] | Claude is processing your prompt |
| Tool Use | [Read], [Edit], [Bash] | Currently executing a tool |
| Idle | [Idle> Read] | Waiting for input, last tool was Read |
| Idle (chat) | [Idle> Chatted] | Waiting for input, no tools were used |
| Done | [Done> Edit] | Claude session exited |
Tmux Keybindings
Add these to your ~/.tmux.conf:
# General sessionizer
bind-key -r f run-shell "tmux neww tmux-sessionizer.sh"
# Claude Code session management
bind-key C-a run-shell "claude-session.sh" # Create new session
bind-key a run-shell "tmux neww claude-session-switch.sh" # fzf picker
bind-key l run-shell "claude-session-last.sh" # Switch to last
bind-key i run-shell "claude-session-cycle.sh" # Cycle idle sessions
| Key | Action |
|---|---|
<prefix> f | Open tmux-sessionizer (project picker) |
<prefix> C-a | Spawn new Claude session in current directory |
<prefix> a | Open fzf picker to switch between Claude sessions |
<prefix> l | Switch to most recently used Claude session |
<prefix> i | Cycle through idle Claude sessions |
Workflow Overview
Typical Session Flow
File Structure
dotenv/
├── claude/
│ └── settings.json # Hook configuration
└── local/bin/
├── tmux-sessionizer.sh # General project sessionizer
├── claude-session.sh # Create new Claude session
├── claude-session-switch.sh # fzf session picker
├── claude-session-last.sh # Switch to last session
├── claude-session-cycle.sh # Cycle through idle sessions
├── claude-tmux-status.sh # Update session status
└── claude-tmux-pretool.sh # PreToolUse hook handler
Scripts
tmux-sessionizer.sh
The foundational project sessionizer - quickly jump to any project directory. Inspired by ThePrimeagen's workflow.
#!/usr/bin/env bash
if [[ $# -eq 1 ]]; then
selected=$1
else
selected=$(find ~/dev ~/python ~/rust ~/dotenv ~/Documents ~/work -mindepth 1 -maxdepth 1 -type d | fzf)
fi
if [[ -z $selected ]]; then
exit 0
fi
selected_name=$(basename "$selected" | tr . _)
tmux_running=$(pgrep tmux)
if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then
tmux new-session -s $selected_name -c $selected
exit 0
fi
if ! tmux has-session -t=$selected_name 2>/dev/null; then
tmux new-session -ds $selected_name -c $selected
fi
tmux switch-client -t $selected_name
Customize the find paths to match your directory structure.
claude-session.sh
Creates a new Claude Code session named after the current directory.
#!/usr/bin/env bash
# Spawn a new Claude Code session in the current directory
# Get current pane's working directory
current_path=$(tmux display-message -p "#{pane_current_path}")
dir_name=$(basename "$current_path")
# Sanitize directory name: replace dots and special chars with underscores
safe_name=$(echo "$dir_name" | tr './:' '_')
# Generate base session name
base_name="Claude-$safe_name"
# Helper to check if session exists (with any status suffix)
session_exists() {
local name="$1"
local escaped_name=$(printf '%s' "$name" | sed 's/[.[\*^$()+?{|]/\\&/g')
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep -qE "^${escaped_name} \["
}
# Find next available session number
counter=1
session_name="$base_name"
while session_exists "$session_name"; do
counter=$((counter + 1))
session_name="${base_name}-${counter}"
done
# Final session name with status
full_name="$session_name [Idle]"
# Create new detached session
tmux new-session -ds "$full_name" -c "$current_path" \
"claude --dangerously-skip-permissions; claude-tmux-status.sh done; exec $SHELL"
# Switch to the new session
tmux switch-client -t "=$full_name"
claude-session-switch.sh
Interactive fzf picker with session preview, sorted by status.
#!/usr/bin/env bash
# Switch between Claude sessions using fzf with preview
# Sorted by status: Running > Idle > Done
sessions=$(tmux list-sessions -F "#{session_id}|#{session_name}" 2>/dev/null | grep "|Claude-")
if [[ -z "$sessions" ]]; then
echo "No Claude sessions found"
sleep 1
exit 0
fi
# Sort by status: Running > Idle > Done
sorted=$(echo "$sessions" | while read -r line; do
name="${line#*|}"
if [[ "$name" == *"[Idle"* ]]; then
echo "2|$line"
elif [[ "$name" == *"[Done"* ]]; then
echo "3|$line"
elif [[ "$name" == *"["* ]]; then
echo "1|$line"
else
echo "4|$line"
fi
done | sort | cut -d"|" -f2-)
selected=$(echo "$sorted" | \
fzf --prompt="Claude sessions > " \
--delimiter="|" \
--with-nth=2 \
--preview='id=$(echo {} | cut -d"|" -f1); tmux capture-pane -p -t "$id:" -S -50 2>/dev/null' \
--preview-window=right:70%)
if [[ -n "$selected" ]]; then
session_id=$(echo "$selected" | cut -d"|" -f1)
tmux switch-client -t "$session_id"
fi
claude-session-last.sh
Quick switch to the most recently used Claude session.
#!/usr/bin/env bash
# Switch to the most recently active Claude session
last_session=$(tmux list-sessions -F "#{session_last_attached} #{session_name}" 2>/dev/null | \
grep "Claude-" | \
sort -rn | \
head -1 | \
cut -d' ' -f2-)
if [[ -n "$last_session" ]]; then
tmux switch-client -t "$last_session"
else
echo "No Claude sessions found"
sleep 1
fi
claude-session-cycle.sh
Cycle through idle sessions without opening fzf.
#!/usr/bin/env bash
# Cycle through idle Claude sessions
idle_sessions=$(tmux list-sessions -F "#{session_id}|#{session_name}" 2>/dev/null | grep "|Claude-" | grep "\[Idle")
if [[ -z "$idle_sessions" ]]; then
tmux display-message "No idle Claude sessions"
exit 0
fi
count=$(echo "$idle_sessions" | wc -l)
if [[ "$count" -eq 1 ]]; then
session_id=$(echo "$idle_sessions" | cut -d"|" -f1)
tmux switch-client -t "$session_id"
exit 0
fi
current_id=$(tmux display-message -p "#{session_id}" 2>/dev/null)
found=0
first_id=""
next_id=""
while IFS= read -r line; do
session_id=$(echo "$line" | cut -d"|" -f1)
[[ -z "$first_id" ]] && first_id="$session_id"
if [[ "$found" -eq 1 ]]; then
next_id="$session_id"
break
fi
if [[ "$session_id" == "$current_id" ]]; then
found=1
fi
done <<< "$idle_sessions"
if [[ -z "$next_id" ]]; then
next_id="$first_id"
fi
tmux switch-client -t "$next_id"
claude-tmux-status.sh
Core status update script that renames tmux sessions.
#!/usr/bin/env bash
# Update tmux session name with Claude status
# Usage: claude-tmux-status.sh <status>
STATUS="${1:-idle}"
tmux list-sessions &>/dev/null || exit 0
current_dir=$(pwd)
dir_name=$(basename "$current_dir")
safe_name=$(echo "$dir_name" | tr './:' '_')
current_session=$(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^Claude-${safe_name}" | head -1)
if [[ -z "$current_session" && -n "$TMUX" ]]; then
current_session=$(tmux display-message -p "#{session_name}" 2>/dev/null)
fi
[[ "$current_session" != Claude-* ]] && exit 0
base_name=$(echo "$current_session" | sed -E 's/ \[.*\]$//')
session_hash=$(echo "$base_name" | md5sum | cut -c1-16)
LAST_TOOL_FILE="/tmp/claude-last-tool-$session_hash"
TOOL_USED_FILE="/tmp/claude-tool-used-$session_hash"
case "$STATUS" in
thinking|running)
rm -f "$TOOL_USED_FILE"
new_name="$base_name [Thinking~]"
;;
tool:*)
tool_name="${STATUS#tool:}"
touch "$TOOL_USED_FILE"
echo "$tool_name" > "$LAST_TOOL_FILE"
new_name="$base_name [$tool_name]"
;;
idle)
if [[ -f "$TOOL_USED_FILE" ]]; then
if [[ -f "$LAST_TOOL_FILE" ]]; then
last_tool=$(cat "$LAST_TOOL_FILE")
new_name="$base_name [Idle> $last_tool]"
else
new_name="$base_name [Idle]"
fi
else
new_name="$base_name [Idle> Chatted]"
fi
rm -f "$TOOL_USED_FILE"
;;
done)
if [[ -f "$LAST_TOOL_FILE" ]]; then
last_tool=$(cat "$LAST_TOOL_FILE")
new_name="$base_name [Done> $last_tool]"
rm -f "$LAST_TOOL_FILE"
else
new_name="$base_name [Done]"
fi
rm -f "$TOOL_USED_FILE"
;;
*)
exit 1
;;
esac
tmux rename-session -t "$current_session" "$new_name" 2>/dev/null
claude-tmux-pretool.sh
Hook handler that captures tool names from Claude Code.
#!/usr/bin/env bash
# PreToolUse hook to update tmux session with current tool name
input=$(cat)
tool_name=$(echo "$input" | /usr/bin/jq -r '.tool_name // "Unknown"' 2>/dev/null)
[[ -z "$tool_name" || "$tool_name" == "null" ]] && exit 0
(/home/ryuichi/dotenv/local/bin/claude-tmux-status.sh "tool:$tool_name" &>/dev/null) &
exit 0
Claude Code Settings
Configure hooks in ~/.claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "/path/to/claude-tmux-status.sh running"
}
]
}
],
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "/path/to/claude-tmux-pretool.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "/path/to/claude-tmux-status.sh idle"
}
]
}
]
}
}
Hook Events
| Event | Trigger | Action |
|---|---|---|
UserPromptSubmit | User sends a message | Set status to [Thinking~] |
PreToolUse | Before each tool executes | Set status to [ToolName] |
Stop | Claude finishes responding | Set status to [Idle> LastTool] |
PreToolUse JSON Input
The PreToolUse hook receives JSON via stdin:
{
"session_id": "abc123",
"hook_event_name": "PreToolUse",
"tool_name": "Read",
"tool_input": {
"file_path": "/path/to/file.txt"
},
"tool_use_id": "toolu_01ABC123..."
}
Requirements
- tmux 3.0+
- fzf (for session picker)
- jq (for JSON parsing in hooks)
- Claude Code CLI
Installation
- Copy scripts to
~/dotenv/local/bin/(or your preferred location) - Make scripts executable:
chmod +x ~/dotenv/local/bin/claude-*.sh - Add keybindings to
~/.tmux.conf - Add hooks to
~/.claude/settings.json - Ensure scripts are in your
$PATH - Reload tmux:
tmux source ~/.tmux.conf
Tips
- Session names use
>instead of:(colons have special meaning in tmux) - Multiple sessions for the same directory get numbered:
Claude-myproject-2 - The fzf picker sorts sessions: Active > Idle > Done
- Press
<prefix> irepeatedly to quickly cycle through waiting sessions
Summary
This tmux integration transforms how you work with Claude Code. Instead of juggling terminal tabs, you get:
- Visual status indicators - Know at a glance what each Claude session is doing
- Fast navigation - Switch between sessions with keyboard shortcuts
- Persistent sessions - Sessions survive even when Claude exits
- Organized workflow - Keep frontend, backend, and other tasks separate
Give it a try and let me know how it works for you!




