Postagem

Múltiplas contas do Claude Code no VSCode

Uso Claude Code em projetos de trabalho e em projetos pessoais, na mesma máquina. São contas diferentes. Por muito tempo isso significou deslogar e logar de novo toda vez que eu trocava de contexto — o que é exatamente o tipo de atrito que faz a gente parar de trocar de contexto.

Tentei resolver com uma extensão de VSCode que prometia perfis. Não funcionou, e o motivo do fracasso dela acabou sendo a parte mais interessante da história: eu estava errado sobre onde o Claude Code guarda a sua conta.

Por que a abordagem por extensão quebrou

A extensão fazia o óbvio: mantinha um diretório por perfil e trocava ~/.claude por um symlink apontando para o perfil ativo. O erro que eu recebia era esse:

Failed to create symlink: EEXIST: file already exists,
symlink '/Users/voce/.claude-work' -> '/Users/voce/.claude'

O EEXIST é simples de ler quando você conhece a assinatura do Node: a mensagem é symlink 'destino' -> 'caminho'. O caminho é ~/.claude, que já existia como diretório de verdade, cheio da minha configuração. Não dá pra criar um symlink por cima.

Mas o problema de fundo era maior. Mesmo se o symlink funcionasse, não ia adiantar — porque ~/.claude não é onde a conta mora.

Onde cada coisa realmente fica

O Claude Code espalha estado em três lugares, e confundir os três foi a raiz de toda a minha dificuldade:

~/.claude.json Config e estado. Tem oauthAccount (email e organização), projects (permissões aprovadas, trust dialog, MCP por projeto) e um monte de cache.
~/.claude/ Dados. projects/ com os transcripts das sessões, plugins/, settings.json, memory.
Keychain do macOS O token OAuth. Item Claude Code-credentials.

A extensão symlinkava só o segundo. Ela trocava meus plugins e meu histórico de lugar e não encostava nem no .claude.json nem no Keychain. Ou seja: mesmo funcionando perfeitamente, ela nunca teria trocado a conta.

Vale reforçar, porque é contraintuitivo: o oauthAccount dentro do .claude.json é só metadado de exibição — o nome e o email que aparecem na interface. Apagar aquilo não desloga nada. O token está no Keychain e sobrevive a qualquer faxina que você faça nos arquivos.

A solução: CLAUDE_CONFIG_DIR

Não precisa de extensão nenhuma. O Claude Code tem uma variável de ambiente nativa, CLAUDE_CONFIG_DIR, que realoca a raiz de configuração inteira. Dá pra testar em trinta segundos num diretório descartável:

mkdir /tmp/cfgtest
CLAUDE_CONFIG_DIR=/tmp/cfgtest claude mcp list
# → "No MCP servers configured"   (o meu normal lista 5)
# → e um .claude.json novo aparece dentro de /tmp/cfgtest

Isso confirma o isolamento: com a variável setada, ele não leu absolutamente nada do meu ~. E o mais elegante é o que acontece no Keychain — o Claude deriva o nome do item a partir do config dir. Depois de usar dois perfis, o meu tinha:

Claude Code-credentials            ← ~/.claude (default)
Claude Code-credentials-a1b2c3d4   ← perfil 1
Claude Code-credentials-e5f6a7b8   ← perfil 2

Cada config dir ganha seu próprio item de credencial, com um sufixo de hash. Ou seja: o isolamento de conta é real, e é o próprio Claude Code que faz. Só precisei parar de brigar com symlink e usar a variável.

Os wrappers

No ~/.zshrc, uma função por perfil:

code-work() {
  CLAUDE_CONFIG_DIR="$HOME/.claude-work" \
    code -n --user-data-dir "$HOME/.vscode-work" "$@"
}

code-personal() {
  CLAUDE_CONFIG_DIR="$HOME/.claude-personal" \
    code -n --user-data-dir "$HOME/.vscode-personal" "$@"
}

Aí é code-work ~/Projects/algo-do-trabalho e pronto: a janela abre já logada na conta certa.

O --user-data-dir não é decoração. O VSCode reusa um único processo para todas as janelas. Sem essa flag, abrir um segundo projeto aproveita o processo que já está rodando, e a variável de ambiente que você acabou de setar simplesmente não chega lá — a janela nova herda o ambiente da primeira. O --user-data-dir força um processo separado, e é isso que faz a variável valer.

Por que a janela nova parece vazia

Ao abrir com um --user-data-dir novo, o VSCode aparece do jeito que veio de fábrica: sem seus ajustes, layout no padrão, tudo estranho. A leitura natural é "perdi minhas extensões". Mas não é isso que acontece.

--user-data-dir e --extensions-dir são flags separadas. Você trocou só a primeira, então as extensões continuam vindo de ~/.vscode/extensions, compartilhadas entre todos os perfis. Dá para confirmar nos logs do perfil novo — a extensão do Claude está lá, carregada normalmente.

O que o --user-data-dir isola de verdade:

Então as extensões estão instaladas e rodando, mas com o estado zerado. Se alguma delas guarda login próprio, vai pedir de novo. E as suas configurações do VSCode você copia uma vez:

cp ~/Library/Application\ Support/Code/User/settings.json \
   ~/.vscode-work/User/settings.json

Se quiser mesmo extensões separadas por perfil — instalar coisa de trabalho só no perfil de trabalho — aí sim é --extensions-dir. Eu não quis: prefiro manter uma instalação só e não ficar atualizando extensão em dois lugares.

