Last active
November 26, 2025 15:34
-
-
Save nazq/3d179988627b15119944da573c91f1ab to your computer and use it in GitHub Desktop.
Claude Session Manager - tmux session manager for Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Claude Session Manager for Git Worktrees | |
| # Usage: claude_session [session_name] [--attach] | |
| # Alias: cs | |
| claude_session() { | |
| local session_name="" | |
| local auto_attach=false | |
| local custom_name="" | |
| local list_sessions=false | |
| local attach_number="" | |
| local kill_number="" | |
| local continue_mode=false | |
| local dangerous_mode=false | |
| # Parse arguments | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --list|-l) | |
| list_sessions=true | |
| shift | |
| ;; | |
| --kill|-k) | |
| if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then | |
| kill_number="$2" | |
| shift 2 | |
| else | |
| echo "β --kill requires a session number" | |
| echo "Use 'cs -l' to see numbered sessions" | |
| return 1 | |
| fi | |
| ;; | |
| --attach|-a) | |
| if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then | |
| attach_number="$2" | |
| shift 2 | |
| else | |
| auto_attach=true | |
| shift | |
| fi | |
| ;; | |
| --continue|-c) | |
| continue_mode=true | |
| shift | |
| ;; | |
| --dangerous|-x) | |
| dangerous_mode=true | |
| shift | |
| ;; | |
| --help|-h) | |
| echo "Usage: claude_session [session_name] [--attach [number]] [--continue] [--dangerous] [--list] [--kill number]" | |
| echo "" | |
| echo "Creates a tmux session with Claude Code running" | |
| echo "" | |
| echo "Options:" | |
| echo " session_name Custom session name (optional)" | |
| echo " --attach, -a Automatically attach to session after creation" | |
| echo " --attach NUM, -a NUM Attach to session number NUM from list" | |
| echo " --continue, -c Start Claude with --continue flag" | |
| echo " --dangerous, -x Start Claude with --dangerously-skip-permissions --permission-mode acceptEdits" | |
| echo " --list, -l List all active tmux sessions with numbers" | |
| echo " --kill NUM, -k NUM Kill session number NUM from list" | |
| echo " --help, -h Show this help message" | |
| echo "" | |
| echo "Session naming logic:" | |
| echo " - In git worktree: REPO:BRANCH" | |
| echo " - Not in git worktree: FOLDER_NAME" | |
| echo " - Custom name overrides automatic naming" | |
| echo "" | |
| echo "Examples:" | |
| echo " cs # Create session with auto-detected name" | |
| echo " cs -l # List all sessions with numbers" | |
| echo " cs -a 3 # Attach to session #3 from the list" | |
| echo " cs -k 2 # Kill session #2 from the list" | |
| echo " cs my-project -a # Create 'my-project' session and attach" | |
| echo " cs -c -a # Create session with --continue and attach" | |
| echo " cs -x -a # Create session with dangerous mode (skip permissions) and attach" | |
| return 0 | |
| ;; | |
| *) | |
| custom_name="$1" | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Handle list sessions | |
| if [[ "$list_sessions" == true ]]; then | |
| echo "π Active tmux sessions:" | |
| local sessions=($(tmux list-sessions -F "#{session_name}" 2>/dev/null | sort)) | |
| if [[ ${#sessions[@]} -eq 0 ]]; then | |
| echo " No active sessions found" | |
| return 0 | |
| fi | |
| local i=1 | |
| for session in "${sessions[@]}"; do | |
| local session_info=$(tmux list-sessions | grep "^$session:" | head -1) | |
| printf "%2d. %s\n" "$i" "$session_info" | |
| ((i++)) | |
| done | |
| return 0 | |
| fi | |
| # Handle kill by number | |
| if [[ -n "$kill_number" ]]; then | |
| local sessions=($(tmux list-sessions -F "#{session_name}" 2>/dev/null | sort)) | |
| if [[ ${#sessions[@]} -eq 0 ]]; then | |
| echo "β No active sessions found" | |
| return 1 | |
| fi | |
| if [[ "$kill_number" -lt 1 || "$kill_number" -gt ${#sessions[@]} ]]; then | |
| echo "β Invalid session number: $kill_number" | |
| echo "π Available sessions (1-${#sessions[@]}):" | |
| claude_session --list | |
| return 1 | |
| fi | |
| local target_session="${sessions[$((kill_number-1))]}" | |
| echo "π Killing session #$kill_number: $target_session" | |
| tmux kill-session -t "$target_session" | |
| if [[ $? -eq 0 ]]; then | |
| echo "β Session killed successfully" | |
| else | |
| echo "β Failed to kill session" | |
| fi | |
| return 0 | |
| fi | |
| # Handle attach by number | |
| if [[ -n "$attach_number" ]]; then | |
| local sessions=($(tmux list-sessions -F "#{session_name}" 2>/dev/null | sort)) | |
| if [[ ${#sessions[@]} -eq 0 ]]; then | |
| echo "β No active sessions found" | |
| return 1 | |
| fi | |
| if [[ "$attach_number" -lt 1 || "$attach_number" -gt ${#sessions[@]} ]]; then | |
| echo "β Invalid session number: $attach_number" | |
| echo "π Available sessions (1-${#sessions[@]}):" | |
| claude_session --list | |
| return 1 | |
| fi | |
| local target_session="${sessions[$((attach_number-1))]}" | |
| echo "π Attaching to session #$attach_number: $target_session" | |
| tmux attach-session -t "$target_session" | |
| return 0 | |
| fi | |
| # Continue with normal session creation logic... | |
| # Determine session name | |
| if [[ -n "$custom_name" ]]; then | |
| session_name="$custom_name" | |
| echo "π― Using custom session name: $session_name" | |
| else | |
| # Check if we're in a git worktree | |
| if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then | |
| # Get repository name (from remote or folder name) | |
| local repo_name="" | |
| local remote_url=$(git remote get-url origin 2>/dev/null) | |
| if [[ -n "$remote_url" ]]; then | |
| # Extract repo name from remote URL | |
| repo_name=$(basename "$remote_url" .git) | |
| else | |
| # Fallback to git root directory name | |
| repo_name=$(basename "$(git rev-parse --show-toplevel)") | |
| fi | |
| # Get current branch name | |
| local branch_name=$(git branch --show-current 2>/dev/null) | |
| if [[ -z "$branch_name" ]]; then | |
| # Fallback for detached HEAD | |
| branch_name=$(git rev-parse --short HEAD 2>/dev/null || echo "detached") | |
| fi | |
| session_name="${repo_name}:${branch_name}" | |
| echo "π Git worktree detected: $session_name" | |
| else | |
| # Use current directory name | |
| session_name=$(basename "$PWD") | |
| echo "π Using directory name: $session_name" | |
| fi | |
| fi | |
| # Sanitize session name for tmux (tmux converts : and . to _) | |
| local original_name="$session_name" | |
| session_name=$(echo "$session_name" | sed 's/[:.]/_/g' | sed 's/[^a-zA-Z0-9_-]/_/g') | |
| if [[ "$session_name" != "$original_name" ]]; then | |
| echo "π§ Sanitized session name: $original_name β $session_name" | |
| fi | |
| # Check if session already exists | |
| if tmux has-session -t "$session_name" 2>/dev/null; then | |
| echo "β‘ Session '$session_name' already exists!" | |
| read -p "Do you want to [a]ttach, [k]ill and recreate, or [c]ancel? (a/k/c): " choice | |
| case $choice in | |
| a|A|"") | |
| echo "π Attaching to existing session..." | |
| tmux attach-session -t "$session_name" | |
| return 0 | |
| ;; | |
| k|K) | |
| echo "π Killing existing session..." | |
| tmux kill-session -t "$session_name" | |
| ;; | |
| c|C) | |
| echo "β Cancelled" | |
| return 0 | |
| ;; | |
| *) | |
| echo "β Invalid choice. Cancelled." | |
| return 1 | |
| ;; | |
| esac | |
| fi | |
| # Warn if dangerous mode is enabled | |
| if [[ "$dangerous_mode" == true ]]; then | |
| echo "β οΈ WARNING: Dangerous mode enabled!" | |
| echo " Claude will run with --dangerously-skip-permissions --permission-mode acceptEdits" | |
| echo " This skips all permission prompts and auto-accepts edits." | |
| fi | |
| # Create tmux session | |
| echo "π Creating tmux session: $session_name" | |
| echo "π Directory: $PWD" | |
| # Create detached session in current directory | |
| tmux new-session -d -s "$session_name" -c "$PWD" | |
| # Give tmux a moment to create the session | |
| sleep 0.5 | |
| # Check if tmux session was created successfully | |
| if ! tmux has-session -t "$session_name" 2>/dev/null; then | |
| echo "β Failed to create tmux session '$session_name'" | |
| echo "π Checking if tmux created it with a different name..." | |
| echo "π Current sessions:" | |
| tmux list-sessions 2>/dev/null | |
| return 1 | |
| fi | |
| # Configure the session | |
| tmux send-keys -t "$session_name" "clear" C-m | |
| tmux send-keys -t "$session_name" "echo 'π€ Starting Claude Code session for: $session_name'" C-m | |
| tmux send-keys -t "$session_name" "echo 'π Directory: $PWD'" C-m | |
| # Check if claude command exists | |
| if ! command -v claude >/dev/null 2>&1; then | |
| echo "β οΈ Warning: 'claude' command not found" | |
| echo " Make sure Claude Code is installed and in your PATH" | |
| tmux send-keys -t "$session_name" "echo 'Warning: claude command not found. Please install Claude Code.'" C-m | |
| else | |
| # Start Claude | |
| echo "π€ Starting Claude Code..." | |
| local claude_cmd="claude" | |
| if [[ "$continue_mode" == true ]]; then | |
| claude_cmd="$claude_cmd --continue" | |
| fi | |
| if [[ "$dangerous_mode" == true ]]; then | |
| claude_cmd="$claude_cmd --dangerously-skip-permissions --permission-mode acceptEdits" | |
| fi | |
| tmux send-keys -t "$session_name" "$claude_cmd" C-m | |
| fi | |
| # Set window name | |
| tmux rename-window -t "$session_name:0" "claude" | |
| echo "β Session created successfully!" | |
| echo "π Connect with: tmux attach -t '$session_name'" | |
| # Auto-attach if requested | |
| if [[ "$auto_attach" == true ]]; then | |
| echo "π Auto-attaching to session..." | |
| sleep 1 # Brief pause to let Claude start | |
| tmux attach-session -t "$session_name" | |
| fi | |
| } | |
| # Convenience aliases | |
| alias cs='claude_session' | |
| alias claude-session='claude_session' | |
| # Helper functions for session management | |
| claude_session_list() { | |
| claude_session --list | |
| } | |
| claude_session_attach() { | |
| if [[ -n "$1" ]]; then | |
| claude_session --attach "$1" | |
| else | |
| echo "Usage: claude_session_attach <number>" | |
| echo "Use 'cs -l' to see numbered session list" | |
| fi | |
| } | |
| claude_session_kill() { | |
| if [[ -n "$1" ]]; then | |
| claude_session --kill "$1" | |
| else | |
| echo "Usage: claude_session_kill <number>" | |
| echo "Use 'cs -l' to see numbered session list" | |
| fi | |
| } | |
| claude_session_kill_all() { | |
| # Get all sessions except some known system ones | |
| local all_sessions=$(tmux list-sessions -F "#{session_name}" 2>/dev/null) | |
| local claude_sessions="" | |
| # Filter for likely Claude sessions (contains underscore, dash, or "claude") | |
| while IFS= read -r session; do | |
| if [[ "$session" =~ (claude|_|-) && "$session" != "crashes" ]]; then | |
| claude_sessions+="$session"$'\n' | |
| fi | |
| done <<< "$all_sessions" | |
| # Remove trailing newline | |
| claude_sessions=$(echo -n "$claude_sessions") | |
| if [[ -n "$claude_sessions" ]]; then | |
| echo "π Found likely Claude sessions:" | |
| echo "$claude_sessions" | sed 's/^/ /' | |
| echo "" | |
| read -p "Kill these sessions? (y/N): " confirm | |
| if [[ "$confirm" =~ ^[Yy]$ ]]; then | |
| echo "$claude_sessions" | xargs -I {} tmux kill-session -t {} | |
| echo "β Sessions terminated" | |
| else | |
| echo "β Cancelled" | |
| fi | |
| else | |
| echo "βΉοΈ No Claude sessions to kill" | |
| fi | |
| } | |
| # Additional aliases | |
| alias cs-list='claude_session --list' | |
| alias cs-l='claude_session --list' | |
| alias cs-attach='claude_session_attach' | |
| alias cs-kill-all='claude_session_kill_all' |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Looks great. I've been toying with switching to zellij for a while now but didn't want to do this leg work. Thanks for this