Skip to content

Instantly share code, notes, and snippets.

@nhatnx
Forked from stracker-phil/claude_quick.sh
Last active September 20, 2026 15:45
Show Gist options
  • Select an option

  • Save nhatnx/7eb71224452d8ee6f8a5fbf6249809ed to your computer and use it in GitHub Desktop.

Select an option

Save nhatnx/7eb71224452d8ee6f8a5fbf6249809ed to your computer and use it in GitHub Desktop.
Cleaned up, verified and translated version of weidwonder/claude-desktop-multi-instance
#!/bin/bash
# Claude Desktop Multi-Instance Launcher
#
# Uses Electron's --user-data-dir flag to run fully isolated instances.
# Each instance gets its own data directory (config, VM bundles, SDK, sessions).
#
# Need to make the helper executable
# chmod +x ./claude_quick.sh
#
# Usage:
# ./claude_quick.sh # Show interactive menu
# ./claude_quick.sh <instance> # Launch named instance
# ./claude_quick.sh list # List all instances
# ./claude_quick.sh delete <name> # Delete an instance
# ./claude_quick.sh wrapper <name> # Create app wrapper for Dock/Launchpad
# ./claude_quick.sh fix # Regenerate wrapper launcher scripts
# ./claude_quick.sh diagnose # Diagnose problems
INSTANCES_BASE="$HOME/.claude-instances"
# ==================== Functions ====================
copy_claude_icon() {
local target_dir="$1"
local icon=$(find "/Applications/Claude.app" -name "*.icns" -type f 2>/dev/null | head -n 1)
if [ -n "$icon" ]; then
cp "$icon" "$target_dir/claude-icon.icns"
echo "✅ Copied icon: $(basename "$icon")"
else
echo "⚠️ No icon found, wrapper will use default macOS icon"
fi
}
list_instance_names() {
ls -1 "$INSTANCES_BASE" 2>/dev/null
}
# List all instance names including the virtual "default" instance.
list_all_instance_names() {
echo "default"
list_instance_names
}
# Find the .app wrapper for a given instance by checking its config.sh.
find_wrapper_for_instance() {
local target="$1"
for app in /Applications/*.app; do
local config="$app/Contents/MacOS/config.sh"
[ ! -f "$config" ] && continue
local name=$(grep "^INSTANCE_NAME=" "$config" 2>/dev/null | head -1 | sed 's/^INSTANCE_NAME=//')
if [ "$name" = "$target" ]; then
echo "$app"
return 0
fi
done
return 1
}
# List all .app bundles that have a config.sh (i.e., Claude instance wrappers).
find_all_wrappers() {
for app in /Applications/*.app; do
[ -f "$app/Contents/MacOS/config.sh" ] && echo "$app"
done
}
# Extract the instance name from a wrapper's config.sh.
get_wrapper_instance_name() {
local config="$1/Contents/MacOS/config.sh"
grep "^INSTANCE_NAME=" "$config" 2>/dev/null | head -1 | sed 's/^INSTANCE_NAME=//'
}
list_instances() {
echo "Claude Desktop Instances"
echo "========================"
echo ""
# Default instance (uses Claude's built-in data directory)
local wrapper_status="no wrapper"
find_wrapper_for_instance "default" &>/dev/null && wrapper_status="has wrapper"
echo " - default ($wrapper_status, built-in)"
local count=0
for dir in "$INSTANCES_BASE"/*/; do
[ ! -d "$dir" ] && continue
local name=$(basename "$dir")
local wrapper_status="no wrapper"
find_wrapper_for_instance "$name" &>/dev/null && wrapper_status="has wrapper"
echo " - $name ($wrapper_status)"
count=$((count + 1))
done
echo ""
}
diagnose() {
echo "🔍 Claude Desktop Diagnostics"
echo "=============================="
echo ""
if [ ! -d "/Applications/Claude.app" ]; then
echo "❌ Claude.app not found in /Applications"
echo " Download from https://claude.ai/download"
return
fi
echo "✅ Claude.app installed"
echo ""
echo "App wrappers:"
local found=false
while IFS= read -r app; do
found=true
local instance=$(get_wrapper_instance_name "$app")
local display=$(basename "$app" .app)
echo " 📱 $display → instance: $instance"
done < <(find_all_wrappers)
$found || echo " (none)"
echo ""
echo "Instance directories:"
if [ -d "$INSTANCES_BASE" ]; then
list_instance_names | sed 's/^/ - /' || echo " (none)"
else
echo " (no instances directory)"
fi
}
# Write the static claude-launcher script into a wrapper.
# Can be safely regenerated at any time — config.sh holds the instance name.
write_launcher_script() {
local wrapper_path="$1"
local script_path="$wrapper_path/Contents/MacOS/claude-launcher"
local main_script=$(which claude_quick.sh 2>/dev/null)
if [ -z "$main_script" ]; then
echo "❌ 'claude_quick.sh' not found in PATH"
return 1
fi
cat > "$script_path" << LAUNCHER_EOF
#!/bin/bash
source "\$(dirname "\$0")/config.sh"
exec "$main_script" "\$INSTANCE_NAME"
LAUNCHER_EOF
chmod +x "$script_path"
}
fix_wrappers() {
echo "🔧 Fixing Claude Desktop wrappers"
echo "=================================="
local found=false
while IFS= read -r app; do
found=true
local instance=$(get_wrapper_instance_name "$app")
echo "🔨 Repairing: $(basename "$app") (instance: $instance)"
if write_launcher_script "$app"; then
echo " ✅ Regenerated launcher script"
else
echo " ❌ Failed to regenerate launcher"
fi
if [ ! -f "$app/Contents/Resources/claude-icon.icns" ]; then
copy_claude_icon "$app/Contents/Resources"
fi
done < <(find_all_wrappers)
$found || echo "No wrappers found to fix."
echo "✅ Done"
}
delete_instance() {
local name="$1"
if [ -z "$name" ]; then
echo "Available instances:"
list_instance_names | sed 's/^/ - /'
echo ""
read -p "Instance name to delete: " name
fi
if [ -z "$name" ]; then
echo "❌ Invalid instance name"
return 1
fi
if is_default_instance "$name"; then
echo "❌ Cannot delete the default instance"
return 1
fi
if [ ! -d "$INSTANCES_BASE/$name" ]; then
echo "❌ Instance '$name' does not exist"
return 1
fi
local wrapper=$(find_wrapper_for_instance "$name")
echo "⚠️ This will delete:"
echo " - Data: $INSTANCES_BASE/$name"
[ -n "$wrapper" ] && echo " - Wrapper: $wrapper"
echo ""
read -p "Type 'yes' to confirm: " confirm
if [ "$confirm" != "yes" ]; then
echo "Cancelled."
return 0
fi
rm -rf "$INSTANCES_BASE/$name"
[ -n "$wrapper" ] && rm -rf "$wrapper"
echo "✅ Instance '$name' deleted"
}
create_app_wrapper() {
local instance_name="$1"
local display_name="$2"
local wrapper_path="/Applications/Claude $display_name.app"
if [ -d "$wrapper_path" ]; then
read -p "⚠️ Wrapper already exists. Overwrite? (y/N): " overwrite
[ "$overwrite" != "y" ] && [ "$overwrite" != "Y" ] && return
rm -rf "$wrapper_path"
fi
mkdir -p "$wrapper_path/Contents/MacOS"
mkdir -p "$wrapper_path/Contents/Resources"
cat > "$wrapper_path/Contents/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>claude-launcher</string>
<key>CFBundleIdentifier</key>
<string>com.anthropic.claude.$instance_name</string>
<key>CFBundleName</key>
<string>Claude $display_name</string>
<key>CFBundleDisplayName</key>
<string>Claude $display_name</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>CFBundleIconFile</key>
<string>claude-icon</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
EOF
cat > "$wrapper_path/Contents/MacOS/config.sh" << CONFIG_EOF
INSTANCE_NAME=$instance_name
CONFIG_EOF
write_launcher_script "$wrapper_path"
copy_claude_icon "$wrapper_path/Contents/Resources"
echo "✅ Wrapper created: $wrapper_path"
echo " Search 'Claude $display_name' in Launchpad or drag to Dock."
}
prompt_wrapper_creation() {
local instance_name="$1"
if [ -z "$instance_name" ]; then
echo "Available instances:"
list_all_instance_names | sed 's/^/ - /'
echo ""
read -p "Create wrapper for which instance: " instance_name
fi
if [ -z "$instance_name" ]; then
echo "❌ Invalid instance name"
return 1
fi
if ! is_default_instance "$instance_name" && [ ! -d "$INSTANCES_BASE/$instance_name" ]; then
echo "❌ Instance '$instance_name' does not exist"
return 1
fi
local default_suffix=$(echo "$instance_name" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
read -p "App name suffix (will show as 'Claude <suffix>') [$default_suffix]: " suffix
[ -z "$suffix" ] && suffix="$default_suffix"
create_app_wrapper "$instance_name" "$suffix"
}
is_default_instance() {
[ "$1" = "default" ]
}
launch_instance() {
local instance_name="$1"
echo "🚀 Launching instance: $instance_name"
if is_default_instance "$instance_name"; then
open -n -a "/Applications/Claude.app"
echo "✅ Claude Desktop launched (default)"
else
local instance_dir="$INSTANCES_BASE/$instance_name"
mkdir -p "$instance_dir"
open -n -a "/Applications/Claude.app" --args --user-data-dir="$instance_dir"
echo "✅ Claude Desktop launched"
echo "📂 Data: $instance_dir"
fi
if ! find_wrapper_for_instance "$instance_name" &>/dev/null; then
echo "💡 Run '$0 wrapper $instance_name' to create a Dock shortcut."
fi
}
# ==================== Main ====================
echo "======================================"
echo " Claude Desktop Multi-Instance Launcher"
echo "======================================"
if [ ! -d "/Applications/Claude.app" ]; then
echo "❌ Claude Desktop not found. Install from https://claude.ai/download"
exit 1
fi
# Handle subcommands
case "$1" in
list) list_instances; exit 0 ;;
delete) delete_instance "$2"; exit $? ;;
wrapper) prompt_wrapper_creation "$2"; exit $? ;;
fix) fix_wrappers; exit 0 ;;
diagnose) diagnose; exit 0 ;;
esac
# Direct instance launch
if [ -n "$1" ]; then
launch_instance "$1"
exit 0
fi
# Interactive menu
echo ""
echo "1. Launch default instance"
echo "2. Select existing instance"
echo "3. Create new instance"
echo "4. Delete instance"
echo "5. Create app wrapper"
echo "6. Diagnose problems"
echo "7. Fix wrappers"
echo ""
read -p "Select (1-7): " choice
case $choice in
1) launch_instance "default" ;;
2)
list_all_instance_names | sed 's/^/ - /'
echo ""
read -p "Instance name: " name
[ -n "$name" ] && launch_instance "$name" || echo "❌ No name provided"
;;
3)
read -p "New instance name: " name
if [ -z "$name" ]; then
echo "❌ No name provided"
elif is_default_instance "$name"; then
echo "❌ 'default' already exists as the built-in instance"
else
launch_instance "$name"
fi
;;
4) delete_instance ;;
5) prompt_wrapper_creation ;;
6) diagnose ;;
7) fix_wrappers ;;
*) echo "Invalid selection." ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment