Skip to content

Instantly share code, notes, and snippets.

@stracker-phil
Last active October 8, 2026 07:12
Show Gist options
  • Select an option

  • Save stracker-phil/9f84927a556632c7f9cc06663b534f14 to your computer and use it in GitHub Desktop.

Select an option

Save stracker-phil/9f84927a556632c7f9cc06663b534f14 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).
#
# 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
@stracker-phil

Copy link
Copy Markdown
Author

@stracker-phil

Copy link
Copy Markdown
Author

Refactored the script to use Electron's --user-data-dir flag to run fully isolated instances, which two benefits over the previous symlink version:

  1. You can use Claude Cowork, which does not run in a symlinked environment
  2. All instances are isolated, making it safe to run them side-by-side

@aoxborrow

Copy link
Copy Markdown

here's a small update that gives you color-coded launchers that "open or focus" the correct instance of the app

https://gist.github.com/aoxborrow/5a3a0ac0aa37819cc19439b70a552221

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment