Last active
April 23, 2026 06:26
-
-
Save gmotzespina/2064a4768eca162a36735567475e7c37 to your computer and use it in GitHub Desktop.
Claude Launcher for Offline Usage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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