Cap 14: Permisos y Sandbox
Permission Modes
Claude Code tiene seis modos de permisos que controlan qué puede hacer sin preguntar:
| Modo | Lectura | Edición | Bash | Uso |
|---|---|---|---|---|
default (etiquetado Manual en la UI) | Auto | Pregunta | Pregunta | Uso normal |
acceptEdits | Auto | Auto | Pregunta | Confiar en ediciones |
plan | Auto | Bloqueado | Bloqueado | Solo exploración |
auto | Auto | Clasificador | Clasificador | Trabajo autónomo supervisado por un modelo |
dontAsk | Auto | Sin prompts | Sin prompts | Sin preguntas al usuario |
bypassPermissions | Auto | Auto | Auto | Solo en contenedores o VMs |
default acepta el alias manual desde v2.1.200.
Los modos se fijan con permissions.defaultMode en settings o con --permission-mode:
# Iniciar en modo plan para investigar
claude --permission-mode plan
# Aceptar ediciones automáticamente
claude --permission-mode acceptEdits
# Trabajo autónomo con clasificador
claude --permission-mode auto
Shift+Tab cicla entre default → acceptEdits → plan, insertando auto y bypassPermissions cuando están habilitados.
Cambiar durante sesión
/permissions # Ver y editar permisos
/sandbox # Configurar el sandbox
/plan no cambia el modo de la sesión: prefija un único prompt para que se ejecute en plan mode.
Auto mode
Auto mode usa un modelo clasificador aparte que revisa cada acción antes de ejecutarla. Bloquea escaladas de alcance, infraestructura no reconocida y acciones inducidas por contenido hostil (prompt injection).
flowchart LR
A[Claude propone una acción] --> B{Clasificador}
B -->|Aprueba| C[Se ejecuta]
B -->|Bloquea| D[Vuelve el prompt al usuario]
D --> E{3 seguidos<br/>o 20 totales?}
E -->|Sí| F[Auto mode se pausa]
E -->|No| A
Puntos clave:
- Está generalizado en todos los planes y superficies: Anthropic API, Claude Platform on AWS, Amazon Bedrock, Google Cloud Agent Platform, Microsoft Foundry y sesiones de gateway.
- El clasificador corre por defecto en Claude Sonnet 5 (desde v2.1.210), no en el modelo de la sesión, y sus llamadas cuentan para el consumo de tokens.
- Frenos no configurables: 3 bloqueos seguidos o 20 en total pausan auto mode y devuelven los prompts. Con
-plos bloqueos repetidos abortan la sesión, porque no hay usuario a quien preguntar. - El clasificador bloquea por defecto lanzar un loop de agente autónomo sin aprobación humana ni sandbox (por ejemplo
--dangerously-skip-permissionso--no-sandbox); desde v2.1.198 cubre también runners de terceros con aislamiento o aprobación desactivados. - La variable
CLAUDE_CODE_ENABLE_AUTO_MODE=1era obligatoria en v2.1.158–v2.1.206; desde v2.1.207 no tiene efecto (se acepta por compatibilidad).
Plan mode
Plan mode se activa de tres formas: Shift+Tab, --permission-mode plan, o prefijando un único prompt con /plan. La barra de estado muestra ⏸ plan mode on.
Al aprobar el plan, las opciones son:
- Yes, and use auto mode
- Yes, manually approve edits
- No, refine with Ultraplan on Claude Code on the web
- No, keep planning
Ctrl+G abre el plan en el editor para editarlo antes de ejecutar, y aceptar un plan renombra la sesión automáticamente.
Con auto mode disponible y useAutoModeDuringPlan activo (default), el clasificador aprueba comandos de solo lectura sin preguntar; las ediciones siguen bloqueadas hasta aprobar el plan.
bypassPermissions
No es “sin restricciones”: es el modo que salta los prompts incluso para escrituras en la propia configuración del agente (.git, .config/git, .claude, .vscode, .idea, .husky, .cargo, .devcontainer, .yarn, .mvn), lo que permite auto-escalada. La documentación es explícita: «Only use this mode in isolated environments like containers or VMs».
Frenos que sí quedan en pie:
- Reglas
askexplícitas. - Herramientas de conectores marcadas
askpor la organización. - Herramientas MCP con
requiresUserInteraction. - El circuit breaker de borrados sobre raíz o home (
rm -rf /,rm -rf ~), que desde v2.1.208 cubre también esas rutas ocultas tras$(...), backticks o<(...).
Además, en Linux y macOS Claude Code se niega a arrancar con --dangerously-skip-permissions bajo root/sudo, salvo dentro de un sandbox reconocido.
Protected paths
Hay escrituras que nunca se auto-aprueban salvo en bypassPermissions:
- Directorios:
.git,.config/git,.vscode,.idea,.husky,.cargo,.devcontainer,.yarn,.mvn,.claude(excepto.claude/worktrees). - Archivos:
.bashrc,.zshrc,.envrc,.npmrc,.mcp.json,.claude.json.
permissions.allow no los pre-aprueba: el chequeo de protected paths corre antes de evaluar allow. Es la causa más común de un allow que “no surte efecto”.
Wildcard syntax detallado
Categorías y orden de evaluación
Las reglas se agrupan en tres categorías: allow, ask y deny. El orden de evaluación es deny → ask → allow, y la primera coincidencia decide.
flowchart TD
A[Acción propuesta] --> B{¿Coincide con deny?}
B -->|Sí| C[Bloqueada]
B -->|No| D{¿Coincide con ask?}
D -->|Sí| E[Pregunta al usuario]
D -->|No| F{¿Coincide con allow?}
F -->|Sí| G[Permitida]
F -->|No| H[Comportamiento del modo actual]
La especificidad no altera el orden: Bash(aws *) en deny bloquea Bash(aws s3 ls) aunque esté en allow. No hay excepciones dentro de un deny.
Patrones por herramienta
{
"permissions": {
"allow": [
"Read",
"Glob",
"Grep",
"WebSearch",
"Bash(git *)",
"Bash(bun test *)",
"Bash(bun run build)",
"Bash(ls *)",
"Bash(cat *)",
"Edit(src/**)",
"mcp__context7__*"
],
"ask": [
"Bash(git push *)",
"WebFetch"
],
"deny": [
"Bash(rm -rf *)",
"Bash(curl * | bash)",
"Bash(git push --force *)",
"Read(.env*)",
"Edit(.env*)"
]
}
}
Desde v2.1.210 solo
Edit(path)yRead(path)se evalúan en los chequeos de permisos de archivo.Write(path),NotebookEdit(path)yGlob(path)se aceptan pero nunca coinciden y generan un warning de arranque. Si teníasWrite(.env*)en deny, no protegía nada: hay que escribirlo comoEdit(.env*)(yRead(.env*), que desde v2.1.208 bloquea también Edit sobre esa ruta).
Sintaxis de patrones
| Patrón | Significado |
|---|---|
* | Cualquier secuencia de caracteres, incluidos espacios, en cualquier posición |
** | Recursivo en directorios (ver nota de anclaje abajo) |
Tool | Toda la herramienta sin restricción |
Tool(pattern) | Herramienta con patrón específico |
Tool(param:value) | Match por parámetro, solo válido en deny y ask |
Reglas de forma que conviene tener presentes:
- Un
denycon nombre desnudo (Bash) elimina la herramienta del contexto del modelo; con especificador (Bash(rm *)) la deja disponible y bloquea solo las coincidencias.EndConversationes la excepción: ningún deny/ask la quita mientras quede otra herramienta. - El match por parámetro
Tool(param:value)(por ejemploAgent(model:opus)oBash(run_in_background:true)) funciona solo endenyyask, nunca enallow. Los campos con canonicalización propia (command,file_path,path,url,notebook_path) se ignoran en esta forma y emiten warning. - Wildcards en el nombre de herramienta:
denyyaskaceptan globs completos ("*","mcp__*");allowsolo acepta glob tras el prefijo literalmcp__<server>__. Por eso"mcp__context7__*"es válido en allow, pero un"mcp__*"en allow se descarta con warning.
Profundidad de los patrones de ruta
Desde v2.1.214 un patrón de un solo segmento como Edit(src/**) coincide solo con src en el directorio de trabajo. Para cualquier profundidad hay que escribirlo así:
{
"permissions": {
"allow": [
"Edit(src/**)",
"Edit(**/src/**)"
]
}
}
Esto aplica tanto a las reglas de permisos como al campo if de los hooks.
Anclajes de ruta (Read/Edit)
Las rutas siguen la especificación gitignore con cuatro anclajes, y son contraintuitivos:
| Forma | Significado |
|---|---|
//path | Absoluta desde la raíz del filesystem |
~/path | Relativa al home |
/path | Relativa al origen del settings (project root, ~/.claude, …) |
path o ./path | Relativa al cwd |
Es decir, Read(/Users/alice/file) no es una ruta absoluta: se resuelve contra el origen del settings. La absoluta se escribe Read(//Users/alice/file).
Wildcards de Bash
*cubre cualquier secuencia, incluidos espacios, en cualquier posición.- El espacio antes del
*impone frontera de palabra:Bash(ls *)matcheals -lapero nolsof;Bash(ls*)matchea ambos. - El sufijo
:*equivale a*y solo se reconoce al final del patrón.
Operadores de shell rompen el match. Bash(safe-cmd *) no autoriza safe-cmd && other-cmd: los separadores reconocidos son &&, ||, ;, |, |&, & y los saltos de línea, y cada subcomando se evalúa por separado. PowerShell usa la misma forma y canonicaliza alias (gci, ls, dir → Get-ChildItem).
Wrappers eliminados antes de evaluar (lista interna, no configurable): timeout, time, nice, nohup, stdbuf, los builtins command y builtin, el noglob de zsh y xargs sin flags.
En cambio no se eliminan runners como npx, docker exec, devbox run, mise exec o direnv exec. Consecuencia práctica: Bash(devbox run *) autoriza cualquier comando interno.
Herramientas y sus patrones
| Herramienta | Qué filtra el patrón | Ejemplo |
|---|---|---|
Bash(...) | El comando a ejecutar | Bash(npm test *) |
Read(...) | Ruta del archivo (con anclajes gitignore) | Read(**/src/**) |
Edit(...) | Ruta del archivo (con anclajes gitignore) | Edit(*.ts) |
WebFetch(...) | Dominio, con prefijo domain: | WebFetch(domain:github.com) |
Agent(...) | Nombre del subagente o parámetro | Agent(code-reviewer), Agent(model:opus) |
Skill(...) | Nombre del skill | Skill(testing) |
mcp__srv__tool | Herramienta MCP | mcp__playwright__* |
La herramienta de subagentes se llama Agent: se deshabilitan o filtran con Agent(NombreAgente) en deny.
Wildcards de dominio en WebFetch
WebFetch compara el prefijo domain: contra el hostname, sin distinguir mayúsculas. Dos sorpresas relevantes para seguridad:
WebFetch(domain:*.example.com)cubre subdominios a cualquier profundidad pero noexample.com.- Fuera de un
*.inicial o de un*solo, el wildcard no cruza puntos:example.*matcheaexample.orgpero noexample.evil.com.
Sandbox
El sandbox aísla la ejecución de comandos bash con mecanismos del sistema operativo: macOS (Seatbelt), Linux y WSL2 (bubblewrap + socat). Windows nativo y WSL1 no están soportados.
Comportamiento por defecto
- Escritura permitida solo en el cwd y el
$TMPDIRde sesión. - Lectura de todo el equipo, salvo los directorios denegados explícitamente.
- Si el sandbox no arranca, por defecto se emite un warning y los comandos corren sin sandbox: falla abierto. Para que falle cerrado hay que poner
sandbox.failIfUnavailable: true.
Activar sandbox
# Via CLI
/sandbox
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true
}
}
Configuración del sandbox
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
"failIfUnavailable": true,
"excludedCommands": ["docker", "ssh"],
"network": {
"allowedDomains": ["github.com", "registry.npmjs.org"]
}
}
}
Campos de sandbox
| Campo | Tipo | Descripción |
|---|---|---|
enabled | boolean | Activar sandbox |
autoAllowBashIfSandboxed | boolean | Auto-aprobar bash dentro del sandbox (default true) |
excludedCommands | string[] | Comandos que salen del sandbox |
allowUnsandboxedCommands | boolean | Permitir comandos fuera del sandbox |
failIfUnavailable | boolean | Fallar en vez de correr sin sandbox |
allowAppleEvents | boolean | Permitir Apple Events (macOS) |
enableWeakerNetworkIsolation | boolean | Aislamiento de red más débil |
enableWeakerNestedSandbox | boolean | Sandbox anidado más débil |
allowUnixSockets | boolean | Permitir sockets Unix |
allowLocalBinding | boolean | Permitir bind a puertos locales |
filesystem.* | objeto | Rutas de lectura/escritura permitidas y denegadas |
network.* | objeto | Aislamiento de red |
credentials.* | objeto | Denegado o enmascarado de credenciales |
Rutas del sandbox
sandbox.filesystem.* usa convenciones estándar y distintas a las de las reglas de permisos:
| Forma | Significado |
|---|---|
/tmp/build | Absoluta |
~/ | Home |
./ o sin prefijo | Raíz del proyecto (settings de proyecto) o ~/.claude (settings de usuario) |
Aquí no existe el // de Read/Edit. Además, sandbox.filesystem.disabled: true (v2.1.216+) apaga el aislamiento de filesystem manteniendo el de red, y solo se honra desde settings de usuario, managed o --settings.
Network isolation
En network isolation no hay ningún dominio pre-permitido: el proxy pide aprobación en el primer uso y, desde v2.1.191, un «Yes» permite ese host el resto de la sesión.
| Campo | Descripción |
|---|---|
allowedDomains | Dominios pre-permitidos sin preguntar |
allowLocalBinding | Permitir bind a puertos locales |
tlsTerminate | Terminar TLS (experimental, v2.1.199+) |
allowManagedDomainsOnly | Solo dominios de managed settings (managed-only) |
También se pre-permite con reglas allow de WebFetch(domain:...).
Limitación importante: el proxy decide por hostname y no termina TLS por defecto, por lo que dominios amplios habilitan domain fronting. network.tlsTerminate termina TLS pero no filtra contenido.
Credenciales
sandbox.credentials (v2.1.187+) permite denegar o enmascarar credenciales:
{
"sandbox": {
"credentials": {
"files": [{ "path": "~/.aws/credentials", "mode": "deny" }],
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] }
]
}
}
}
files[].modeacepta"deny".envVars[].modeacepta"deny"o"mask".mask(v2.1.199+) sustituye la credencial por un sentinel por sesión y el proxy inyecta el valor real solo hacia losinjectHosts. Requierenetwork.tlsTerminatey se ignora si viene de settings de proyecto o local.
El sandbox también deniega escrituras a los settings.json de todos los scopes (resolviendo symlinks desde v2.1.210).
Combinando permisos con sandbox
La guía actual apoya el trabajo autónomo en tres mecanismos: auto mode, allowlists vía /permissions y sandboxing OS-level vía /sandbox. Ya no se recomienda --dangerously-skip-permissions.
claude --permission-mode auto -p "fix all lint errors"
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
"failIfUnavailable": true,
"network": {
"allowedDomains": ["github.com", "registry.npmjs.org"]
},
"credentials": {
"files": [{ "path": "~/.aws/credentials", "mode": "deny" }]
}
},
"permissions": {
"allow": [
"Read",
"Glob",
"Grep",
"Edit(**/src/**)",
"Bash(bun test *)",
"Bash(git *)"
],
"ask": [
"Bash(git push *)"
],
"deny": [
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Read(.env*)",
"Edit(.env*)"
]
}
}
Permisos en equipos
Compartir configuración de permisos del proyecto vía .claude/settings.json (commiteable):
{
"permissions": {
"allow": [
"Read",
"Glob",
"Grep",
"Bash(npm test *)",
"Bash(npm run lint *)"
],
"defaultMode": "default"
}
}
Override personal vía .claude/settings.local.json (gitignored):
{
"permissions": {
"allow": [
"Edit(**/src/**)",
"Bash(docker *)"
]
}
}
Las reglas de permisos no siguen la precedencia normal
Las listas se fusionan: las reglas del equipo + las personales aplican juntas. Y esto es la excepción documentada a la precedencia de settings: las reglas allow/ask/deny se fusionan entre todos los scopes, y un deny en cualquier scope bloquea un allow de cualquier otro, incluido --allowedTools.
Los arrays de settings se fusionan y deduplican entre scopes, salvo fallbackModel (máx. 3), que no se fusiona.
Restricción de scope de defaultMode: "auto"
defaultMode: "auto" se ignora si viene de .claude/settings.json o .claude/settings.local.json (desde v2.1.142), para que un repositorio no pueda autoconcederse auto mode. Debe ir en ~/.claude/settings.json o en managed settings.
Bloqueos organizacionales
{
"permissions": {
"disableBypassPermissionsMode": "disable",
"disableAutoMode": "disable"
}
}
En managed settings estas claves no pueden sobrescribirse ni con flags de CLI. disableBypassPermissionsMode funciona desde cualquier scope.
Workspace trust
permissions.allow y permissions.additionalDirectories de un settings de proyecto solo aplican tras aceptar el diálogo de workspace trust; deny y ask no lo requieren. Además, additionalDirectories concede solo acceso a archivos: no carga skills ni subagentes, a diferencia de --add-dir.
Dónde se guardan las aprobaciones interactivas
Desde v2.1.211, las aprobaciones «Yes, don’t ask again» se guardan en .claude/settings.local.json en la raíz del repositorio git (resuelta a través de worktrees), y ese archivo se carga desde la raíz aunque la sesión se inicie en un subdirectorio.
Siguiente: Plugins