Last active
March 10, 2026 10:16
-
-
Save gatopeich/2b4e805f44791fc817bc043eb82ecaac to your computer and use it in GitHub Desktop.
Claude Code Chooser - TUI project picker and launcher 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/sh | |
| # Claude Code Chooser - TUI project picker and launcher for Claude Code | |
| # Lists recent projects, browse directories, toggle dangerous mode, set extra args, | |
| # then exec's claude in the chosen directory. | |
| # | |
| # https://gist.github.com/gatopeich/2b4e805f44791fc817bc043eb82ecaac | |
| # Author: gatopeich @ github | |
| # | |
| # Recommended: latest kitty + tmux. Uses exec + tmux remain-on-exit so the | |
| # shell process is replaced by claude and the pane stays open after exit | |
| # for scrollback review. | |
| # | |
| # Tmux integration: | |
| # Bind to a key (e.g. Ctrl+F2) in tmux.conf: | |
| # bind -n C-F2 split-window -h /path/to/claude-chooser.sh | |
| # | |
| # Kitty terminal integration: | |
| # # Desktop launcher (~/.local/share/applications/claude-kitty.desktop): | |
| # Exec=kitty --class claude-kitty -T "Claude Code" --session ~/.config/kitty/claude-session.conf | |
| # | |
| # # Session file (~/.config/kitty/claude-session.conf) - new tabs also open chooser: | |
| # launch /path/to/claude-chooser.sh | |
| # | |
| # Optional: set window icon on X11 with xseticon.py: | |
| # https://gist.github.com/gatopeich/9d0c61cae0a4e01b23b0afc543c27fbb | |
| # | |
| # Requires: dialog, claude, python3 (for session discovery) | |
| ICON="$HOME/.local/share/icons/claude-code.png" | |
| EXTRA_FILE="$HOME/.config/claude-chooser-extra-args" | |
| # Temp file for dialog --file menus | |
| MENUFILE=$(mktemp) | |
| trap 'rm -f "$MENUFILE"' EXIT | |
| # Write a "tag" "description" line to MENUFILE, escaping embedded quotes | |
| menuline() { | |
| local tag desc | |
| tag=$(printf '%s' "$1" | sed 's/"/\\"/g') | |
| desc=$(printf '%s' "$2" | sed 's/"/\\"/g') | |
| printf '"%s" "%s"\n' "$tag" "$desc" >> "$MENUFILE" | |
| } | |
| # Dialog theme: override color7 (WHITE) to actual white for dialog backgrounds | |
| printf '\033]4;7;#f0f0f0\007' | |
| printf '\033]4;15;#ffffff\007' | |
| export DIALOGRC="$HOME/.config/claude-chooser-dialogrc" | |
| # First entry: invoking CWD (tmux pane's CWD or $HOME) | |
| START_DIR=$(tmux display-message -p -t "$TMUX_PANE" '#{pane_current_path}' 2>/dev/null || echo "$HOME") | |
| # Extract recent project paths from Claude sessions, sorted by most recent session activity | |
| DIRS=$(echo "$START_DIR"; python3 -c " | |
| import json, glob, os, sys | |
| first = sys.argv[1] | |
| seen = {first} | |
| for f in sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')), key=os.path.getmtime, reverse=True): | |
| try: | |
| with open(f) as fh: | |
| for line in fh: | |
| rec = json.loads(line) | |
| path = rec.get('cwd') or rec.get('projectPath') or '' | |
| if os.path.isdir(path) and path not in seen: | |
| seen.add(path) | |
| print(path) | |
| break | |
| if len(seen) >= 15: break | |
| except: pass | |
| " "$START_DIR" 2>/dev/null) | |
| # Browse directories using dialog menus | |
| browse_dir() { | |
| local cur="${1:-$HOME}" | |
| while true; do | |
| : > "$MENUFILE" | |
| menuline ">>> SELECT THIS FOLDER <<<" "$cur" | |
| menuline ".." "Parent directory" | |
| for d in "$cur"/*/; do | |
| [ -d "$d" ] && menuline "$(basename "$d")/" "" | |
| done | |
| local pick | |
| pick=$(dialog --clear --title "Browse: $cur" \ | |
| --menu "Choose directory:" 0 0 0 \ | |
| --file "$MENUFILE" \ | |
| 3>&1 1>&2 2>&3) || return 1 | |
| if [ "$pick" = ">>> SELECT THIS FOLDER <<<" ]; then | |
| echo "$cur" | |
| return 0 | |
| elif [ "$pick" = ".." ]; then | |
| cur=$(dirname "$cur") | |
| else | |
| cur="$cur/${pick%/}" | |
| fi | |
| done | |
| } | |
| # State | |
| DANGEROUS=off | |
| EXTRA=$(cat "$EXTRA_FILE" 2>/dev/null) | |
| while true; do | |
| # Build menu file | |
| : > "$MENUFILE" | |
| i=0 | |
| printf '%s\n' "$DIRS" | while IFS= read -r d; do | |
| [ -n "$d" ] && menuline "$d" "" && i=$((i + 1)) | |
| [ $i -ge 10 ] && break | |
| done | |
| menuline "Browse..." "" | |
| menuline "---" "─────────────────────" | |
| if [ "$DANGEROUS" = "on" ]; then | |
| menuline "[X] Dangerous mode" "skip all permissions" | |
| else | |
| menuline "[ ] Dangerous mode" "skip all permissions" | |
| fi | |
| if [ -n "$EXTRA" ]; then | |
| menuline "EXTRA" "args: $EXTRA" | |
| else | |
| menuline "EXTRA" "args..." | |
| fi | |
| PICK=$(dialog --clear --title "Claude Code" \ | |
| --menu "Select project to launch, or toggle options:" 0 0 0 \ | |
| --file "$MENUFILE" \ | |
| 3>&1 1>&2 2>&3) || exit 1 | |
| case "$PICK" in | |
| "---") continue ;; | |
| *"Dangerous mode") | |
| [ "$DANGEROUS" = "off" ] && DANGEROUS=on || DANGEROUS=off | |
| continue ;; | |
| "EXTRA") | |
| EXTRA=$(dialog --clear --title "Extra args" \ | |
| --inputbox "Claude arguments (e.g. --continue --model opus):" 8 60 "$EXTRA" \ | |
| 3>&1 1>&2 2>&3) || EXTRA="" | |
| echo "$EXTRA" > "$EXTRA_FILE" | |
| continue ;; | |
| "Browse...") | |
| DIR=$(browse_dir "$HOME") || continue | |
| break ;; | |
| *) | |
| DIR="$PICK" | |
| break ;; | |
| esac | |
| done | |
| # Restore original terminal colors | |
| printf '\033]4;7;#6e7781\007' | |
| printf '\033]4;15;#8c959f\007' | |
| clear | |
| CLAUDE_ARGS="" | |
| if [ "$DANGEROUS" = "on" ]; then | |
| CLAUDE_ARGS="$CLAUDE_ARGS --dangerously-skip-permissions" | |
| printf '\033]11;#fcf3cf\007' | |
| printf '\033]10;#1a1a1a\007' | |
| fi | |
| [ -n "$EXTRA" ] && CLAUDE_ARGS="$CLAUDE_ARGS $EXTRA" | |
| # Set window icon (optional: requires xseticon and xdotool) | |
| wid=$(xdotool getactivewindow 2>/dev/null) && xseticon "$wid" "$ICON" 2>/dev/null | |
| DIRNAME=$(basename "$DIR") | |
| printf '\033]0;Claude on %s\007' "$DIRNAME" | |
| [ -n "$TMUX_PANE" ] && tmux rename-window -t "$TMUX_PANE" "✳$DIRNAME" 2>/dev/null | |
| # Remap Ctrl+Z to Ctrl+_ (undo) in panes launched by this script, normal suspend elsewhere | |
| [ -n "$TMUX_PANE" ] && tmux bind -n C-z if-shell "grep -qw $0 /proc/#{pane_pid}/cmdline" "send-keys C-_" "send-keys C-z" | |
| # Keep pane open after exit so scrollback is reviewable, then replace shell with claude | |
| [ -n "$TMUX_PANE" ] && tmux set-option -p -t "$TMUX_PANE" remain-on-exit on | |
| cd "$DIR" && exec claude $CLAUDE_ARGS |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment