A

Claude Code + Tmux Integration - Managing Multiple AI Sessions Like a Pro

January 3, 20265 min read
Back to all posts

A comprehensive guide to managing multiple Claude Code sessions with tmux, featuring real-time status updates and seamless session navigation for power users.

Share

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:

Loading diagram...

Hook Event Flow

When you interact with Claude, the hooks fire in sequence to update the session name:

Loading diagram...

Session Status States

The session name dynamically updates to show Claude's current state:

StatusDisplayMeaning
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:

Bash
# 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
KeyAction
<prefix> fOpen tmux-sessionizer (project picker)
<prefix> C-aSpawn new Claude session in current directory
<prefix> aOpen fzf picker to switch between Claude sessions
<prefix> lSwitch to most recently used Claude session
<prefix> iCycle through idle Claude sessions

Workflow Overview

Loading diagram...

Typical Session Flow

Loading diagram...

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.

Bash
#!/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.

Bash
#!/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.

Bash
#!/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.

Bash
#!/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.

Bash
#!/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.

Bash
#!/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.

Bash
#!/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:

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

EventTriggerAction
UserPromptSubmitUser sends a messageSet status to [Thinking~]
PreToolUseBefore each tool executesSet status to [ToolName]
StopClaude finishes respondingSet status to [Idle> LastTool]

PreToolUse JSON Input

The PreToolUse hook receives JSON via stdin:

JSON
{ "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

  1. Copy scripts to ~/dotenv/local/bin/ (or your preferred location)
  2. Make scripts executable: chmod +x ~/dotenv/local/bin/claude-*.sh
  3. Add keybindings to ~/.tmux.conf
  4. Add hooks to ~/.claude/settings.json
  5. Ensure scripts are in your $PATH
  6. 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> i repeatedly 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!

Written by

Ameyanagi

Ameyanagi

Principal Scientist at the intersection of chemistry and computational science. Passionate about XAFS analysis, heterogeneous catalysis, and Rust programming.