From f3d1b6d5c50cc5adec9c84dd9cc495629e134e54 Mon Sep 17 00:00:00 2001 From: vitaly Date: Fri, 12 Jun 2026 09:06:44 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=BE=D0=BF=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20Claude-=D0=B0=D0=BA=D0=BA?= =?UTF-8?q?=D0=B0=D1=83=D0=BD=D1=82=D0=B0=20=D0=BF=D0=BE=20=D1=82=D0=BE?= =?UTF-8?q?=D0=BA=D0=B5=D0=BD=D1=83=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=20auth=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Корень багов с потерей токенов: claude auth status читает oauthAccount.emailAddress из ~/.claude.json, который рассинхронизирован с реальным токеном в .credentials.json. Из-за этого хуки определяли текущий аккаунт неверно и сохраняли активный токен под чужим именем, затирая credentials другого аккаунта. - account-email.sh (новый): определяет email по OAuth-токену — локальный матчинг с accounts/, затем API /api/oauth/profile - switch-account-hook.sh: current выводится из токена, а не из auth status/хрупкого файла current — порча файлов исключена. Перезапуск не нужен: на Linux Claude Code перечитывает .credentials.json на лету - add-account-hook.sh: email нового аккаунта тоже через хелпер - skill add-account: убрано упоминание перезапуска - ai-setup.sh: деплой account-email.sh (секция 6.7.05) Co-Authored-By: Claude Opus 4.8 --- home-configs/claude/hooks/account-email.sh | 33 ++++++++++++++ home-configs/claude/hooks/add-account-hook.sh | 14 +++--- .../claude/hooks/switch-account-hook.sh | 43 +++++++++---------- .../claude/skills/add-account/SKILL.md | 2 +- scripts/ai-setup.sh | 11 +++++ 5 files changed, 74 insertions(+), 29 deletions(-) create mode 100644 home-configs/claude/hooks/account-email.sh diff --git a/home-configs/claude/hooks/account-email.sh b/home-configs/claude/hooks/account-email.sh new file mode 100644 index 0000000..0c090d6 --- /dev/null +++ b/home-configs/claude/hooks/account-email.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Определяет email Claude.ai аккаунта по OAuth-токену в credentials-файле. +# Источник истины — сам токен (НЕ claude auth status, который читает +# рассинхронизированный oauthAccount из ~/.claude.json). +# Сначала локальный матчинг с сохранёнными accounts/, затем API /api/oauth/profile. +# Использование: account-email.sh [credentials-file] (по умолчанию ~/.claude/.credentials.json) + +CREDS="${1:-$HOME/.claude/.credentials.json}" +ACCOUNTS_DIR="$HOME/.claude/accounts" + +[ -f "$CREDS" ] || exit 0 +token=$(jq -r '.claudeAiOauth.accessToken // empty' "$CREDS" 2>/dev/null) +[ -z "$token" ] && exit 0 + +# 1) Локальный матчинг по токену с сохранёнными аккаунтами (мгновенно) +if [ -d "$ACCOUNTS_DIR" ]; then + for f in "$ACCOUNTS_DIR"/*.credentials.json; do + [ -f "$f" ] || continue + t=$(jq -r '.claudeAiOauth.accessToken // empty' "$f" 2>/dev/null) + if [ -n "$t" ] && [ "$t" = "$token" ]; then + basename "$f" .credentials.json + exit 0 + fi + done +fi + +# 2) Достоверно через API (по реальному владельцу токена) +email=$(curl -s --max-time 15 "https://api.anthropic.com/api/oauth/profile" \ + -H "Authorization: Bearer $token" \ + -H "anthropic-beta: oauth-2025-04-20" 2>/dev/null \ + | jq -r '.account.email // empty' 2>/dev/null) +[ -n "$email" ] && echo "$email" +exit 0 diff --git a/home-configs/claude/hooks/add-account-hook.sh b/home-configs/claude/hooks/add-account-hook.sh index ad9c3fc..0e06468 100755 --- a/home-configs/claude/hooks/add-account-hook.sh +++ b/home-configs/claude/hooks/add-account-hook.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # UserPromptSubmit hook: перехватывает /add-account. -# 1) сохраняет текущий аккаунт по его реальному email (claude auth status) +# 1) сохраняет текущий аккаунт по его реальному email (account-email.sh) # 2) запускает oauth-логин в фоне (открывает браузер) -# 3) после логина фоновый процесс сам сохраняет новый аккаунт и делает его current +# 3) после логина фоновый процесс сам определяет email нового аккаунта по токену +# и сохраняет его credentials + делает current input=$(cat) prompt=$(echo "$input" | jq -r '.user_prompt // .prompt // empty' 2>/dev/null) @@ -13,12 +14,13 @@ normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||') CREDS="$HOME/.claude/.credentials.json" ACCOUNTS_DIR="$HOME/.claude/accounts" CURRENT_FILE="$ACCOUNTS_DIR/current" +EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh" mkdir -p "$ACCOUNTS_DIR" -# Сохраняем текущий активный аккаунт под его реальным email (источник истины — auth status) +# Сохраняем текущий активный аккаунт под его реальным email (по токену) if [ -f "$CREDS" ]; then - cur_email=$(claude auth status 2>/dev/null | jq -r '.email // empty' 2>/dev/null) + cur_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null) if [ -n "$cur_email" ]; then cp "$CREDS" "$ACCOUNTS_DIR/${cur_email}.credentials.json" chmod 600 "$ACCOUNTS_DIR/${cur_email}.credentials.json" @@ -28,10 +30,10 @@ fi # Фоновый процесс: логин нового аккаунта + автосохранение после успеха. # claude auth login ждёт авторизации в браузере и завершается после неё, -# затем мы читаем новый email и сохраняем credentials под ним. +# затем определяем email нового аккаунта по токену (через API) и сохраняем. ( claude auth login --claudeai /tmp/claude-add-account.log 2>&1 - new_email=$(claude auth status 2>/dev/null | jq -r '.email // empty' 2>/dev/null) + new_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null) if [ -n "$new_email" ] && [ -f "$CREDS" ]; then cp "$CREDS" "$ACCOUNTS_DIR/${new_email}.credentials.json" chmod 600 "$ACCOUNTS_DIR/${new_email}.credentials.json" diff --git a/home-configs/claude/hooks/switch-account-hook.sh b/home-configs/claude/hooks/switch-account-hook.sh index eb44d21..d1bd93b 100755 --- a/home-configs/claude/hooks/switch-account-hook.sh +++ b/home-configs/claude/hooks/switch-account-hook.sh @@ -1,36 +1,44 @@ #!/usr/bin/env bash -# UserPromptSubmit hook: перехватывает /switch-account и обновляет аккаунт. -# Используем exit 0 (не exit 2) чтобы Claude ответил - это единственный способ -# обновить статусную строку, т.к. Claude Code перерисовывает её только после ответа LLM. +# UserPromptSubmit hook: перехватывает /switch-account и циклически меняет аккаунт. +# Текущий аккаунт определяется ПО ТОКЕНУ в .credentials.json (account-email.sh), +# а не по claude auth status — он читает рассинхронизированный oauthAccount. +# На Linux Claude Code перечитывает .credentials.json на лету: новый аккаунт +# применяется со следующего сообщения, перезапуск не нужен. +# exit 0 (не exit 2): /switch-account доходит до Claude, грузится скилл, +# отвечает "✓" (1 токен) — так перерисовывается статусная строка. input=$(cat) prompt=$(echo "$input" | jq -r '.user_prompt // .prompt // empty' 2>/dev/null) - -# Нормализуем: убираем пробелы и слэш в начале normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||') [ "$normalized" != "switch-account" ] && exit 0 -# --- Переключаем аккаунт --- ACCOUNTS_DIR="$HOME/.claude/accounts" CREDS="$HOME/.claude/.credentials.json" CURRENT_FILE="$ACCOUNTS_DIR/current" +EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh" mkdir -p "$ACCOUNTS_DIR" +# Реальный текущий аккаунт — по токену активной сессии (не по хрупкому current) +current=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null) +[ -z "$current" ] && current=$(cat "$CURRENT_FILE" 2>/dev/null) + +# Сохранить актуальные (возможно обновлённые рефрешем) токены под реальным email. +# current выведен из самого токена — порча файла другого аккаунта исключена. +if [ -n "$current" ] && [ -f "$CREDS" ]; then + cp "$CREDS" "$ACCOUNTS_DIR/${current}.credentials.json" + chmod 600 "$ACCOUNTS_DIR/${current}.credentials.json" +fi + mapfile -t accounts < <(ls "$ACCOUNTS_DIR"/*.credentials.json 2>/dev/null \ | xargs -I{} basename {} .credentials.json | sort) -if [ ${#accounts[@]} -eq 0 ]; then - echo "Аккаунты не настроены. Создай ~/.claude/accounts/.credentials.json для каждого аккаунта." >&2 +if [ ${#accounts[@]} -le 1 ]; then + echo "Только один аккаунт (${current:-нет}). Добавь второй через /add-account." >&2 exit 2 fi -# Реальный активный аккаунт — источник истины claude auth status (а не хрупкий -# файл current). Это защищает от порчи сохранённых credentials при рассинхроне. -current=$(claude auth status 2>/dev/null | jq -r '.email // empty' 2>/dev/null) -[ -z "$current" ] && current=$(cat "$CURRENT_FILE" 2>/dev/null || echo "") - # Найти следующий по кругу idx=-1 for i in "${!accounts[@]}"; do @@ -39,17 +47,8 @@ done next_idx=$(( (idx + 1) % ${#accounts[@]} )) next="${accounts[$next_idx]}" -# Сохранить текущие (возможно обновлённые Claude Code) токены обратно в файл аккаунта -if [ -n "$current" ] && [ -f "$CREDS" ]; then - cp "$CREDS" "$ACCOUNTS_DIR/${current}.credentials.json" - chmod 600 "$ACCOUNTS_DIR/${current}.credentials.json" -fi - cp "$ACCOUNTS_DIR/${next}.credentials.json" "$CREDS" chmod 600 "$CREDS" echo "$next" > "$CURRENT_FILE" -# exit 0 (не exit 2): оригинальный /switch-account доходит до Claude, -# Claude загружает скилл switch-account, скилл велит ответить только "✓" (1 токен). -# Это единственный способ обновить статусную строку без лишних токенов. exit 0 diff --git a/home-configs/claude/skills/add-account/SKILL.md b/home-configs/claude/skills/add-account/SKILL.md index 415fd1f..7861c29 100644 --- a/home-configs/claude/skills/add-account/SKILL.md +++ b/home-configs/claude/skills/add-account/SKILL.md @@ -5,4 +5,4 @@ description: Add a new Claude.ai account (handled by UserPromptSubmit hook, no L Хук сохранил текущий аккаунт и открыл браузер для логина нового. Ответь ТОЛЬКО этим текстом (без markdown, без лишних слов): -Браузер открыт — авторизуйся там. После авторизации новый аккаунт сохранится автоматически (никаких ручных шагов). Затем перезапусти ai-claude — он подхватит новый аккаунт, и /switch-account будет переключать между всеми. +Браузер открыт — авторизуйся там. После авторизации новый аккаунт сохранится автоматически и сразу станет активным (Claude Code на Linux перечитывает токен на лету). /switch-account переключает между всеми сохранёнными аккаунтами по кругу. diff --git a/scripts/ai-setup.sh b/scripts/ai-setup.sh index bf8e4fb..d93d833 100755 --- a/scripts/ai-setup.sh +++ b/scripts/ai-setup.sh @@ -717,6 +717,17 @@ if isinstance(stop, list): PYEOF success "Старый хук effort-save удалён" +# ── 6.7.05. Хелпер account-email (определение email по токену) ── +# Вспомогательный скрипт для хуков switch-account/add-account. +# Не регистрируется в settings.json — вызывается из хуков напрямую. +EMAIL_HELPER_SRC="$SCRIPT_DIR/home-configs/claude/hooks/account-email.sh" +EMAIL_HELPER_DST="$HOME/.claude/hooks/account-email.sh" +mkdir -p "$HOME/.claude/hooks" +if [ -f "$EMAIL_HELPER_SRC" ]; then + cp "$EMAIL_HELPER_SRC" "$EMAIL_HELPER_DST" + chmod +x "$EMAIL_HELPER_DST" +fi + # ── 6.7.1. Хук switch-account ─────────────────────────────────── info "Деплою хук switch-account..." SWITCH_HOOK_SRC="$SCRIPT_DIR/home-configs/claude/hooks/switch-account-hook.sh"