Skip to content

Instantly share code, notes, and snippets.

@gmotzespina
Last active April 23, 2026 06:26
Show Gist options
  • Select an option

  • Save gmotzespina/2064a4768eca162a36735567475e7c37 to your computer and use it in GitHub Desktop.

Select an option

Save gmotzespina/2064a4768eca162a36735567475e7c37 to your computer and use it in GitHub Desktop.
Claude Launcher for Offline Usage
#!/bin/bash
# claude-launcher.sh
# Smart launcher for Claude Code — switch between local (LM Studio) and cloud (Anthropic) modes.
#
# Usage:
# ./claude-launcher.sh Interactive menu
# ./claude-launcher.sh local Launch with LM Studio
# ./claude-launcher.sh cloud Launch with Anthropic account
# ./claude-launcher.sh status Show current config & LM Studio status
set -euo pipefail
# ──────────────────────────────────────────────
# Configuration — edit these to match your setup
# ──────────────────────────────────────────────
LM_STUDIO_URL="http://localhost:1234"
LM_STUDIO_API_KEY="lm-studio"
LOCAL_MODEL="" # leave empty to use whatever LM Studio is serving, or set e.g. "glm-4.7-flash"
CLAUDE_JSON="$HOME/.claude.json"
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
# ──────────────────────────────────────────────
# Colors
# ──────────────────────────────────────────────
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# ──────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────
ensure_onboarding_done() {
# Make sure Claude Code doesn't show the login screen
if [[ ! -f "$CLAUDE_JSON" ]]; then
echo '{"hasCompletedOnboarding": true}' > "$CLAUDE_JSON"
elif ! grep -q '"hasCompletedOnboarding"' "$CLAUDE_JSON" 2>/dev/null; then
# Add the flag to existing file using a temp file (portable, no jq dependency)
local tmp
tmp=$(mktemp)
# If valid JSON object, inject the key; otherwise overwrite
if python3 -c "
import json, sys
with open('$CLAUDE_JSON') as f:
d = json.load(f)
d['hasCompletedOnboarding'] = True
with open('$tmp', 'w') as f:
json.dump(d, f, indent=2)
" 2>/dev/null; then
mv "$tmp" "$CLAUDE_JSON"
else
rm -f "$tmp"
echo '{"hasCompletedOnboarding": true}' > "$CLAUDE_JSON"
fi
fi
}
ensure_settings_dir() {
mkdir -p "$HOME/.claude"
}
check_lm_studio() {
# Returns 0 if LM Studio is responding, 1 otherwise
if curl -s --connect-timeout 2 "$LM_STUDIO_URL/v1/models" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
get_lm_studio_models() {
# Get loaded LLM model names from LM Studio
local response
response=$(curl -s --connect-timeout 2 "$LM_STUDIO_URL/api/v0/models" 2>/dev/null) || return 1
python3 -c "
import json
try:
data = json.loads('''$response''')
for m in data.get('data', []):
if m.get('state') == 'loaded' and m.get('type') == 'llm':
print(m.get('id', 'unknown'))
except:
pass
" 2>/dev/null
}
pick_lm_studio_model() {
local models=()
while IFS= read -r line; do
[[ -n "$line" ]] && models+=("$line")
done < <(get_lm_studio_models)
if [[ ${#models[@]} -eq 0 ]]; then
echo "no model loaded"
return 1
elif [[ ${#models[@]} -eq 1 ]]; then
echo "${models[0]}"
else
echo -e "\n ${BOLD}Available LM Studio models:${NC}" >&2
for i in "${!models[@]}"; do
echo -e " ${BOLD}$((i+1)))${NC} ${models[$i]}" >&2
done
echo "" >&2
read -rp " Choose model [1-${#models[@]}]: " choice </dev/tty
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#models[@]} )); then
echo "${models[$((choice-1))]}"
else
echo -e " ${RED}Invalid choice, using first model.${NC}" >&2
echo "${models[0]}"
fi
fi
}
# ──────────────────────────────────────────────
# Launch modes
# ──────────────────────────────────────────────
launch_local() {
echo -e "${BLUE}${BOLD}🖥 Local Mode (LM Studio)${NC}"
echo ""
# Check if LM Studio is running
if ! check_lm_studio; then
echo -e "${RED}✗ LM Studio is not responding at ${LM_STUDIO_URL}${NC}"
echo -e " Make sure LM Studio is running and the server is started."
echo ""
read -rp "Wait and retry? (y/n) " choice
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
echo -e " Waiting for LM Studio..."
for i in {1..30}; do
if check_lm_studio; then
echo -e " ${GREEN}✓ Connected!${NC}"
break
fi
sleep 2
printf "."
done
echo ""
if ! check_lm_studio; then
echo -e "${RED} Timed out. Please start LM Studio and try again.${NC}"
exit 1
fi
else
exit 1
fi
fi
echo -e "${GREEN}✓ LM Studio is running${NC}"
echo ""
ensure_onboarding_done
ensure_settings_dir
# Create the apiKeyHelper script — this is what actually bypasses the login screen
local key_helper="$HOME/.claude/api-key-helper.sh"
cat > "$key_helper" << EOF
#!/bin/bash
echo "$LM_STUDIO_API_KEY"
EOF
chmod +x "$key_helper"
# Set env vars for this session
export ANTHROPIC_BASE_URL="$LM_STUDIO_URL"
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
# Do NOT set ANTHROPIC_API_KEY — it conflicts with apiKeyHelper
unset ANTHROPIC_API_KEY 2>/dev/null || true
# Set model — use LOCAL_MODEL if pinned, otherwise let user pick from LM Studio
if [[ -n "$LOCAL_MODEL" ]]; then
export ANTHROPIC_MODEL="$LOCAL_MODEL"
else
local model_name
model_name=$(pick_lm_studio_model)
export ANTHROPIC_MODEL="$model_name"
fi
echo -e " Model: ${BOLD}${ANTHROPIC_MODEL}${NC}"
# Write settings with apiKeyHelper + attribution header fix
python3 -c "
import json, os
path = '$CLAUDE_SETTINGS'
d = {}
if os.path.exists(path):
try:
with open(path) as f:
d = json.load(f)
except:
pass
# Set apiKeyHelper to bypass login
d['apiKeyHelper'] = '$key_helper'
# Set attribution header fix for KV cache performance
env = d.setdefault('env', {})
env['CLAUDE_CODE_ATTRIBUTION_HEADER'] = '0'
with open(path, 'w') as f:
json.dump(d, f, indent=2)
" 2>/dev/null
echo -e " ${GREEN}Launching Claude Code → LM Studio${NC}"
echo -e " ─────────────────────────────────"
echo ""
exec claude "$@"
}
launch_cloud() {
echo -e "${BLUE}${BOLD}☁️ Cloud Mode (Anthropic)${NC}"
echo ""
# Clear any local overrides
unset ANTHROPIC_BASE_URL 2>/dev/null || true
unset ANTHROPIC_API_KEY 2>/dev/null || true
unset ANTHROPIC_MODEL 2>/dev/null || true
unset CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 2>/dev/null || true
# Remove apiKeyHelper + attribution header from settings so Claude uses normal OAuth
if [[ -f "$CLAUDE_SETTINGS" ]]; then
python3 -c "
import json
with open('$CLAUDE_SETTINGS') as f:
d = json.load(f)
# Remove local-mode keys
d.pop('apiKeyHelper', None)
env = d.get('env', {})
env.pop('CLAUDE_CODE_ATTRIBUTION_HEADER', None)
if not env:
d.pop('env', None)
with open('$CLAUDE_SETTINGS', 'w') as f:
json.dump(d, f, indent=2)
" 2>/dev/null
fi
# Clean up the helper script
rm -f "$HOME/.claude/api-key-helper.sh"
echo -e " ${GREEN}Launching Claude Code → Anthropic API${NC}"
echo -e " ─────────────────────────────────────"
echo ""
exec claude "$@"
}
show_status() {
echo -e "${BLUE}${BOLD}📊 Claude Code Launcher — Status${NC}"
echo ""
# LM Studio
if check_lm_studio; then
local lm_models=()
while IFS= read -r line; do
[[ -n "$line" ]] && lm_models+=("$line")
done < <(get_lm_studio_models)
echo -e " LM Studio: ${GREEN}● running${NC} at ${LM_STUDIO_URL} — ${#lm_models[@]} model(s): ${BOLD}${lm_models[*]}${NC}"
else
echo -e " LM Studio: ${RED}● not running${NC} at ${LM_STUDIO_URL}"
fi
# Current env
if [[ -n "${ANTHROPIC_BASE_URL:-}" ]]; then
echo -e " Base URL: ${YELLOW}${ANTHROPIC_BASE_URL}${NC}"
else
echo -e " Base URL: ${GREEN}Anthropic default (cloud)${NC}"
fi
# Onboarding flag
if [[ -f "$CLAUDE_JSON" ]] && grep -q '"hasCompletedOnboarding": true' "$CLAUDE_JSON" 2>/dev/null; then
echo -e " Onboarding: ${GREEN}✓ bypassed${NC}"
else
echo -e " Onboarding: ${YELLOW}not set (may show login screen)${NC}"
fi
# apiKeyHelper
if [[ -f "$CLAUDE_SETTINGS" ]] && grep -q '"apiKeyHelper"' "$CLAUDE_SETTINGS" 2>/dev/null; then
echo -e " Auth: ${YELLOW}apiKeyHelper (local mode)${NC}"
else
echo -e " Auth: ${GREEN}OAuth / Anthropic account${NC}"
fi
# Settings
if [[ -f "$CLAUDE_SETTINGS" ]]; then
echo -e " Settings: ${GREEN}✓ exists${NC} at ${CLAUDE_SETTINGS}"
else
echo -e " Settings: ${YELLOW}not found${NC}"
fi
echo ""
}
show_menu() {
echo ""
echo -e "${BOLD}┌─────────────────────────────────────┐${NC}"
echo -e "${BOLD}│ Claude Code Launcher 🚀 │${NC}"
echo -e "${BOLD}└─────────────────────────────────────┘${NC}"
echo ""
# Quick status line
if check_lm_studio; then
local lm_models=()
while IFS= read -r line; do
[[ -n "$line" ]] && lm_models+=("$line")
done < <(get_lm_studio_models)
echo -e " LM Studio: ${GREEN}● online${NC} — ${#lm_models[@]} model(s): ${lm_models[*]}"
else
echo -e " LM Studio: ${RED}● offline${NC}"
fi
echo ""
echo -e " ${BOLD}1)${NC} 🖥 Local mode (LM Studio)"
echo -e " ${BOLD}2)${NC} ☁️ Cloud mode (Anthropic)"
echo -e " ${BOLD}3)${NC} 📊 Status"
echo -e " ${BOLD}q)${NC} Exit"
echo ""
read -rp " Choose [1/2/3/q]: " choice
case "$choice" in
1) launch_local ;;
2) launch_cloud ;;
3) show_status ;;
q|Q) exit 0 ;;
*) echo -e " ${RED}Invalid choice${NC}"; show_menu ;;
esac
}
# ──────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────
case "${1:-}" in
local) shift; launch_local "$@" ;;
cloud) shift; launch_cloud "$@" ;;
status) show_status ;;
help|-h|--help)
echo "Usage: $(basename "$0") [local|cloud|status|help]"
echo ""
echo " local Launch Claude Code with LM Studio"
echo " cloud Launch Claude Code with Anthropic account"
echo " status Show configuration and LM Studio status"
echo " (none) Interactive menu"
;;
*) show_menu ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment