Toc, toc. ¿Quién es? Touch ID. Otra vez.
Imagina esto: estás trabajando en tu terminal, consultando secretos de 1Password con op read. Necesitas la API key de Linear. Touch ID. La de OpenRouter. Touch ID. La de Gitea. Touch ID.
En media hora me pidió el dedo catorce veces.
¿Sabes qué pasa cuando una herramienta de seguridad te interrumpe catorce veces en treinta minutos? Que a la quinta ya no lees qué te está pidiendo. Pones el dedo como un acto reflejo. «Sí, lo que sea, déjame trabajar.»
Y ahí es exactamente donde la seguridad se va al garete.
Auth fatigue: el problema que nadie quiere ver
Esto tiene nombre en seguridad: fatiga de autorización. Y no es un concepto nuevo. Es el mismo principio que usan los ataques de MFA fatigue: bombardear al usuario con peticiones de autorización hasta que acepta una por puro agotamiento.
En 2022, un chaval de 17 años entró en los sistemas internos de Uber exactamente así. Mandó peticiones de push notification de autenticación al empleado una y otra vez, de madrugada, hasta que el tipo aceptó una para que le dejara dormir.
Obviamente, 1Password pidiéndote Touch ID no es un ataque. Pero el efecto psicológico es idéntico: te entrena para aprobar sin pensar.
Es como esas cookie banners que llevan años apareciendo en cada web. Al principio las leías. Ahora le das a «Aceptar todo» sin mirar. Enhorabuena: un mecanismo diseñado para proteger tu privacidad te ha enseñado a regalar tu privacidad más rápido.
Por qué 1Password me pedía el dedo cada vez
Mi setup: uso op read para leer secretos de 1Password desde la terminal. Funciona genial. El problema es que uso Claude Code (un asistente de IA en terminal), y cada comando que ejecuta es un proceso nuevo.
1Password tiene un timeout de sesión biométrica de 10 minutos de inactividad, y se refresca con cada uso. En teoría, no debería pedir el dedo tan seguido. Pero Claude Code no reutiliza procesos: cada vez que necesita un secreto, lanza un nuevo shell, y 1Password lo interpreta como una sesión nueva.
Resultado: Touch ID cada vez que Claude necesita un secreto. Que es constantemente.
La solución: un cache de 40 líneas
La idea es simple: un wrapper que se pone delante de op en el PATH. Cuando haces op read, mira si ya tiene el resultado cacheado y fresco. Si sí, te lo devuelve sin tocar 1Password. Si no, llama al op real, cachea el resultado, y listo.
Para cualquier otro subcomando (op signin, op item list, etc.), pasa directo al op real sin intervenir.
#!/bin/bash
# ~/.local/bin/op — Caching wrapper para 1Password CLI
# Solo cachea 'op read'. Todo lo demás pasa directo al op real.
# Cache TTL configurable con OP_CACHE_TTL (default: 3600s = 1h)
REAL_OP="/opt/homebrew/bin/op"
CACHE_DIR="${HOME}/.cache/op-cache"
CACHE_TTL="${OP_CACHE_TTL:-3600}"
# Solo cachear 'op read'
if [[ "$1" == "read" ]]; then
mkdir -p "$CACHE_DIR" && chmod 700 "$CACHE_DIR"
# Hash de todos los argumentos como clave de cache
CACHE_KEY=$(printf '%s\0' "$@" | shasum -a 256 | cut -d' ' -f1)
CACHE_FILE="${CACHE_DIR}/${CACHE_KEY}"
# Cache hit: fichero existe y no ha expirado
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 o expirado: llamar al op real
RESULT=$("$REAL_OP" "$@")
EXIT_CODE=$?
# Solo cachear si op tuvo éxito
if [[ $EXIT_CODE -eq 0 ]]; then
printf '%s' "$RESULT" > "$CACHE_FILE"
chmod 600 "$CACHE_FILE"
fi
printf '%s' "$RESULT"
exit $EXIT_CODE
else
# Cualquier otro subcomando: pass-through directo
exec "$REAL_OP" "$@"
fi
Lo guardas en ~/.local/bin/op, le das permisos de ejecución, y como ~/.local/bin está antes que /opt/homebrew/bin en el PATH, tu wrapper intercepta las llamadas.
Las decisiones de seguridad
Vamos a ser honestos sobre lo que estamos haciendo: guardar secretos en texto plano en disco. Eso suena fatal. Pero vamos a ponerlo en contexto.
Lo que cachea:
– Solo resultados de op read (lectura de secretos puntuales)
– Todo lo demás pasa directo al op real
Dónde lo guarda:
– ~/.cache/op-cache/ con permisos 700 (solo tu usuario)
– Cada fichero de cache con permisos 600 (solo lectura/escritura para ti)
Cuánto dura:
– 1 hora por defecto, configurable con OP_CACHE_TTL
Los nombres de fichero: – Son hashes SHA-256 de los argumentos, no revelan qué secreto contienen
¿Es más peligroso que un .env.local? No. Es exactamente lo mismo. Tus ficheros .env.local también son secretos en texto plano en disco con permisos restrictivos. Y esos los tienes en cada proyecto.
¿Es más peligroso que lo que 1Password ya hace? La app de 1Password mantiene tu vault descifrado en memoria mientras está desbloqueada. Nuestro cache es más limitado (solo los secretos que has leído, no todo el vault) pero menos sofisticado (disco vs memoria).
Lo que nos preocupa (y no sabemos resolver)
Aquí viene la parte honesta. No somos expertos en seguridad. Hemos tomado decisiones que nos parecen razonables, pero puede que se nos escape algo. Algunas dudas:
1. ¿Debería limpiarse el cache al bloquear la pantalla? Ahora mismo, si bloqueas el Mac y alguien accede al disco (robo, evil maid), los secretos cacheados están ahí. Aunque si alguien tiene acceso a tu disco, probablemente ya tienes problemas mayores (FileVault debería proteger contra esto).
2. ¿Hay race conditions?
Si dos procesos hacen op read del mismo secreto a la vez, ambos podrían intentar escribir el cache simultáneamente. En la práctica no debería causar problemas graves (el peor caso es una lectura parcial), pero no es bonito.
3. ¿El hash es suficiente?
Usamos SHA-256 de los argumentos como nombre de fichero. Si alguien tiene acceso a ~/.cache/op-cache/, no puede saber qué secreto hay en cada fichero, pero puede leer el contenido de todos. Los permisos 600 deberían impedirlo, pero si hay un proceso comprometido corriendo como tu usuario…
Lo que se podría mejorar
Algunas ideas que no hemos implementado (por ahora):
- Limpieza automática de ficheros expirados (un
crono unlaunchdque purgue periódicamente) - Cifrado del cache con una clave derivada de la sesión
- Notificación cuando un secreto se sirve desde cache («(cached)» en stderr)
- Invalidación manual con
op cache clearo similar
Aquí es donde entras tú
Mira, escribo esto con la honestidad de quien sabe que no es experto en seguridad. Hemos tomado decisiones que nos parecen sensatas. El threat model es claro: protegernos de la fatiga de autorización sin abrir agujeros obvios.
Pero «nos parece sensato» y «es seguro» son dos cosas muy diferentes.
Si sabes más de seguridad que nosotros (que no es difícil) y ves un fallo evidente, un edge case que no hemos considerado, o simplemente una forma mejor de hacer esto: dímelo. En serio. Los comentarios están abiertos y mi email también.
Prefiero que me digan «eso que has hecho es una ñapa peligrosa» a enterarme cuando sea demasiado tarde.
El elefante en la habitación
¿Debería 1Password resolver esto de serie? Sí, probablemente. Un timeout configurable por aplicación, o un modo «sesión de trabajo» que mantuviera la autorización activa durante un periodo definido, eliminaría la necesidad de este wrapper.
Pero mientras no lo hagan, la alternativa es peor: seguir poniendo el dedo cada 30 segundos hasta que tu cerebro desconecte y empieces a aprobar sin mirar.
Porque eso es lo paradójico de la seguridad excesiva: si la herramienta te molesta demasiado, terminas siendo menos seguro que si no la usaras. Al menos sin ella eres consciente de que estás desprotegido. Con fatiga de autorización, crees que estás protegido mientras apruebas cualquier cosa con los ojos cerrados.
La mejor cerradura del mundo no sirve de nada si el dueño deja la puerta abierta porque está harto de buscar la llave.
Relacionado: Si te interesa por qué centralizamos todos los secretos en 1Password, lee 39 millones de secretos filtrados en GitHub. Y si quieres ver qué pasa cuando le das demasiadas capacidades a una IA (spoiler: envía 44 emails inventados), Cuando tu IA se vuelve tu peor enemigo es la historia de terror.
Read this article in English.



