Toc, toc. Quem é? Touch ID. De novo.

Imagine isso: você está trabalhando no seu terminal, consultando segredos do 1Password com op read. Precisa da API key do Linear. Touch ID. A do OpenRouter. Touch ID. A do Gitea. Touch ID.

Em meia hora me pediu o dedo quatorze vezes.

Sabe o que acontece quando uma ferramenta de segurança te interrompe quatorze vezes em trinta minutos? Que na quinta vez você já não lê mais o que está pedindo. Põe o dedo como um ato reflexo. “Sim, tanto faz, me deixa trabalhar.”

E é exatamente aí que a segurança vai pro espaço.

Auth fatigue: o problema que ninguém quer ver

Isso tem nome na segurança: fadiga de autorização. E não é um conceito novo. É o mesmo princípio usado nos ataques de MFA fatigue: bombardear o usuário com pedidos de autorização até que aceite um por puro esgotamento.

Em 2022, um moleque de 17 anos entrou nos sistemas internos do Uber exatamente assim. Mandou pedidos de push notification de autenticação para o funcionário uma e outra vez, de madrugada, até que o cara aceitou um para conseguir dormir.

Obviamente, o 1Password pedindo Touch ID não é um ataque. Mas o efeito psicológico é idêntico: te treina para aprovar sem pensar.

É como aqueles cookie banners que há anos aparecem em cada site. No início você lia. Agora clica em “Aceitar tudo” sem olhar. Parabéns: um mecanismo projetado para proteger sua privacidade te ensinou a entregar sua privacidade mais rápido.

Por que o 1Password pedia meu dedo toda vez

Meu setup: uso op read para ler segredos do 1Password pelo terminal. Funciona muito bem. O problema é que uso Claude Code (um assistente de IA no terminal), e cada comando que executa é um processo novo.

O 1Password tem um timeout de sessão biométrica de 10 minutos de inatividade, e se renova a cada uso. Em teoria, não deveria pedir o dedo tão seguido. Mas o Claude Code não reutiliza processos: cada vez que precisa de um segredo, lança um novo shell, e o 1Password interpreta como uma sessão nova.

Resultado: Touch ID toda vez que o Claude precisa de um segredo. Que é constantemente.

A solução: um cache de 40 linhas

A ideia é simples: um wrapper que fica na frente do op no PATH. Quando você faz op read, verifica se já tem o resultado cacheado e fresquinho. Se sim, te retorna sem tocar no 1Password. Se não, chama o op real, cacheia o resultado, e pronto.

Para qualquer outro subcomando (op signin, op item list, etc.), passa direto para o op real sem intervir.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash
# ~/.local/bin/op — Wrapper de cache para 1Password CLI
# Só cacheia 'op read'. Todo o resto passa direto para o op real.
# Cache TTL configurável com OP_CACHE_TTL (default: 3600s = 1h)

REAL_OP="/opt/homebrew/bin/op"
CACHE_DIR="${HOME}/.cache/op-cache"
CACHE_TTL="${OP_CACHE_TTL:-3600}"

# Só cachear 'op read'
if [[ "$1" == "read" ]]; then
    mkdir -p "$CACHE_DIR" && chmod 700 "$CACHE_DIR"

    # Hash de todos os argumentos como chave do cache
    CACHE_KEY=$(printf '%s\0' "$@" | shasum -a 256 | cut -d' ' -f1)
    CACHE_FILE="${CACHE_DIR}/${CACHE_KEY}"

    # Cache hit: arquivo existe e não expirou
    if [[ -f "$CACHE_FILE" ]]; then
        FILE_AGE=$(( $(date +%s) - $(stat -c %Y "$CACHE_FILE") ))
        if [[ $FILE_AGE -lt $CACHE_TTL ]]; then
            cat "$CACHE_FILE"
            exit 0
        fi
    fi

    # Cache miss ou expirado: chamar o op real
    RESULT=$("$REAL_OP" "$@")
    EXIT_CODE=$?

    # Só cachear se op teve sucesso
    if [[ $EXIT_CODE -eq 0 ]]; then
        printf '%s' "$RESULT" > "$CACHE_FILE"
        chmod 600 "$CACHE_FILE"
    fi

    printf '%s' "$RESULT"
    exit $EXIT_CODE
else
    # Qualquer outro subcomando: pass-through direto
    exec "$REAL_OP" "$@"
fi

Você salva em ~/.local/bin/op, dá permissões de execução, e como ~/.local/bin está antes que /opt/homebrew/bin no PATH, seu wrapper intercepta as chamadas.