Começando do zero (sem configurações)

Se a máquina é nova, ou se você ainda não tem nada configurado, é bem menos trabalho do que o resto deste post sugere. Crie os wrappers acima, abra cada perfil uma vez e faça login. Acabou.

Os diretórios se criam sozinhos, o Keychain ganha um item por perfil, e nunca vai existir estado compartilhado para desembaraçar depois. Se puder escolher, comece assim — a seção seguinte é só para quem já tem histórico acumulado num perfil único.

Já tenho projetos: migrando

Quem já usa Claude Code há um tempo tem tudo acumulado num perfil só — no meu caso, 22 projetos. Dividir isso tem duas partes, porque transcript e permissão moram em arquivos diferentes.

Vamos supor quatro projetos, dois de cada lado:

~/Projects/acme-api      → trabalho
~/Projects/acme-web      → trabalho
~/Projects/meu-blog      → pessoal
~/Projects/jogo-2d       → pessoal

1. Entenda o slug

Os transcripts ficam em ~/.claude/projects/, um diretório por projeto. O nome do diretório é o path absoluto com /, . e _ trocados por -:

Path do projeto Diretório do transcript
~/Projects/acme-api -Users-voce-Projects-acme-api
~/Projects/ruby-3.4/loja -Users-voce-Projects-ruby-3-4-loja
~/Projects/app_v2/core -Users-voce-Projects-app-v2-core

Repare nas duas últimas linhas: o ponto do ruby-3.4 e o underscore do app_v2 também viram traço. Se for automatizar, a regra é essa:

slug() { echo "$1" | sed 's/[\/._]/-/g'; }

slug "/Users/voce/Projects/acme-api"
# → -Users-voce-Projects-acme-api

2. Copie os transcripts

Com o slug em mãos, é cp. Copie, não mova — assim o ~/.claude original continua funcionando enquanto você testa:

mkdir -p ~/.claude-work/projects ~/.claude-personal/projects

cp -R ~/.claude/projects/-Users-voce-Projects-acme-api \
      ~/.claude/projects/-Users-voce-Projects-acme-web \
      ~/.claude-work/projects/

cp -R ~/.claude/projects/-Users-voce-Projects-meu-blog \
      ~/.claude/projects/-Users-voce-Projects-jogo-2d \
      ~/.claude-personal/projects/

3. Migre as permissões

Aqui está a parte que é fácil esquecer. Tudo que você já aprovou num projeto — os allowedTools, o trust dialog, os servidores MCP — está na chave projects do ~/.claude.json, indexada pelo path real do projeto:

jq '.projects | keys[]' ~/.claude.json

# "/Users/voce/Projects/acme-api"
# "/Users/voce/Projects/acme-web"
# "/Users/voce/Projects/meu-blog"
# "/Users/voce/Projects/jogo-2d"

Sem migrar isso, o projeto abre no perfil novo e pede aprovação de tudo outra vez. A ideia é gerar um .claude.json por perfil, cada um com o seu subconjunto. Liste os paths de trabalho uma vez:

WORK=(
  "/Users/voce/Projects/acme-api"
  "/Users/voce/Projects/acme-web"
)

O perfil de trabalho fica com os que estão na lista. O --args passa os paths para o jq, que os enxerga em $ARGS.positional:

jq '.projects |= with_entries(
      select(.key as $k | $ARGS.positional | index($k)))' \
   ~/.claude.json --args "${WORK[@]}" \
   > ~/.claude-work/.claude.json

O perfil pessoal fica com todo o resto — mesmo filtro, com | not no fim. Assim você não precisa listar os pessoais, e nenhum projeto fica de fora por esquecimento:

jq '.projects |= with_entries(
      select(.key as $k | $ARGS.positional | index($k) | not))' \
   ~/.claude.json --args "${WORK[@]}" \
   > ~/.claude-personal/.claude.json

Confira se a conta bate antes de seguir:

jq '.projects | length' ~/.claude-work/.claude.json      # 2
jq '.projects | length' ~/.claude-personal/.claude.json  # 2
jq '.projects | length' ~/.claude.json                    # 4, intacto

4. Plugins e settings

Por último, cada perfil precisa dos seus plugins e configurações:

for P in ~/.claude-work ~/.claude-personal; do
  cp -R ~/.claude/plugins "$P/"
  cp ~/.claude/settings.json "$P/"
done

Copiar deixa os perfis independentes: atualizar um plugin no trabalho não mexe no pessoal. O custo é duplicar em disco e atualizar duas vezes. Se preferir o contrário, ln -s ~/.claude/plugins resolve — aí é uma cópia só para os dois.

Repare que o ~/.claude original ficou intacto o tempo todo. Ele não atrapalha em nada e serve de fallback enquanto você confirma que a divisão ficou certa.

Prós e contras

A favor

Contra

Uma última pegadinha

Se você tem ANTHROPIC_API_KEY exportada no shell — coisa comum pra quem mexe com a API ou com ferramentas tipo ai-memory — ela atropela tudo isso. O Claude Code prioriza a API key sobre o OAuth, e aí os dois perfis autenticam pela mesma chave, sem trocar conta nenhuma. Você monta o setup inteiro e ele parece funcionar, mas está separando só o histórico.

Se precisar da variável pra outra coisa, escope ela onde é usada em vez de exportar global.

← Voltar para postagens