pass + GPG: injetando API keys no terminal sem export nem histórico vazado
Parei de dar export das minhas API keys e de ver sk-ant-... no histórico do shell. Uso pass + GPG e injeto o segredo direto no comando com $(pass anthropic/oauth-token). Este é o guia direto de como faço.
Há uns meses abri o ~/.zshrc para ajustar uma coisa boba e dei de cara com umas quatro export ANTHROPIC_API_KEY=sk-ant-... esquecidas ali, em texto puro. Rodei um history | grep sk- e, cara, o histórico do shell estava lotado de segredo. Chave de API, token de OAuth, senha de banco de dev. Tudo legível.
É engraçado, mas é sério: eu passo o dia falando de qualidade e de risco, e meus segredos estavam mais expostos que post no LinkedIn.
Resolvi arrumar aquilo com pass e GPG. Desde então virou o jeito padrão que uso para rodar o Claude Code e outros CLIs que pedem chave. Vou te contar como funciona e — sendo honesto — o que esse método não resolve.
Bora lá.
O problema: seu segredo mora em texto puro em três lugares
Quando você faz export ANTHROPIC_API_KEY=sk-ant-..., o valor literal da chave vaza para pelo menos três lugares:
- O histórico do shell — a linha inteira, com a chave, vai para o
~/.zsh_historyou~/.bash_history. Fica lá. Em texto puro. - O ambiente da sessão inteira — a variável exportada é herdada por todo processo filho que você abrir naquele terminal, do começo ao fim da sessão.
- Seus dotfiles — se você colocou o
exportno.zshrc/.bashrcpara “não digitar de novo”, agora a chave está num arquivo que, com um descuido, sobe para o Git.
Quero destacar que não é paranoia. É o famigerado exemplo do backup que ninguém revisa: o dia que você compartilha a tela, cola um .env no chat errado ou publica um dotfile, a chave vaza junto. É melhor prevenir do que remediar.
Entra o pass (e o GPG por baixo)
O pass se autodenomina “the standard unix password manager”. A ideia é simples ao ponto de ser bonita: cada segredo é um arquivo criptografado com GPG, guardado numa arvorezinha de pastas em ~/.password-store/.
- Nada de banco de dados, nada de app pesado.
- Cada entrada é um
.gpg. Sem sua chave privada, é só ruído. - A hierarquia é livre:
anthropic/oauth-token,bancos/dev/postgres,github/token-ci.
O GPG é o carinha que faz o trabalho pesado de cripto. O pass é uma casca fina de shell script por cima dele. Gosto justamente disso: é explícito, sem mágica. Dá para ler o script e entender tudo.
Setup em 4 comandos
Assumindo que você já tem uma chave GPG (se não tiver, gpg --full-generate-key e siga o passo a passo — escolhe RSA 4096 e uma boa passphrase).
Primeiro, descubra o ID da sua chave:
gpg --list-secret-keys --keyid-format=longAgora inicialize o pass apontando para ela (pode usar o e-mail associado à chave no lugar do ID):
Guarde seu primeiro segredo. O insert abre um prompt que não ecoa o que você digita — nada de segredo aparecendo na tela:
pass insert anthropic/oauth-tokenE recupere quando precisar:
pass anthropic/oauth-tokenEsse último comando imprime a primeira linha da entrada no stdout. É exatamente esse comportamento que vamos usar para resolver o problema do export.
O pulo do gato: $(pass ...) em vez de export
Aqui está a mudança que faz diferença no dia a dia. Em vez de exportar a chave e deixá-la solta na sessão, você injeta o valor só naquele comando, na hora:
CLAUDE_CODE_OAUTH_TOKEN=$(pass anthropic/oauth-token) claudeUm parêntese que importa: aqui eu guardo um OAuth token (aquele que você gera com claude setup-token), então a variável certa é CLAUDE_CODE_OAUTH_TOKEN. Se o seu segredo é uma API key de verdade (sk-ant-...), aí sim o nome é ANTHROPIC_API_KEY. São coisas diferentes — misturar as duas é justamente o tipo de detalhe que passa batido. Guarde cada uma com um nome honesto no pass, tipo anthropic/oauth-token e anthropic/api-key.
Repara no que muda:
$(pass anthropic/oauth-token)é resolvido em tempo de execução. O GPG descriptografa, entrega o valor, e ele vive só pelo tempo daquele processoclaude.- A variável existe apenas para esse comando. Acabou o comando, acabou a variável. Nada fica exportado para sessão.
- E o mais importante: o que vai para o histórico do shell é a linha literal
CLAUDE_CODE_OAUTH_TOKEN=$(pass anthropic/oauth-token) claude— não o valor descriptografado. Seuhistory | grep -i tokenvolta limpo.
Se você usa o mesmo segredo o tempo todo, joga uma função no .zshrc — e olha que beleza, agora o dotfile guarda a referência, nunca a chave:
# no ~/.zshrc — nenhum segredo em texto puro aquiclaude() { CLAUDE_CODE_OAUTH_TOKEN=$(pass anthropic/oauth-token) command claude "$@"}Esse padrão vale para qualquer CLI faminto por chave: AWS_SECRET_ACCESS_KEY, OPENAI_API_KEY, GITHUB_TOKEN, token de deploy, o que for.
O que isso NÃO resolve — sendo honesto
Skin in the game: não vou te vender isso como bala de prata, porque não é. Uso e gosto, mas conheço os limites.
Quando você faz VAR=$(pass ...) comando, o valor descriptografado vira parte do ambiente daquele processo enquanto ele roda. Ou seja:
- Um processo do seu próprio usuário (ou o root) consegue ler
/proc/<pid>/environe ver a chave enquanto o comando está de pé. Isso não é blindagem contra máquina comprometida — é redução de superfície, não cofre inviolável. - Se você tiver
set -x(xtrace) ligado no shell, o comando é expandido no log e a chave aparece. Cuidado com isso em scripts. - Ferramentas que logam o próprio ambiente vão logar a chave. A responsabilidade de não fazer besteira continua sua.
O que o pass + $(...) resolve, e resolve bem, é o segredo em texto puro parado em disco e o vazamento para o histórico. Que, na prática, são os dois furos por onde a maioria das chaves vaza no dia a dia. Aceite isso: você está tapando os buracos reais, não construindo Fort Knox.
Bônus: as duas coisas que uso toda semana
Alguns comandos que viraram mão na roda:
# copia para o clipboard e limpa sozinho depois de 45spass -c anthropic/oauth-token
# segredo multilinha (chave privada, JSON de service account, etc.)pass insert -m gcp/service-accountVersionar suas chaves no Git (sim, dá — e faz sentido)
Aqui é onde muita gente trava: “versionar segredo no Git?! Cara, está maluco?”. Calma, respira. O pass foi feito pensando nisso, e a coisa é mais tranquila do que parece.
Como cada entrada já é um arquivo .gpg criptografado, o pass trata o ~/.password-store/ inteiro como um repositório Git normal:
# transforma o cofre num repo Gitpass git init
# aponta para um remoto privado seupass git push -u origin masterA partir daí, todo pass insert, pass edit ou pass rm já gera um commit automático. Você só empurra quando quiser:
pass git pushPor que isso é bom no dia a dia:
- Sincroniza suas máquinas. Notebook do trabalho, desktop de casa, VPS —
pass git pulle você tem os mesmos segredos em todo lugar, sem mandar chave por WhatsApp igual sofredor. - Histórico de verdade. Trocou um token? O commit registra. Dá para ver quando cada segredo mudou e voltar atrás se precisar.
- Backup honesto. Se o disco morrer, seu cofre está no remoto. E continua criptografado lá.
O ponto que faz isso ser seguro: o que sobe são só os .gpg, já cifrados. Quem clonar o repositório sem a sua chave privada GPG está segurando um monte de lixo ininteligível. A segurança mora na chave, não na obscuridade do repositório.
Agora, skin in the game de novo — dois cuidados que não dá para ignorar:
- Use remoto privado. Óbvio, mas preciso dizer: repositório público é não. Mesmo cifrado, você não expõe seu cofre inteiro para o mundo tentar quebrar offline no tempo dele.
- Sua chave privada GPG é o cofre de verdade. Se você perder a chave, perde o acesso a tudo (por isso, backup dela em lugar seguro). Se alguém roubar a chave e o repositório, aí sim leva tudo. Proteja a privada com uma boa passphrase e trate ela como o item mais sensível de todos.
No fim, é o mesmo princípio de sempre: você move o problema de “proteger N segredos espalhados” para “proteger uma chave só, muito bem”. Bem mais gerenciável.
E onde você guarda a chave privada?
Se tudo passa a depender da sua chave privada GPG, a pergunta óbvia é: e se eu perder ela? Aí perde o acesso a tudo que está no pass. Então vale sentar cinco minutos e resolver isso direito.
Uma coisa a favor: a chave privada já é protegida pela passphrase que você definiu ao criá-la. O arquivo exportado não é texto puro — é cifrado por essa senha. Ainda assim, trate como o item mais sensível que você tem. A regra é simples: duas cópias ou mais, em lugares diferentes. Uma cópia só é um ponto único de falha esperando acontecer.
Primeiro, exporte o que importa:
# descubra o IDgpg --list-secret-keys --keyid-format=long
# chave privada (protegida pela sua passphrase)gpg --export-secret-keys --armor SEU_KEYID > gpg-private.asc
# chave pública (não é segredo, mas guarde junto)gpg --export --armor SEU_KEYID > gpg-public.asc
# certificado de revogação — para "matar" a chave se ela vazargpg --gen-revoke --armor --output gpg-revoke.asc SEU_KEYIDAgora, onde colocar. Escolha dois ou mais destes, em locais diferentes:
- Pendrive criptografado (LUKS ou VeraCrypt), guardado fisicamente — gaveta trancada, casa dos pais, um cofre. Dois pendrives em dois lugares é o padrão simples e que funciona.
- Backup em papel com o
paperkey. Imprime a chave e guarda o papel. Sobrevive a disco queimado e bit rot, que backup digital esquecido numa gaveta costuma sofrer.
sudo apt install paperkeygpg --export-secret-keys SEU_KEYID | paperkey --output gpg-paper.txt# imprime o gpg-paper.txt e guarda o papel num lugar seguro- Gerenciador de senhas com anexo (Bitwarden, 1Password): dá para anexar o
.ascnum item. É cômodo para sincronizar, mas aí você depende da segurança daquela conta.
E o que não fazer, para não sabotar todo o resto:
- Não guarde no mesmo repositório Git do
pass. - Não jogue em Drive, Dropbox ou qualquer cloud em texto puro.
- Não deixe a chave sem passphrase — aí o export vira ouro puro para quem achar.
Um detalhe que muita gente esquece: a passphrase é a fechadura do backup, então não guarde ela colada na cópia da chave. Memorize, ou guarde separada num gerenciador de senhas. Chave num lugar, passphrase noutro — quem roubar um não abre o outro.
Se quiser subir de nível, existe o jeito paranoico: uma YubiKey (ou outro token de hardware). A chave privada mora dentro do chip e nunca sai dele. Toda operação GPG acontece no próprio hardware. Roubaram seu notebook? Não levaram a chave — precisariam do token físico e do PIN. Admito que aqui eu ainda estou migrando, mas é o caminho de quem leva a sério.
O elefante na sala: agentes de IA
Deixei o assunto mais importante para o fim, porque é o que mais me fez levar isso a sério. Hoje eu rodo o Claude Code e outros agentes que executam comandos no meu terminal sozinhos. Isso muda completamente o tamanho do estrago que um segredo mal guardado pode causar.
Pensa comigo. Um agente lê arquivos, roda comando, faz curl, às vezes em modo auto-aprovado, sem você confirmar cada passo. Se sua AWS_SECRET_ACCESS_KEY está exportada na sessão de onde você subiu o agente, ele herda essa variável — e todo subprocesso que ele abrir também. Some a isso o famigerado prompt injection: um README malicioso, uma resposta de API envenenada, um issue do GitHub com instruções escondidas. O agente lê aquilo como comando e pode ser induzido a ler /proc/self/environ e mandar suas chaves para fora. Complica, né?
E tem o vazamento silencioso: quase todo agente registra o que faz. Se um segredo aparecer num comando ecoado ou numa saída, ele vai parar no transcript, no log, e muitas vezes é enviado junto para a API do modelo. Sua chave dentro da janela de contexto de uma LLM é o tipo de coisa que você não quer nem imaginar.
Onde o pass ajuda de verdade aqui
O ganho concreto é este: não exporte nada na sessão do agente. Deixe o ambiente dele limpo e injete o segredo só no comando exato que precisa, com $(pass ...):
# ruim: a chave fica no ambiente do agente a sessão inteiraexport AWS_SECRET_ACCESS_KEY=...claude
# bom: o agente sobe sem segredo nenhum no ambiente;# a chave só existe no comando que realmente usa elaAWS_SECRET_ACCESS_KEY=$(pass aws/deploy-key) terraform applyAssim, um agente fazendo tarefa genérica não fica com suas credenciais de deploy paradas no ambiente dele o tempo todo, à disposição de qualquer instrução injetada.
Agora, skin in the game, porque isso é importante e muita gente vende ilusão: se você faz CLAUDE_CODE_OAUTH_TOKEN=$(pass ...) claude, essa chave está no ambiente do próprio agente — afinal é ela que autentica ele. O pass não protege o agente dele mesmo. O que o pass faz bem é manter todos os outros segredos fora do alcance do ambiente dele.
Dicas que uso
- Nunca dê
exportno shell de onde você lança o agente. Ele herda tudo. Ambiente limpo, sempre. - Encurte o cache do gpg-agent. Por padrão ele lembra sua passphrase por um tempo generoso — o que significa que um agente sequestrado poderia descriptografar segredos por horas sem te pedir nada. Aperte isso no
~/.gnupg/gpg-agent.conf:
default-cache-ttl 60max-cache-ttl 120gpg-connect-agent reloadagent /bye- Trate o prompt da passphrase como um ponto de controle. É chato digitar? É. Mas é justamente ali que você, humano, entra no meio: o agente não decifra nada sozinho sem você destravar o gpg-agent. Essa fricção é uma feature, não um bug.
- Cuidado com o auto-aprovar. Se o agente roda qualquer comando sem confirmação, o
passvira sua última linha de defesa, não a única. Reveja o que você libera. - Rotacione o que o agente tocou. Desconfiou de um comportamento estranho? Troca a chave. É barato e dorme melhor.
A motivação, no fim, é essa: agente autônomo multiplica o alcance de qualquer segredo exposto. Antes, uma chave vazada era um problema; agora ela pode virar um problema automatizado, rápido, rodando enquanto você toma café. A disciplina de manter os segredos cifrados e injetá-los sob demanda encolhe drasticamente o que um agente comprometido consegue pegar. Não custa quase nada e muda tudo.
Por fim
No fim do dia, a troca é essa: em vez de deixar sk-ant-... em texto puro no .zshrc e no histórico, você deixa uma referência — $(pass anthropic/oauth-token) — e o segredo de verdade fica criptografado, decifrado só na hora exata em que o comando precisa dele.
Não é perfeito, e eu fiz questão de mostrar onde ele não te cobre. Mas fecha os dois furos que mais aparecem na vida real, custa quatro comandos de setup, e o GPG faz o trabalho sujo por baixo sem você pensar nele.
Se você ainda tem chave exportada no dotfile, o teste é rápido: roda um history | grep -iE 'key|token|secret' aí no seu terminal. Se voltar alguma coisa legível, já sabe por onde começar.