As decisões de segurança

Vamos ser honestos sobre o que estamos fazendo: guardar segredos em texto plano no disco. Isso soa horrível. Mas vamos colocar em contexto.

O que cacheia:

  • Só resultados de op read (leitura de segredos pontuais)
  • Todo o resto passa direto para o op real

Onde guarda:

  • ~/.cache/op-cache/ com permissões 700 (só seu usuário)
  • Cada arquivo de cache com permissões 600 (só leitura/escrita para você)

Quanto dura:

  • 1 hora por padrão, configurável com OP_CACHE_TTL

Os nomes dos arquivos:

  • São hashes SHA-256 dos argumentos, não revelam qual segredo contêm

É mais perigoso que um .env.local? Não. É exatamente a mesma coisa. Seus arquivos .env.local também são segredos em texto plano no disco com permissões restritivas. E esses você tem em cada projeto.

É mais perigoso que o que o 1Password já faz? O app do 1Password mantém seu vault descriptografado na memória enquanto está desbloqueado. Nosso cache é mais limitado (só os segredos que você leu, não todo o vault) mas menos sofisticado (disco vs memória).

O que nos preocupa (e não sabemos resolver)

Aqui vem a parte honesta. Não somos especialistas em segurança. Tomamos decisões que nos parecem razoáveis, mas pode ser que algo nos escape. Algumas dúvidas:

1. Deveria limpar o cache ao bloquear a tela? Agora mesmo, se você bloquear o Mac e alguém acessar o disco (roubo, evil maid), os segredos cacheados estão lá. Embora se alguém tem acesso ao seu disco, provavelmente você já tem problemas maiores (FileVault deveria proteger contra isso).

2. Há race conditions? Se dois processos fazem op read do mesmo segredo ao mesmo tempo, ambos poderiam tentar escrever o cache simultaneamente. Na prática não deveria causar problemas graves (o pior caso é uma leitura parcial), mas não é bonito.

3. O hash é suficiente? Usamos SHA-256 dos argumentos como nome do arquivo. Se alguém tem acesso a ~/.cache/op-cache/, não pode saber qual segredo há em cada arquivo, mas pode ler o conteúdo de todos. As permissões 600 deveriam impedir isso, mas se há um processo comprometido rodando como seu usuário…

O que poderia melhorar

Algumas ideias que não implementamos (por enquanto):

  • Limpeza automática de arquivos expirados (um cron ou launchd que purgue periodicamente)
  • Criptografia do cache com uma chave derivada da sessão
  • Notificação quando um segredo é servido do cache ("(cached)" no stderr)
  • Invalidação manual com op cache clear ou similar

Aqui é onde você entra

Olha, escrevo isso com a honestidade de quem sabe que não é especialista em segurança. Tomamos decisões que nos parecem sensatas. O threat model é claro: proteger-nos da fadiga de autorização sem abrir brechas óbvias.

Mas “nos parece sensato” e “é seguro” são duas coisas muito diferentes.

Se você sabe mais de segurança que nós (o que não é difícil) e vê uma falha evidente, um edge case que não consideramos, ou simplesmente uma forma melhor de fazer isso: me diga. Sério. Os comentários estão abertos e meu email também.

Prefiro que me digam “isso que você fez é uma gambiarra perigosa” a descobrir quando for tarde demais.

O elefante na sala

O 1Password deveria resolver isso de série? Sim, provavelmente. Um timeout configurável por aplicação, ou um modo “sessão de trabalho” que mantivesse a autorização ativa durante um período definido, eliminaria a necessidade deste wrapper.

Mas enquanto não fazem isso, a alternativa é pior: continuar pondo o dedo a cada 30 segundos até que seu cérebro desconecte e comece a aprovar sem olhar.

Porque isso é o paradoxal da segurança excessiva: se a ferramenta te chateia demais, você acaba sendo menos seguro do que se não a usasse. Pelo menos sem ela você tem consciência de que está desprotegido. Com fadiga de autorização, acredita que está protegido enquanto aprova qualquer coisa de olhos fechados.

A melhor fechadura do mundo não serve para nada se o dono deixa a porta aberta porque está cansado de procurar a chave.


Relacionado: Se te interessa por que centralizamos todos os segredos no 1Password, leia 39 milhões de segredos vazados no GitHub. E se quer ver o que acontece quando você dá capacidades demais para uma IA (spoiler: envia 44 emails inventados), Quando sua IA se torna seu pior inimigo é a história de terror.