Skip to content

Instantly share code, notes, and snippets.

@gatopeich
Last active March 10, 2026 10:16
Show Gist options
  • Select an option

  • Save gatopeich/2b4e805f44791fc817bc043eb82ecaac to your computer and use it in GitHub Desktop.

Select an option

Save gatopeich/2b4e805f44791fc817bc043eb82ecaac to your computer and use it in GitHub Desktop.
Claude Code Chooser - TUI project picker and launcher for Claude Code
#!/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