Seguridad para agentes: controlar operaciones destructivas¶
Dejar que un agente de IA toque tu contabilidad es útil pero riesgoso: puede
crear facturas, registrar pagos, eliminar registros y emitir facturas
electrónicas ante la DIAN/SAT. Esta página muestra cómo asegurarte de que un
agente no pueda ejecutar las operaciones que no quieres — ya sea que maneje
alegra por un shell o por el servidor alegra mcp.
Ten claro qué hace cada capa:
| Capa | Mecanismo | Qué hace | ¿Hace cumplir? |
|---|---|---|---|
| 1. Anotaciones de tools | alegra mcp marca cada tool como solo lectura o destructiva |
Permite que un host que respete anotaciones controle las escrituras por su cuenta | Advisoria — depende del host |
| 2. Config del host | reglas deny / ask y un hook PreToolUse | El hook inspecciona el comando real y no se esquiva con trucos del shell — el bloqueo definitivo | Sí — esta es la barrera real |
| 3. Built-ins del CLI | --dry-run, confirmación de delete |
Previsualización y confirmación manual en una terminal | Solo shell (no la superficie MCP) |
El resumen honesto: la capa 1 hace que los buenos hosts se porten bien por defecto, pero no es un límite de seguridad. La capa 2 es la que de verdad bloquea. Usa ambas.
El hook es la capa que no se puede esquivar
Los globs de deny/ask son fáciles de leer, pero un comando de shell bien
armado puede esquivarlos (comillas, subshells, &&). Un hook PreToolUse
corre tu código contra el comando o tool real antes de ejecutarlo, así que
es la capa que bloquea de forma definitiva — en las superficies shell y
MCP. alegra agent guard --host claude-code te lo genera. (Los hooks son una
función de Claude Code; Codex bloquea con un sandbox de solo lectura,
OpenCode con reglas deny.)
La vía rápida: alegra agent guard¶
alegra te genera la config de la capa 2, con las operaciones destructivas
derivadas del árbol de comandos en vivo (así la lista siempre está completa):
alegra agent guard --host claude-code # imprime settings.json + el hook PreToolUse
alegra agent guard --host codex # imprime config.toml
alegra agent guard --host opencode # imprime opencode.json
Por defecto bloquea de forma definitiva las acciones irreversibles (delete,
void, emit, stamp, close, y las acciones *-delete) y hace que las
escrituras normales (create, update, import) requieran aprobación; las
lecturas quedan permitidas. Flags:
--all-writes— bloquea toda escritura, no solo las irreversibles.--write— instala los archivos en vez de imprimirlos (nunca sobreescribe una config existente; esa la imprime para que la mergees).
Revisa la salida, pégala en tu host y listo. El resto de la página explica qué genera y cómo ajustarla a mano.
Qué cuenta como destructivo¶
Las operaciones de lectura de alegra son list, get y export (más
catalog, reports, doctor, version). Todo lo demás escribe: create,
update, delete, import, y las acciones de recurso void, emit, stamp,
email, open, close, transfer, …
Capa 1 — anotaciones MCP integradas¶
alegra mcp anuncia la naturaleza de cada tool con las anotaciones estándar de
MCP: las tools de lectura llevan readOnlyHint: true y las de
escritura/acciones llevan destructiveHint: true. Lo obtienes gratis, sin
configurar nada.
Lo que eso te da depende por completo del host:
- Un host que respeta anotaciones (p. ej. Codex) pedirá aprobación para las tools destructivas y dejará correr las de solo lectura, automáticamente.
- Un host que controla por nombre de tool (p. ej. Claude Code) ignora la anotación para las decisiones de permiso — ahí configuras reglas (capa 2).
Las anotaciones son advisorias según la spec de MCP: un host que las ignore ejecuta todo. Nunca reemplazan la capa 2.
Nombres de las tools
Las tools MCP se llaman alegra_<recurso>_<subcomando> — p. ej.
alegra_invoices_void, alegra_contacts_delete, alegra_invoices_list.
Los hosts les agregan namespace; en Claude Code una tool es
mcp__<server>__alegra_<recurso>_<subcomando>.
Capa 2 — enforcement por host¶
Claude Code¶
Claude Code controla por nombre de tool/comando, y deny siempre le gana a
allow. Pon esto en el .claude/settings.json del proyecto (compartido con
el equipo) o en ~/.claude/settings.json (todos los proyectos).
Superficie shell — bloquea los comandos alegra destructivos con reglas
exactas por comando (Claude evalúa las reglas de permiso como patrones de
prefijo literales; un glob intermedio como alegra * delete es poco fiable y
puede tapar o perder comandos):
{
"permissions": {
"deny": [
"Bash(alegra invoices delete:*)",
"Bash(alegra invoices emit:*)",
"Bash(alegra invoices void:*)",
"Bash(alegra invoices stamp:*)",
"Bash(alegra contacts delete:*)",
"Bash(alegra bills close:*)"
]
}
}
alegra agent guard --host claude-code emite la lista completa (una regla
exacta por comando irreversible, derivada del árbol de comandos vivo) para que
no la mantengas a mano.
Usa "ask" en vez de "deny" para pedir aprobación en lugar de bloquear de
plano. Un comando denegado nunca corre; en una sesión headless/CI ask también
falla cerrado (sin humano que apruebe → bloqueado).
El bloqueo definitivo es un hook PreToolUse. Los globs de deny de arriba
ayudan, pero un comando de shell bien armado puede esquivarlos (comillas,
subshells, &&); un hook corre tu código contra el comando real antes de
ejecutarlo, así que no se puede esquivar — y el mismo hook cubre también la
superficie MCP. Esta es la pieza más importante en Claude Code. Crea
.claude/hooks/block-alegra-writes.sh:
#!/usr/bin/env bash
# Lee el payload del hook desde stdin y deniega un comando alegra destructivo.
# Compara la RUTA DE SUBCOMANDO exacta en posición de comando — un grep de verbo
# suelto como `alegra.*delete` también denegaría líneas benignas como
# `alegra invoices create --data 'nota: delete luego'`. El patrón anclado de
# abajo también atrapa binarios invocados por ruta (./bin/alegra,
# /usr/local/bin/alegra) sin coincidir con otro binario que apenas termine en
# "alegra".
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
if printf '%s' "$cmd" | grep -qiE '(^|[;&|([:space:]]+)([^[:space:]]*/)?alegra[[:space:]]+invoices[[:space:]]+delete([[:space:]]|$)'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Operación alegra destructiva bloqueada por política."
}
}'
fi
exit 0
Ese es el esqueleto para un comando. El hook generado
(alegra agent guard --host claude-code) hace esto para todos los comandos
irreversibles y añade des-ofuscación (quita comillas/backslashes y aplana
saltos de línea, así alegra invoices vo""id 1 también cae), un fallback sin
jq que falla cerrado, y la rama MCP. Límites conocidos: la indirección por
variables (a=void; alegra invoices $a 1), los alias de shell y eval no se
detectan — y como se quitan las comillas, un comando bloqueado entre comillas
dentro de un argumento (p. ej. rg "alegra invoices delete" src/) también se
deniega, un falso positivo conservador deliberado. Para una garantía dura corre
el agente solo-MCP o en un sandbox de solo lectura.
Regístralo en .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-alegra-writes.sh" }
]
}
]
}
}
Un hook PreToolUse que imprime permissionDecision: "deny" (con exit code 0)
bloquea la llamada; el exit code 2 también bloquea y muestra stderr al modelo.
Es la opción más fuerte — corre tu lógica, no un glob.
Superficie MCP — cuando alegra corre como servidor MCP local
(claude mcp add alegra -- alegra mcp start), deniega las tools de escritura
por nombre exacto. Las reglas de permiso de Claude Code no interpretan
expresiones regulares — un patrón como mcp__alegra__alegra_.*_(void|delete)
nunca coincide con nada — así que lista cada tool:
{
"permissions": {
"deny": [
"mcp__alegra__alegra_invoices_delete",
"mcp__alegra__alegra_invoices_void",
"mcp__alegra__alegra_invoices_emit",
"mcp__alegra__alegra_invoices_stamp",
"mcp__alegra__alegra_contacts_delete"
]
}
}
alegra agent guard --host claude-code emite la lista exacta completa. Un hook
PreToolUse también puede controlar tools MCP — los matchers de hooks (a
diferencia de las reglas de permiso) sí son regex, así que
"matcher": "mcp__.*alegra.*" manda cada llamada de tool alegra al script, que
compara .tool_name contra el conjunto bloqueado.
Codex¶
Codex controla con dos ajustes en config.toml (~/.codex/config.toml):
sandbox_mode y approval_policy.
Superficie shell — la barrera más fuerte y simple es quitar el acceso de escritura:
sandbox_mode = "read-only" # Codex puede leer; editar/comandos/red requieren aprobación
approval_policy = "untrusted" # solo las lecturas seguras corren solas; lo demás pregunta
Con sandbox_mode = "read-only", Codex no puede correr un comando alegra que
cambie estado sin una aprobación explícita — sin lista de comandos que
mantener. Si quieres que las escrituras sean posibles pero siempre revisadas,
deja approval_policy en un modo que pregunte para que cada escritura se pause
ante un humano.
Superficie MCP — aquí es donde la capa 1 rinde. La doc de Codex dice que las
llamadas a tools MCP con anotación destructive siempre requieren aprobación.
Como alegra mcp marca void/emit/delete/update/… como destructivas,
Codex pregunta antes de ejecutarlas automáticamente, mientras que las de solo
lectura corren sin fricción. No necesitas config por tool; deja approval_policy
en un modo que pregunte para que la aprobación se respete.
Nota honesta: en Codex la barrera es aprobación (un humano dice que sí), no
un bloqueo duro — está bien para uso interactivo, pero implica que una corrida
desatendida de Codex debería usar sandbox_mode = "read-only" para que no haya
nada que aprobar.
OpenCode¶
OpenCode controla con un bloque permission en opencode.json. Cada regla es
"allow", "ask" o "deny", y gana el último patrón que coincide, así que
pon el comodín primero y las denegaciones específicas después.
Superficie shell — bash acepta globs por patrón:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"bash": {
"*": "allow",
"alegra invoices delete": "deny",
"alegra invoices emit": "deny",
"alegra invoices void": "deny",
"alegra invoices stamp": "deny",
"alegra contacts delete": "deny",
"alegra contacts create": "ask",
"alegra contacts update": "ask"
}
}
}
deny bloquea el comando; ask pide aprobación antes de ejecutarlo. Prefiere
reglas exactas por comando en vez de globs de verbo como alegra * delete* —
un glob intermedio puede coincidir por error con un argumento que contenga el
verbo y es más difícil de revisar. alegra agent guard --host opencode emite
la lista exacta.
Superficie MCP — las mismas claves de permiso coinciden con los nombres de las tools MCP, así que puedes denegar las tools de escritura de un servidor por nombre exacto:
{
"permission": {
"alegra_invoices_delete": "deny",
"alegra_invoices_void": "deny",
"alegra_invoices_emit": "deny",
"alegra_invoices_stamp": "deny",
"alegra_contacts_delete": "deny",
"alegra_contacts_create": "ask"
}
}
Los nombres exactos (en vez de globs alegra_*_delete) también cubren tools
compuestas como alegra_bills_attachment-delete, que un glob con sufijo
_delete se pierde. Confirma el prefijo exacto que tu OpenCode usa para el
servidor y haz coincidir el patrón (el nombre de la tool es
alegra_<recurso>_<subcomando>).
Capa 3 — built-ins del CLI¶
Cuando el agente usa un shell, dos built-ins ayudan incluso sin config del host:
--dry-runen cualquier comando imprime la petición exacta y no envía nada.deletepide confirmación salvo que pases-y.
Estos no cubren la superficie MCP: una llamada a tool MCP no tiene terminal,
así que la confirmación interactiva de delete no aplica. En la superficie MCP,
apóyate en las capas 1 y 2.
Configuración recomendada¶
Defensa en profundidad:
- Deja que la capa 1 haga su trabajo — viene activa por defecto.
- Agrega la capa 2 de tu host: una regla
deny(oask) y un hook en Claude Code;sandbox_mode = "read-only"en Codex; denegaciones depermissionen OpenCode. - Mantén
--dry-runen tus hábitos y scripts.
Pruébalo: pídele al agente que ejecute alegra invoices void 1 (o que llame la
tool alegra_invoices_void) y confirma que queda bloqueado o pausado para
aprobación antes de que algo llegue a la API.