TL;DR Cuando Claude Code te pide permiso para algo que ya aprobaste, mira qué hay al otro lado del
&&: esa parte es la que no tiene regla. Escribe una regla para cada parte (Bash(npm run build)además deBash(npm test)), o elige la opción 2 del prompt, que guarda una regla por parte en.claude/settings.local.json, y revisa lo que escribió.
Claude Code no compara tus reglas con la línea entera. Parte el comando por sus operadores (&&, ||, ;, |, |&, & y los saltos de línea) y exige que cada parte case con una regla allow por sí sola, o que sea uno de los comandos de solo lectura que ya aprueba sin preguntar (cd, ls, grep, tail, git status...). Con Bash(npm test) aprobado, npm test se ejecuta sin más, pero npm run build && npm test vuelve a preguntar: npm run build no tiene regla. Y al revés, una regla deny o ask salta si casa con cualquier parte.
Qué pasa con cada forma
Comprobado en Claude Code v2.1.310, en un proyecto de prueba, con claude -p --allowedTools "Bash(npm test)" y sin ninguna otra regla. La lista de denegados sale de permission_denials:
| Comando | Resultado | Por qué |
|---|---|---|
npm test |
Se ejecuta | Casa con la regla |
npm run build && npm test |
Pide permiso | npm run build no tiene regla |
npm run lint ; npm test |
Pide permiso | npm run lint no tiene regla |
npm test | tail -1 |
Se ejecuta | tail es de solo lectura |
git status && npm test |
Se ejecuta | git status es de solo lectura |
cd web && npm test |
Se ejecuta | cd dentro del proyecto es de solo lectura |
timeout 30 npm test |
Se ejecuta | timeout es un wrapper que se quita antes de comparar |
NODE_ENV=test npm test |
Se ejecuta | NODE_ENV está en la lista de variables seguras |
FOO=1 npm test |
Pide permiso | Una variable desconocida delante corta el allow |
npm test && |
Pide permiso | Un && sin nada detrás no se puede partir |
El problema casi nunca es la parte que aprobaste, es la otra. Claude encadena comandos por costumbre (compilar y luego probar, instalar y luego arrancar), y basta una parte sin regla para que vuelva el prompt.
Lo que guarda "Yes, and don't ask again"
En una sesión interactiva, sin reglas, el prompt para npm run build && npm test ya anuncia que guardará dos cosas:
Bash command
Run build and tests
npm run build && npm test
This command requires approval
Do you want to proceed?
❯ 1. Yes
2. Yes, and don't ask again for npm run and npm test commands in /ruta/al/proyecto
3. Yes, and switch to auto mode · auto mode handles these prompts for you
4. No
La opción 2 escribe una regla por cada parte que necesitaba aprobación, nunca una para la línea entera:
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(npm test *)"
]
}
}
A partir de ahí, npm run lint ; npm test | tail -1 se ejecutó sin preguntar en esa misma sesión. Y con npm test ya cubierto, el prompt de npm ci && npm test solo ofreció Yes, and don't ask again for: npm ci *: la parte que ya tiene regla no se vuelve a guardar. La documentación pone un tope de cinco reglas por comando.
Cómo dejar de aprobarlo
1. Lee qué parte nombra el prompt
La opción 2 lista las partes que no tienen regla. Esas son las que tienes que cubrir, no el comando que ves completo arriba.
2. Revisa lo que guardó la opción 2
Guarda el prefijo con *, así que aprobar npm run build te deja Bash(npm run *): cualquier script de package.json, incluido uno que despliegue. Si quieres solo ese, cambia la línea en .claude/settings.local.json por la forma exacta:
"Bash(npm run build)"
3. Escribe una regla por parte, no por línea
Una regla como Bash(npm run build && npm test) no sirve de nada, porque nunca se compara con la línea entera. Lo que funciona es una por pieza, en .claude/settings.json si es para todo el equipo o en .claude/settings.local.json si es solo tuya:
{
"permissions": {
"allow": [
"Bash(npm run build)",
"Bash(npm run lint)",
"Bash(npm test *)"
]
}
}
El * se pone después del subcomando: Bash(npm *) aprobaría también npm publish. La sintaxis completa está en reglas deny, modos y wildcards.
4. Ten claro qué seguirá preguntando
Hay combinaciones que ninguna regla allow de prefijo aprueba; saberlo evita buscar una regla que no existe:
cd web && git status: uncda otra carpeta seguido degitpregunta siempre, porquegitpuede ejecutar los hooks de esa carpeta.cd /tmp && npm test: uncdfuera del proyecto necesita su propio permiso de lectura sobre esa ruta, aunquenpm testtenga regla.FOO=1 npm test: una variable que no está en la lista segura delante del comando.
5. En un repo clonado, acepta antes el diálogo de confianza
Las reglas allow del .claude/settings.json de un proyecto no se aplican hasta que aceptas el diálogo de confianza de esa carpeta. En claude -p lo avisa al arrancar y las ignora:
Ignoring 1 permissions.allow entry from .claude/settings.json: this workspace has not been trusted. Run Claude Code interactively here once and accept the trust dialog, or set projects["/ruta/al/proyecto"].hasTrustDialogAccepted: true in /root/.claude.json.
Las reglas deny y ask sí se aplican sin confianza, porque solo restringen.
Si lo que se repite son siempre los mismos comandos sueltos, la allowlist automática los convierte en reglas a partir de tu historial. Y si las reglas se te quedan cortas, la guía de permisos de Claude Code ordena el resto de palancas, de las reglas a auto mode.
Documentación oficial: Configure permissions