← Volver a todas las publicaciones
Publicado el · por Renaud Deraison

La regla de permiso se quedó con la contraseña

El 6 de noviembre, Apiiro informó de que el botón “Yes, and don't ask again” de Claude Code guarda el comando aprobado, secretos incluidos, en .claude/settings.local.json, y de que 1.122 repositorios públicos de GitHub habían hecho commit de ese archivo con contraseñas, tokens y claves de aspecto válido. Dos escáneres de secretos populares pasaron por alto 215. En Bromure Agentic Coding, cada secreto que el agente puede escribir es un sustituto, así que una regla guardada contiene un valor que no funciona en ningún sitio, y cada push te espera en el Mac.

Haces clic en “Yes, and don't ask again” para que una pregunta no vuelva a salir. Claude Code guarda el comando entero para reconocerlo la próxima vez, contraseña incluida, en un archivo dentro de tu proyecto.

El 6 de noviembre, Apiiro publicó una investigación sobre agentes de código que filtran secretos a través de “don't ask again”. The Hacker News lo cubrió a la mañana siguiente.

El mecanismo es corriente. Cuando Claude Code pide ejecutar un comando de shell y eliges Yes, and don't ask again, escribe una regla de permiso en .claude/settings.local.json, dentro del proyecto. A veces la regla es un patrón como npm test *. A veces es el comando exacto, y cuando ese comando llevaba una contraseña después de PGPASSWORD=, sshpass -p o mysql -p, la contraseña queda en un archivo JSON junto a tu código.

Apiiro buscó ese archivo en GitHub público y encontró:

  • 104.185 copias en 103.617 repositorios;
  • 1.122 copias con al menos un secreto, 1.379 secretos distintos en total;
  • 379 contraseñas de bases de datos, 43 accesos SSH a servidores públicos, 42 tokens de GitHub, GitLab y npm, y 23 claves de AWS;
  • secretos de 63 organizaciones, entre ellos una clave de nube con nivel de administrador de un proveedor de bases de datos y la contraseña de propietario de una base de historiales médicos.

Todos esos archivos seguían en su rama por defecto cuando Apiiro lo comprobó. En la mitad de los repositorios llevaban ahí al menos 217 días. Commits más antiguos contenían 734 secretos más en 509 repositorios, y 211 forks de 74 repositorios llevaban copias. Sumando el .claude/settings.json compartido, el historial y los registros de chat de agentes guardados, Apiiro contó 2.539 secretos en 1.994 repositorios.

Un clic, cuatro pasos hasta un secreto públicoSolicitud de aprobaciónPGPASSWORD=s3cr3t psql-h db.example.com …"Yes, and don't ask again"Regla de permiso guardada.claude/settings.local.jsonel comando entero, en claroEn un commit539 / 1.105repos rastreados donde un commitcon Claude de coautor lo tocóPúblico en GitHub1.122copias con secretos;la mitad, 217 días o másBorrar el archivo no borra el secreto734 secretos más en commits antiguos de 509 repositorios; 211 forks de 74 repositorios llevan los mismos valores
Cómo llega una contraseña de una solicitud de aprobación a un repositorio público. La regla de permiso guarda el comando exacto. El agente hace commit de la carpeta del proyecto, el archivo se va con ella, y cada fork y cada commit antiguo conserva una copia después de limpiar la rama por defecto.

Por qué se coló por todas partes

Claude Code sí intenta mantener el archivo fuera de git. La primera vez que escribe settings.local.json, lo añade a tus exclusiones globales de git. Esa regla vive en un solo ordenador. El portátil de un compañero, un runner de CI o un clon recién hecho no la tienen, y no hace nada por un archivo que ya está en un commit. Claude Code nunca añade el archivo al .gitignore propio del proyecto. De los 16 agentes de código que comparó Apiiro, Claude Code es el único confirmado que guarda por defecto el comando aprobado completo en un archivo del proyecto.

Los escáneres de secretos no lo hicieron mejor. Apiiro pasó gitleaks y TruffleHog por cada copia. Gitleaks marcó 564 de los 1.122 archivos, TruffleHog 643, y 215 se escaparon de ambos. La mayoría de esos 215 eran contraseñas escritas dentro de comandos. Los escáneres buscan formatos de clave conocidos como AKIA… o ghp_…, y sshpass -p hunter2 no encaja con ninguno.

Los usuarios avisaron del problema hace más de un año. Una incidencia de agosto de 2025, anthropics/claude-code#5544, decía que Claude Code guardaba en este archivo, palabra por palabra, comandos aprobados con claves de API. Un bot la cerró como “not planned” tras 60 días sin actividad. Otros cuatro informes relacionados corrieron la misma suerte con el mismo bot, el último el día antes del artículo de The Hacker News.

El secreto estaba en el comando porque estaba en manos del agente

Cada uno de esos 1.379 secretos era un valor que el agente podía escribir. Un desarrollador lo había pegado en el chat, o lo había dejado en un archivo .env, o el agente lo leyó de ~/.aws/credentials o ~/.pgpass y lo metió en línea en el comando que quería ejecutar. La regla de permiso anotó lo que el agente ya tenía.

El consejo de Apiiro para el futuro apunta en la misma dirección: mantener los secretos fuera de los comandos y hacer referencia a una variable de un gestor de secretos, para que la regla guardada solo contenga $DB_URL. Eso ayuda si cada desarrollador y cada agente lo cumple siempre. Un agente que intenta que funcione un comando que falla expandirá la variable. Bromure toma el otro camino y mantiene el valor real fuera del entorno del agente por completo.

El mismo clic, en Bromure Agentic Coding

Bromure Agentic Coding ejecuta Claude Code, Codex, Grok, Kimi y omp en máquinas virtuales Ubuntu en tu Mac. El agente recibe las credenciales a través de lo que llamamos la frontera del cable: dentro de la VM, cada secreto es un sustituto, y el valor real está cifrado en el Mac, con su clave maestra en el llavero de macOS. Un proxy en el Mac cambia el sustituto por el valor real en el cable, y solo en una petición al host al que pertenece esa credencial.

Así que cuando el agente pide ejecutar un comando con un token dentro y eliges Yes, and don't ask again, Claude Code guarda el comando igual que antes. El token que contiene es el sustituto.

Cada secreto que el agente puede escribir es un sustituto

Los sustitutos conservan la forma que esperan las herramientas, así que en el proyecto no cambia nada:

Lo que Apiiro encontró filtradoLo que tiene el agente en una máquina Bromure
Tokens de GitHub y GitLabghp_ + 36 caracteres, glpat- + 20, en GH_TOKEN, GITLAB_TOKEN y ~/.git-credentials
Tokens de npmun valor brm_… en NPM_TOKEN, de una entrada Other API key limitada a registry.npmjs.org
Claves de AWSel ID de clave de acceso real con una clave secreta falsa; el Mac vuelve a firmar cada petición con la real
Contraseñas de bases de datosun valor brm-db-… en la variable que elijas, para ClickHouse, la Mongo Data API y Elasticsearch
Accesos SSHninguna contraseña: firmas hechas con claves que se quedan en el Mac

Toma un comando que un agente podría ejecutar contra un endpoint de ClickHouse:

curl -u default:brm-db-7Qk2…Xw9 \
  'https://analytics.example.com:8443/?query=SELECT+count()+FROM+events'

Apruébalo para siempre y la regla de permiso contendrá brm-db-7Qk2…Xw9. El proxy cambia el sustituto en las peticiones a analytics.example.com, en la cabecera Basic-auth y en el cuerpo de la petición, porque los secretos de bases de datos suelen viajar dentro de SQL y JSON. En cualquier otro sitio, la cadena no sirve de nada.

Con AWS, el secreto nunca llega a la VM. El ~/.aws/config del agente apunta a un ayudante que entrega el ID de clave de acceso real con un secreto falso de 40 caracteres. La AWS CLI, boto3 y terraform firman con el falso, y el Mac quita esa firma y vuelve a firmar con el secreto real antes de que salga la petición. La clave secreta real nunca existe en la VM, así que ningún comando que escriba el agente puede contenerla.

SSH cubre los 43 accesos a servidores del recuento de Apiiro. El SSH_AUTH_SOCK de una máquina Bromure es un puente hacia el Mac: la VM puede pedir firmas, y los bytes de la clave privada nunca cruzan. Las claves protegidas por una YubiKey, Secretive o 1Password se pueden ofrecer una a una, con la petición de toque en tu Mac. El agente nunca necesita sshpass -p, así que no hay contraseña que una regla de permiso pueda guardar.

VM de la máquinaComando aprobadoGH_TOKEN=ghp_Xk3…a7Q gh api …"Yes, and don't ask again"settings.local.jsonguarda el sustituto, que solosirve vía el proxy de este Macgit pushel commit incluye el archivoTu MacEl proxy cambia en el cablesustituto → token real, solo enpeticiones a github.comValores reales, en el llaverotokens, secretos AWS, claves SSH:nunca en la VM, de ninguna formaPrompt before writeel push espera tu respuestaInternetapi.github.comrecibe el token real, elcomando va como siempreAlguien copia el archivode un repo, un fork o uncommit viejo y prueba el token:401 Bad credentials
El mismo clic en una máquina Bromure. La regla de permiso sigue guardando el comando, pero el token que contiene es un sustituto. El push que lo publicaría espera un diálogo en el Mac, y un sustituto copiado de GitHub falla en todas partes, porque solo el proxy de tu Mac puede convertirlo en el valor real.

Un sustituto en GitHub no abre nada

Supongamos que el archivo sí llega a un repositorio público, a un fork y a una docena de commits antiguos. Quien lo extraiga obtiene ghp_ y 36 caracteres que GitHub nunca ha emitido. Enviado desde su portátil, falla la autenticación. Solo el proxy de tu Mac sabe a qué token real corresponde, y ese proxy solo da servicio a la VM de tu máquina.

Si un sustituto sale alguna vez hacia el host equivocado desde dentro de la máquina, el proxy lo trata como un intento de exfiltración. Bloquea la petición con un HTTP 451, pausa la VM y abre una alerta titulada This environment may have been compromised que nombra la credencial, el host al que pertenece y el host al que se envió. Así cada sustituto es también un cable trampa, no solo un marcador de posición.

La rotación se hace en un solo sitio

La primera instrucción de Apiiro es rotar cada contraseña, token y clave que el archivo haya contenido, antes de limpiar nada. Con secretos reales repartidos entre archivos .env, el historial del shell y la configuración del agente, eso es una búsqueda del tesoro. En Bromure, los valores reales están en un solo sitio: el panel Credentials de la máquina, en el Mac. Pega ahí el nuevo token y el sustituto en la VM sigue siendo el mismo, así que las reglas de permiso, los scripts y las configuraciones dentro de la máquina siguen funcionando.

Una Other API key también puede ser una referencia de 1Password como op://Engineering/npm/token. Bromure la resuelve en el Mac y la vuelve a resolver cada 2 minutos, así que una clave rotada en 1Password surte efecto en un par de minutos, sin reiniciar y sin nada que editar en la VM.

Llevar los secretos a ese panel es un solo paso. Import env file… lee un .env o un ~/.bashrc existente, muestra las variables con los valores ocultos y las convierte en credenciales. La carpeta del proyecto ya no necesita el archivo .env del que el agente leía las contraseñas.

El push te espera

En 539 de los repositorios que rastreó Apiiro, un commit que acreditaba a Claude como coautor modificó el archivo. Los agentes hacen commit y push como parte de terminar una tarea.

En las máquinas Bromure nuevas, cada servicio de forja empieza en Prompt before write en el panel Guardrails. Las lecturas pasan. Un git push por HTTPS se detiene en un diálogo en el Mac que muestra la operación, y nada sale hasta que respondes. La política se aplica en el proxy, así que nada dentro de la VM puede desactivarla.

Un revisor que lee la línea

Los 215 secretos que ningún escáner detectó eran contraseñas dentro de comandos. Un modelo que lee PGPASSWORD=… psql -h db.example.com en un archivo JSON con commit sabe lo que tiene delante.

Una code review de Bromure vigila un repositorio de GitHub con un agente en una de tus máquinas, en Every new commit, Every new pull request o con un calendario semanal, y Bromure elimina los hallazgos duplicados entre análisis. Para el paso de Apiiro “busca en tu organización, historial incluido”, Run Baseline Now lanza una revisión puntual de todo el repositorio. GitHub se consulta desde el Mac, y el agente de revisión trabaja con los mismos sustitutos que cualquier otro.

La transcripción también guarda el sustituto

El recuento de Apiiro incluía registros de chat de agentes guardados. Los archivos de sesión del propio Claude Code en la VM registran lo que vio el agente, y en una máquina Bromure eso es el sustituto. Las trazas de Bromure en el Mac guardan los cambios de token como vistas previas cortas del tipo sk-a…f9q3, y los bytes del secreto real nunca entran en un registro de traza. Cuando aun así aparece en el tráfico un secreto de aspecto real, porque alguien lo pegó a mano en la VM, la traza lo marca como posible fuga, y bromure-cli trace leaks las lista todas. Activa Only allow credentials Bromure injects en Guardrails, y una petición que lleve una credencial que Bromure no puso ahí queda bloqueada y dispara la alerta de compromiso.

Sustitutos en cada comando

Los tokens, los secretos de bases de datos y las claves de AWS son sustitutos en la VM. Una regla de permiso guarda un valor que solo funciona a través de tu Mac.

Sin contraseña para SSH

La VM pide firmas al Mac. La clave privada nunca cruza, y sshpass -p nunca hace falta.

Los push esperan en el Mac

Prompt before write es el valor por defecto en máquinas nuevas. Un push espera tu respuesta, y lo impone el proxy.

Rotar una vez

Los valores reales viven en un solo panel, o en 1Password. Cámbialos ahí y cada sustituto sigue funcionando.

Qué hacer esta semana

De la lista de Apiiro, para todo equipo que use agentes de código:

  • Busca .claude/settings.local.json y .claude/settings.json en tu organización de GitHub, historial incluido.
  • Rota cada secreto que esos archivos hayan contenido y después limpia.
  • Ejecuta git rm --cached .claude/settings.local.json y añade el archivo al .gitignore del proyecto y a las plantillas de tu organización.
  • Trata .claude/, .cursor/, .gemini/ y .specstory/ como archivos .env en el análisis pre-commit y de CI, y asegúrate de que el escáner detecta contraseñas dentro de comandos.

Después, saca los secretos reales del alcance del agente. En una máquina Bromure, el próximo “don't ask again” guarda un sustituto, tanto si el desarrollador se acordó ese día de las reglas de Apiiro como si no.

Para el patrón más amplio de agentes que copian secretos en el historial y los registros, mira La transcripción se quedó con el token. Para ver cómo funciona el cambio en detalle, lee el capítulo Credentials del manual.