Cómo usar 1Password para dar secretos a agentes y apps de escritorio

Hay una fase bastante curiosa cuando empiezas a trabajar en serio con agentes locales.
Al principio todo parece fácil: tienes una API key, la metes en .zshrc, .bashrc, .env, common_rc o donde sea, y a correr.
El problema es que ese “a correr” empieza a crecer.
Un día la necesita un CLI. Otro día una skill. Otro día una automatización. Otro día una app de escritorio que no se abre desde tu terminal. Y cuando te das cuenta, tienes un montón de secretos viviendo en ficheros que no deberían ser el centro de tu seguridad.
Yo he estado migrando esa parte a 1Password.
No como “guardo las claves en 1Password y las copio a mano cuando me hacen falta”, sino como un runtime real para agentes: las credenciales viven en 1Password, los procesos las reciben solo cuando las necesitan, y los ficheros de configuración del shell dejan de ser el almacén de secretos.
El problema no es solo dónde guardas la API key
Guardar una clave en un fichero tipo .zshrc tiene dos problemas distintos.
El primero es obvio: el secreto está en texto plano en un sitio que se carga constantemente.
El segundo es más sutil: acabas tratando el entorno del shell como si fuera tu sistema de permisos.
Si una herramienta necesita OPENROUTER_API_KEY, la exportas. Si otra necesita TELEGRAM_BOT_TOKEN, lo exportas también. Si luego un proceso hijo hereda todo eso, pues mala suerte.
Durante un tiempo puede compensar por simplicidad. Pero si tu forma de trabajar depende cada vez más de agentes, automatizaciones y herramientas que se llaman entre sí, empieza a ser una base bastante mala.
No porque todo vaya a explotar mañana, sino porque pierdes control.
La pieza base: 1Password + un wrapper
El enfoque que he terminado usando es este:
- Un vault específico de 1Password para runtime de agentes.
- Un service account con permisos limitados de lectura sobre ese vault.
- Un fichero de entorno con referencias de 1Password, no con valores reales.
- Un wrapper que ejecuta comandos con esas variables resueltas.
La idea del fichero de entorno es que no contiene secretos:
OPENROUTER_API_KEY=op://Agent Runtime/OpenRouter API Key/password
GEMINI_API_KEY=op://Agent Runtime/Gemini API Key/password
LISTMONK_API_KEY=op://Agent Runtime/Listmonk API Key/password
Es el equivalente conceptual a tener un .common_rc para secretos, pero sin guardar los secretos ahí.
Luego el wrapper hace algo de este estilo:
#!/usr/bin/env bash
set -euo pipefail
token_file="${OPRUN_TOKEN_FILE:-$HOME/.config/1password/agent-runtime-token}"
env_file="${OPRUN_ENV_FILE:-$HOME/.config/op/agent-runtime.env}"
export OP_SERVICE_ACCOUNT_TOKEN
OP_SERVICE_ACCOUNT_TOKEN="$(<"$token_file")"
exec op run --env-file "$env_file" -- env -u OP_SERVICE_ACCOUNT_TOKEN "$@"
Lo llamé oprun, porque al final la ergonomía importa:
oprun listmonk campaigns list
oprun postflow schedule list
oprun node script-que-necesita-openrouter.js
La parte importante no es el nombre. Es que el comando recibe las variables resueltas, pero el token del service account no llega al proceso hijo.
Ese detalle merece la pena. El proceso necesita OPENROUTER_API_KEY, no necesita tener acceso genérico al vault.
Las skills empiezan a saber pedir sus secretos
Una vez que tienes esto, puedes actualizar las skills o runbooks para que dejen de asumir que las variables están exportadas globalmente.
Antes:
listmonk campaigns list
Después:
oprun listmonk campaigns list
Parece un cambio pequeño, pero cambia mucho la frontera de seguridad.
El shell sigue teniendo configuración no sensible: URLs base, nombres de usuario, flags, paths. Pero las API keys y tokens pasan por 1Password en el momento de ejecución.
Para agentes esto encaja muy bien, porque convierte el acceso a credenciales en una convención operativa. Si una skill necesita Listmonk, sabe que debe usar oprun. Si necesita PostFlow, igual. Si necesita OpenRouter o Gemini, lo mismo.
No dependes de que una sesión concreta de terminal tenga el entorno cargado.
El caso raro: las apps de escritorio
El problema aparece con las apps de escritorio.
Un CLI ejecutado desde tu terminal puede usar oprun sin mucho misterio. Pero una app que abres desde Finder, Spotlight o el Dock no hereda el entorno de tu shell interactivo.
Esto me pasó con OpenCode.
La app necesitaba credenciales que ya estaban bien en 1Password y funcionaban desde terminal. Pero al abrirla como aplicación normal de macOS, no veía esas variables.
Aquí hay varias opciones:
- dejar esas variables en el entorno global del usuario;
- usar algún plugin específico de la herramienta;
- abrir siempre la app desde terminal;
- crear un wrapper
.app.
La última opción es la que más me convence.
Un .app pequeño que apunta a la app real
El wrapper no es una copia de la aplicación.
Es una app mínima de macOS que vive, por ejemplo, en /Applications/OpenCode Agent Runtime.app, pero que por dentro ejecuta:
$HOME/.local/bin/oprun /Applications/OpenCode.app/Contents/MacOS/OpenCode
La app original sigue estando en /Applications/OpenCode.app.
Si OpenCode se actualiza, se actualiza la app real. El wrapper solo apunta al ejecutable dentro del bundle. Mientras la ruta y el nombre del ejecutable no cambien, no hay nada más que hacer.
El wrapper tiene su propio Info.plist, puede copiar el icono de la app original para que quede bien en Finder/Dock, y registra LaunchServices para que macOS la trate como una app normal.
La parte ejecutable puede ser tan simple como:
#!/usr/bin/env bash
set -euo pipefail
log_dir="${AGENT_RUNTIME_LOG_DIR:-$HOME/Library/Logs/AgentRuntime}"
mkdir -p "$log_dir"
exec "$HOME/.local/bin/oprun" \
/Applications/OpenCode.app/Contents/MacOS/OpenCode \
"$@" >>"$log_dir/opencode-agent-runtime.log" 2>&1
Con esto puedes dejar el acceso directo en el Dock y abrirlo como cualquier otra app.
No es perfecto, porque si la app cambia su estructura interna tendrás que regenerar el wrapper. Pero es bastante limpio, reversible y fácil de entender.
Cosas que conviene verificar
Hay tres comprobaciones que para mí son obligatorias:
- Que la app real se abre desde Finder/Dock.
- Que el proceso hijo recibe las variables que necesita.
- Que
OP_SERVICE_ACCOUNT_TOKENno aparece en el entorno del proceso hijo.
La tercera es la que más me importa.
El service account sirve para resolver secretos. No debería quedarse disponible dentro de cualquier app que abras con el wrapper.
También conviene que oprun no dependa del PATH del shell interactivo. Las apps abiertas desde Finder suelen tener un entorno mucho más pobre que una terminal. Si tu wrapper llama a op, mejor que oprun sepa encontrarlo por ruta absoluta o por configuración explícita.
Este fue precisamente el fallo que me encontré en la primera prueba: desde terminal funcionaba, desde Finder no encontraba op. La solución fue hacer que oprun resolviera el binario de 1Password CLI sin depender del PATH interactivo.
El patrón que me interesa
Lo que me gusta de esta solución no es solo que funcione con OpenCode.
Me gusta porque deja un patrón general:
- secretos en 1Password;
- configuración no sensible en el shell;
- comandos de agente vía
oprun; - apps de escritorio mediante wrappers
.app; - cero claves copiadas en ficheros de inicio;
- cero dependencia de abrirlo todo desde terminal.
Y, sobre todo, es fácil de convertir en una skill.
Ahora un agente puede crear un launcher de este tipo para cualquier app macOS: inspecciona el bundle original, detecta el ejecutable real, genera el .app wrapper, copia el icono si existe, registra la aplicación y te dice cómo verificar que los secretos llegan sin filtrar el token del service account.
No lo cuento como una arquitectura definitiva para todo el mundo.
Pero si trabajas con agentes locales, CLIs, automatizaciones y apps de escritorio que necesitan las mismas credenciales, separar “dónde viven los secretos” de “cómo los recibe cada proceso” empieza a ser bastante sano.
Y de paso, tu .zshrc deja de parecer una caja fuerte abierta.
Cómo conseguir la localización amplia en Android
Cómo pedir permisos en Jetpack Compose