Skip to content

Instantly share code, notes, and snippets.

@jeffreyschultz
Last active January 19, 2026 22:45
Show Gist options
  • Select an option

  • Save jeffreyschultz/3530806e5641d539d08ffa687098386a to your computer and use it in GitHub Desktop.

Select an option

Save jeffreyschultz/3530806e5641d539d08ffa687098386a to your computer and use it in GitHub Desktop.
VSCode launcher that manages VSCode and VSCode Insiders profiles with isolated user data directories
#!/usr/bin/env bash
set -euo pipefail
# VSCode Profile Launcher
# Manages VSCode and VSCode Insiders profiles with isolated user data directories
# Detect if we're in WSL and set up paths accordingly
if [[ -n "$WSL_DISTRO_NAME" ]]; then
# In WSL, store profiles in Windows filesystem to avoid locking issues
WINDOWS_USERPROFILE="$(wslpath "$(cmd.exe /c 'echo %USERPROFILE%' 2>/dev/null | tr -d '\r')")"
STABLE_BASE_DIR="$WINDOWS_USERPROFILE/.vscode-profiles/stable"
INSIDERS_BASE_DIR="$WINDOWS_USERPROFILE/.vscode-profiles/insiders"
else
# Native Linux or other environments
STABLE_BASE_DIR="$HOME/.vscode/stable"
INSIDERS_BASE_DIR="$HOME/.vscode/insiders"
fi
STABLE_CMD="code"
INSIDERS_CMD="code-insiders"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_error() {
echo -e "${RED}Error: $1${NC}" >&2
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_info() {
echo -e "${BLUE}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
show_help() {
cat <<EOF
VSCode Profile Launcher
Usage:
vscode [OPTIONS] <profile-name> [vscode-args...]
vscode <command> [args...]
Launch VSCode with a profile:
vscode <profile-name> Launch VSCode stable with the specified profile
vscode -i <profile-name> Launch VSCode Insiders with the specified profile
vscode <profile-name> [args...] Pass additional arguments to VSCode
Commands:
list, ls List all profiles (stable and insiders)
list-stable List stable profiles only
list-insiders List insiders profiles only
remove, rm <profile-name> Remove a stable profile
remove-insiders <profile-name> Remove an insiders profile
path <profile-name> Show path to stable profile directory
path-insiders <profile-name> Show path to insiders profile directory
clean Remove empty profile directories
help, -h, --help Show this help message
Options:
-i, --insiders Use VSCode Insiders instead of stable
-n, --new Force creation of new profile (create dirs if needed)
Examples:
vscode work Launch VSCode with 'work' profile
vscode -i personal Launch VSCode Insiders with 'personal' profile
vscode work /path/to/project Open project with 'work' profile
vscode list List all profiles
vscode rm old-project Remove 'old-project' profile from stable
vscode path work Show path to 'work' profile directory
Profile directories:
Stable: $STABLE_BASE_DIR/{name}
Insiders: $INSIDERS_BASE_DIR/{name}
EOF
}
check_vscode_installed() {
local cmd=$1
local variant=$2
if ! command -v "$cmd" &> /dev/null; then
print_error "$variant is not installed or not in PATH"
exit 1
fi
}
get_vscode_executable() {
local cmd=$1
local cmd_path
cmd_path=$(which "$cmd" 2>/dev/null)
if [[ -z "$cmd_path" ]]; then
echo ""
return
fi
# Check if we're in WSL and need to find the Windows executable
if [[ -n "$WSL_DISTRO_NAME" ]]; then
# Read the script to find VSCODE_PATH
local vscode_dir
vscode_dir=$(dirname "$(dirname "$cmd_path")")
# Determine the executable name based on stable/insiders
local exe_name="Code.exe"
if [[ "$cmd" == "code-insiders" ]]; then
exe_name="Code - Insiders.exe"
fi
local windows_exe="$vscode_dir/$exe_name"
if [[ -f "$windows_exe" ]]; then
echo "$windows_exe"
return
fi
fi
# Fallback to the command itself
echo "$cmd"
}
get_profile_dir() {
local profile_name=$1
local use_insiders=$2
if [[ $use_insiders -eq 1 ]]; then
echo "$INSIDERS_BASE_DIR/$profile_name"
else
echo "$STABLE_BASE_DIR/$profile_name"
fi
}
ensure_profile_dir() {
local profile_dir=$1
if [[ ! -d "$profile_dir" ]]; then
mkdir -p "$profile_dir"
print_success "Created profile directory: $profile_dir"
fi
}
launch_vscode() {
local profile_name=$1
local use_insiders=$2
shift 2
local extra_args=("$@")
local cmd
local variant
if [[ $use_insiders -eq 1 ]]; then
cmd=$INSIDERS_CMD
variant="VSCode Insiders"
else
cmd=$STABLE_CMD
variant="VSCode"
fi
check_vscode_installed "$cmd" "$variant"
local profile_base_dir
profile_base_dir=$(get_profile_dir "$profile_name" "$use_insiders")
# Profile and server are stored in subdirectories
local profile_dir="$profile_base_dir/profile"
local server_dir="$profile_base_dir/server"
ensure_profile_dir "$profile_dir"
ensure_profile_dir "$server_dir"
# Get the actual executable (handles WSL)
local executable
executable=$(get_vscode_executable "$cmd")
if [[ -z "$executable" ]]; then
print_error "Could not locate $variant executable"
exit 1
fi
# Set VSCODE_SERVER_HOME to isolate the vscode-server for this profile
export VSCODE_SERVER_HOME="$server_dir"
print_info "Launching $variant with profile: $profile_name"
print_info "User data dir: $profile_dir"
print_info "Server dir: $server_dir"
# Convert path to Windows format if in WSL (profiles may be on Windows filesystem)
local exec_profile_dir="$profile_dir"
local remote_flag=()
if [[ -n "$WSL_DISTRO_NAME" ]] && command -v wslpath &> /dev/null; then
# Convert to Windows path and replace backslashes with forward slashes
# Windows handles forward slashes fine and it avoids escaping issues
exec_profile_dir=$(wslpath -w "$profile_dir" | sed 's|\\\\|/|g')
# Add --remote flag to establish WSL context
remote_flag=("--remote" "wsl+$WSL_DISTRO_NAME")
fi
# Convert any relative paths in extra_args to absolute Linux paths
local resolved_args=()
for arg in "${extra_args[@]}"; do
# Check if arg looks like a path (doesn't start with -)
if [[ ! "$arg" =~ ^- ]] && [[ -e "$arg" || "$arg" =~ / ]]; then
# Convert to absolute path
resolved_args+=("$(realpath -m "$arg")")
else
resolved_args+=("$arg")
fi
done
"$executable" --user-data-dir "$exec_profile_dir" "${remote_flag[@]}" "${resolved_args[@]}"
}
list_profiles() {
local base_dir=$1
local label=$2
if [[ ! -d "$base_dir" ]]; then
return
fi
local profiles=()
while IFS= read -r -d '' dir; do
profiles+=("$(basename "$dir")")
done < <(find "$base_dir" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null | sort -z)
if [[ ${#profiles[@]} -gt 0 ]]; then
echo -e "${GREEN}$label:${NC}"
for profile in "${profiles[@]}"; do
echo " - $profile"
done
else
echo -e "${YELLOW}No $label profiles found${NC}"
fi
}
list_all_profiles() {
list_profiles "$STABLE_BASE_DIR" "Stable profiles"
echo
list_profiles "$INSIDERS_BASE_DIR" "Insiders profiles"
}
remove_profile() {
local profile_name=$1
local use_insiders=$2
local profile_dir
profile_dir=$(get_profile_dir "$profile_name" "$use_insiders")
if [[ ! -d "$profile_dir" ]]; then
print_error "Profile '$profile_name' does not exist at: $profile_dir"
exit 1
fi
local variant="stable"
[[ $use_insiders -eq 1 ]] && variant="insiders"
print_warning "This will permanently delete the profile directory and all its contents:"
print_warning " $profile_dir"
read -p "Are you sure? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf "$profile_dir"
print_success "Removed $variant profile: $profile_name"
else
print_info "Cancelled"
fi
}
show_profile_path() {
local profile_name=$1
local use_insiders=$2
local profile_dir
profile_dir=$(get_profile_dir "$profile_name" "$use_insiders")
if [[ -d "$profile_dir" ]]; then
echo "$profile_dir"
else
print_error "Profile '$profile_name' does not exist"
print_info "Path would be: $profile_dir"
exit 1
fi
}
clean_empty_profiles() {
local removed_count=0
for base_dir in "$STABLE_BASE_DIR" "$INSIDERS_BASE_DIR"; do
if [[ ! -d "$base_dir" ]]; then
continue
fi
while IFS= read -r -d '' dir; do
if [[ -z "$(ls -A "$dir")" ]]; then
rmdir "$dir"
print_info "Removed empty directory: $dir"
((removed_count++))
fi
done < <(find "$base_dir" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
done
if [[ $removed_count -eq 0 ]]; then
print_info "No empty profile directories found"
else
print_success "Removed $removed_count empty profile director(y|ies)"
fi
}
# Main script logic
main() {
if [[ $# -eq 0 ]]; then
show_help
exit 0
fi
local use_insiders=0
local force_new=0
# Parse global options
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help|help)
show_help
exit 0
;;
list|ls)
list_all_profiles
exit 0
;;
list-stable)
list_profiles "$STABLE_BASE_DIR" "Stable profiles"
exit 0
;;
list-insiders)
list_profiles "$INSIDERS_BASE_DIR" "Insiders profiles"
exit 0
;;
remove|rm)
if [[ $# -lt 2 ]]; then
print_error "Profile name required"
exit 1
fi
remove_profile "$2" 0
exit 0
;;
remove-insiders)
if [[ $# -lt 2 ]]; then
print_error "Profile name required"
exit 1
fi
remove_profile "$2" 1
exit 0
;;
path)
if [[ $# -lt 2 ]]; then
print_error "Profile name required"
exit 1
fi
show_profile_path "$2" 0
exit 0
;;
path-insiders)
if [[ $# -lt 2 ]]; then
print_error "Profile name required"
exit 1
fi
show_profile_path "$2" 1
exit 0
;;
clean)
clean_empty_profiles
exit 0
;;
-i|--insiders)
use_insiders=1
shift
;;
-n|--new)
force_new=1
shift
;;
-*)
print_error "Unknown option: $1"
echo
show_help
exit 1
;;
*)
# This is the profile name, break out of option parsing
break
;;
esac
done
# At this point, we should have a profile name
if [[ $# -eq 0 ]]; then
print_error "Profile name required"
echo
show_help
exit 1
fi
local profile_name=$1
shift
# Validate profile name
if [[ ! "$profile_name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
print_error "Invalid profile name. Use only letters, numbers, hyphens, and underscores"
exit 1
fi
# Launch VSCode with the profile
launch_vscode "$profile_name" "$use_insiders" "$@"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment