From 68c17671b07c061dd4b631f4653f9ca62a46defd Mon Sep 17 00:00:00 2001 From: jesse23 Date: Mon, 6 Apr 2026 18:23:25 -0400 Subject: [PATCH 1/2] fix(account): restore display name and org on switch When switching accounts, ccm only swapped the Keychain/credential token but left ~/.claude.json's oauthAccount untouched, causing Claude Code to show the previous account's display name, org, and email after restart. Fixes: https://github.com/foreveryh/claude-code-switch/issues/25 Changes: - Add read_oauth_account() / write_oauth_account() helpers to read and write the oauthAccount block in ~/.claude.json - save-account now also saves oauthAccount as __oauth alongside the credential entry, and prints the display name on save for confirmation - switch-account now restores oauthAccount to ~/.claude.json so display name and org update correctly after restart - switch-account warns upfront if the saved token is expired Co-Authored-By: Claude Sonnet 4.6 --- ccm.sh | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/ccm.sh b/ccm.sh index ccded74..9c4860c 100755 --- a/ccm.sh +++ b/ccm.sh @@ -1108,6 +1108,47 @@ init_accounts_file() { fi } +# Read oauthAccount JSON from ~/.claude.json +read_oauth_account() { + local claude_json="$HOME/.claude.json" + if [[ -f "$claude_json" ]]; then + python3 -c " +import json, sys +try: + d = json.load(open('$claude_json')) + oa = d.get('oauthAccount') + if oa: + print(json.dumps(oa)) +except Exception: + pass +" 2>/dev/null + fi +} + +# Write oauthAccount JSON back into ~/.claude.json +write_oauth_account() { + local oauth_json="$1" + local claude_json="$HOME/.claude.json" + if [[ -f "$claude_json" && -n "$oauth_json" ]]; then + local temp_file + temp_file=$(mktemp) + echo "$oauth_json" > "$temp_file" + python3 -c " +import json +with open('$claude_json', 'r') as f: + d = json.load(f) +with open('$temp_file', 'r') as f: + d['oauthAccount'] = json.load(f) +with open('$claude_json', 'w') as f: + json.dump(d, f, indent=2) +" 2>/dev/null + local result=$? + rm -f "$temp_file" + return $result + fi + return 1 +} + # 保存当前账号 save_account() { # 检查是否需要禁用颜色(用于 eval) @@ -1177,12 +1218,38 @@ EOF fi fi + # Also save oauthAccount from ~/.claude.json (display name, org, email) + local oauth_data + oauth_data=$(read_oauth_account) + if [[ -n "$oauth_data" ]]; then + local oauth_key="${account_name}__oauth" + local encoded_oauth + encoded_oauth=$(echo "$oauth_data" | base64_encode_nolinebreak) + if grep -q "\"$oauth_key\":" "$ACCOUNTS_FILE" 2>/dev/null; then + if [[ "$OS_TYPE" == "macos" ]]; then + sed -i '' "s|\"$oauth_key\": *\"[^\"]*\"|\"$oauth_key\": \"$encoded_oauth\"|" "$ACCOUNTS_FILE" + else + sed -i "s|\"$oauth_key\": *\"[^\"]*\"|\"$oauth_key\": \"$encoded_oauth\"|" "$ACCOUNTS_FILE" + fi + else + local temp_oauth + temp_oauth=$(mktemp) + sed '$d' "$ACCOUNTS_FILE" | sed '$s/$/,/' > "$temp_oauth" + echo " \"$oauth_key\": \"$encoded_oauth\"" >> "$temp_oauth" + echo "}" >> "$temp_oauth" + mv "$temp_oauth" "$ACCOUNTS_FILE" + fi + fi + chmod 600 "$ACCOUNTS_FILE" # 提取订阅类型用于显示 local subscription_type=$(echo "$credentials" | grep -o '"subscriptionType":"[^"]*"' | cut -d'"' -f4) + local display_name + display_name=$(echo "$oauth_data" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('displayName') or d.get('emailAddress',''))" 2>/dev/null || echo "") echo -e "${GREEN}✅ $(t 'account_saved'): $account_name${NC}" echo -e " $(t 'subscription_type'): ${subscription_type:-Unknown}" + [[ -n "$display_name" ]] && echo -e " Display name: ${display_name}" rm -f "$temp_file" } @@ -1219,9 +1286,37 @@ switch_account() { # 解码凭证 local credentials=$(echo "$encoded_creds" | base64_decode) + # Warn if token is already expired + local expires + expires=$(echo "$credentials" | grep -o '"expiresAt":[0-9]*' | cut -d':' -f2) + if [[ -n "$expires" ]]; then + local now_ms + now_ms=$(python3 -c "import time; print(int(time.time() * 1000))" 2>/dev/null || echo "0") + if [[ "$now_ms" != "0" && "$expires" -lt "$now_ms" ]]; then + echo -e "${YELLOW}⚠️ Token for '$account_name' is expired — run 'claude login' after switching.${NC}" + fi + fi + # 写入 Keychain if write_keychain_credentials "$credentials"; then echo -e "${GREEN}✅ $(t 'account_switched'): $account_name${NC}" + + # Restore oauthAccount to ~/.claude.json for correct display name & org + local oauth_key="${account_name}__oauth" + local encoded_oauth + encoded_oauth=$(grep -o "\"$oauth_key\": *\"[^\"]*\"" "$ACCOUNTS_FILE" 2>/dev/null | cut -d'"' -f4) + if [[ -n "$encoded_oauth" ]]; then + local oauth_data + oauth_data=$(echo "$encoded_oauth" | base64_decode) + if write_oauth_account "$oauth_data"; then + local display_name + display_name=$(echo "$oauth_data" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('displayName') or d.get('emailAddress',''))" 2>/dev/null || echo "") + [[ -n "$display_name" ]] && echo -e " ${GREEN}✓ Display name updated: ${display_name}${NC}" + else + echo -e " ${YELLOW}⚠️ Could not update display info in ~/.claude.json${NC}" + fi + fi + echo -e "${YELLOW}⚠️ $(t 'please_restart_claude_code')${NC}" else echo -e "${RED}❌ $(t 'failed_to_switch_account')${NC}" >&2 From e754de23fbf61ef0875a3cdbe449dda1240dcfc1 Mon Sep 17 00:00:00 2001 From: jesse23 Date: Mon, 6 Apr 2026 19:56:17 -0400 Subject: [PATCH 2/2] fix(account): use python3 for oauth JSON to avoid sed corruption The sed-based append to ~/.ccm_accounts could produce double commas (,,) when the last entry already had a trailing comma, corrupting the JSON file and breaking list-accounts. Replace sed/grep manipulation of the __oauth key with python3 json module, which handles reads and writes atomically and correctly regardless of file state or base64 content. Co-Authored-By: Claude Sonnet 4.6 --- ccm.sh | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/ccm.sh b/ccm.sh index 9c4860c..d96d246 100755 --- a/ccm.sh +++ b/ccm.sh @@ -1225,20 +1225,19 @@ EOF local oauth_key="${account_name}__oauth" local encoded_oauth encoded_oauth=$(echo "$oauth_data" | base64_encode_nolinebreak) - if grep -q "\"$oauth_key\":" "$ACCOUNTS_FILE" 2>/dev/null; then - if [[ "$OS_TYPE" == "macos" ]]; then - sed -i '' "s|\"$oauth_key\": *\"[^\"]*\"|\"$oauth_key\": \"$encoded_oauth\"|" "$ACCOUNTS_FILE" - else - sed -i "s|\"$oauth_key\": *\"[^\"]*\"|\"$oauth_key\": \"$encoded_oauth\"|" "$ACCOUNTS_FILE" - fi - else - local temp_oauth - temp_oauth=$(mktemp) - sed '$d' "$ACCOUNTS_FILE" | sed '$s/$/,/' > "$temp_oauth" - echo " \"$oauth_key\": \"$encoded_oauth\"" >> "$temp_oauth" - echo "}" >> "$temp_oauth" - mv "$temp_oauth" "$ACCOUNTS_FILE" - fi + CCM_OAUTH_KEY="$oauth_key" CCM_ENCODED_OAUTH="$encoded_oauth" CCM_ACCOUNTS_FILE="$ACCOUNTS_FILE" \ + python3 -c " +import json, os +key = os.environ['CCM_OAUTH_KEY'] +encoded = os.environ['CCM_ENCODED_OAUTH'] +path = os.environ['CCM_ACCOUNTS_FILE'] +with open(path, 'r') as f: + accounts = json.load(f) +accounts[key] = encoded +with open(path, 'w') as f: + json.dump(accounts, f, indent=2) + f.write('\n') +" 2>/dev/null || echo -e " ${YELLOW}⚠️ Could not save display info${NC}" fi chmod 600 "$ACCOUNTS_FILE" @@ -1303,11 +1302,21 @@ switch_account() { # Restore oauthAccount to ~/.claude.json for correct display name & org local oauth_key="${account_name}__oauth" - local encoded_oauth - encoded_oauth=$(grep -o "\"$oauth_key\": *\"[^\"]*\"" "$ACCOUNTS_FILE" 2>/dev/null | cut -d'"' -f4) - if [[ -n "$encoded_oauth" ]]; then - local oauth_data - oauth_data=$(echo "$encoded_oauth" | base64_decode) + local oauth_data + oauth_data=$(CCM_OAUTH_KEY="$oauth_key" CCM_ACCOUNTS_FILE="$ACCOUNTS_FILE" python3 -c " +import json, os, base64 +key = os.environ['CCM_OAUTH_KEY'] +path = os.environ['CCM_ACCOUNTS_FILE'] +try: + with open(path, 'r') as f: + accounts = json.load(f) + encoded = accounts.get(key, '') + if encoded: + print(base64.b64decode(encoded).decode()) +except Exception: + pass +" 2>/dev/null) + if [[ -n "$oauth_data" ]]; then if write_oauth_account "$oauth_data"; then local display_name display_name=$(echo "$oauth_data" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('displayName') or d.get('emailAddress',''))" 2>/dev/null || echo "")