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:
{
"permissions": {
"deny": [
"Bash(alegra * delete:*)",
"Bash(alegra * create:*)",
"Bash(alegra * update:*)",
"Bash(alegra invoices emit:*)",
"Bash(alegra invoices void:*)",
"Bash(alegra invoices stamp:*)",
"Bash(alegra invoices email:*)"
]
}
}
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 comandos alegra destructivos.
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
if printf '%s' "$cmd" | grep -qE 'alegra\b.*\b(delete|create|update|import|emit|void|stamp|email|open|close|transfer)\b'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Operación alegra destructiva bloqueada por política."
}
}'
fi
exit 0
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. Como deny le gana a allow, deniega los verbos de escritura en
vez de intentar hacer allow-list de las lecturas:
{
"permissions": {
"deny": [
"mcp__alegra__alegra_.*_(create|update|delete|import|void|emit|stamp|email|open|close|transfer)"
]
}
}
Los patrones de permiso MCP se evalúan como expresiones regulares, así que el
.* y el grupo (...) funcionan tal cual; las tools de lectura (..._list,
..._get, ..._export) quedan intactas. Un hook PreToolUse también puede
controlar tools MCP — pon "matcher": "mcp__alegra__.*" e inspecciona
.tool_name en el script para semántica de allow-list (deniega cualquier cosa
que no sea un verbo de lectura).
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 * delete*": "deny",
"alegra * create*": "ask",
"alegra * update*": "ask",
"alegra invoices emit*": "deny",
"alegra invoices void*": "deny",
"alegra invoices stamp*": "deny"
}
}
}
deny bloquea el comando; ask pide aprobación antes de ejecutarlo.
Superficie MCP — las mismas claves de permiso coinciden con los nombres de las tools MCP como patrones comodín, así que puedes denegar las tools de escritura de un servidor:
{
"permission": {
"alegra_*_delete": "deny",
"alegra_*_create": "ask",
"alegra_*_update": "ask",
"alegra_invoices_void": "deny",
"alegra_invoices_emit": "deny",
"alegra_invoices_stamp": "deny"
}
}
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.