Agent guard¶
n8nctl agent guard generates host-level safety config so an AI agent driving
n8nctl — through Bash or through the MCP server — cannot run
destructive n8n operations. It produces permission rules and hooks for the host
you name and either prints them for review or installs them.
n8nctl agent guard --host claude-code
n8nctl agent guard --host codex
n8nctl agent guard --host opencode --all-writes
n8nctl agent guard --host claude-code --write
What it protects against¶
Left unguarded, an agent that can call n8nctl can delete workflows, drop data
table rows, or remove credentials — all irreversible over the API. The guard
fences those operations at the host level, before the agent's tool call ever
reaches the CLI.
Default posture vs --all-writes¶
The default posture splits operations into three tiers:
- Hard-block the irreversible operations:
delete,delete-rows, andprune, plus the destructive raw-api methods (n8nctl api DELETE/PUT/POST/PATCH). The agent cannot run these at all. - Require approval for ordinary writes:
create,update,activate,deactivate,archive,transfer,restore,sync,apply,retry,stop,packages import, member changes, and the rest. These pause for a human to confirm. - Allow reads (
list,get,search,lint,diff,schema,members,audit,backup) to run freely.
Pass --all-writes to fold the write tier into the hard-block set: with it,
every state-changing operation is blocked outright and only reads run.
The operation list is derived from the live command tree and the MCP tool
annotations, not hardcoded. Regenerate the config after upgrading n8nctl and
any new actions are classified and covered automatically.
Review and install¶
The guard prints its output for review by default. Pass --write to install the
files. Installation never overwrites an existing file — if a target already
exists, the guard leaves it untouched so it cannot clobber config you have
already customized.
Hosts¶
Claude Code¶
For --host claude-code the guard emits two files:
.claude/hooks/n8nctl-guard.sh— aPreToolUsehook that hard-blocks the MCP-tool branch and best-effort-blocks the Bash branch..claude/settings.json— deny/ask permission rules plus the hook wiring.
The hook matches blocked operations by exact subcommand path at the command
position, not by bare verbs anywhere in the line. That means
n8nctl workflows create --set name=delete-old is allowed (the blocked word is
in an argument), while n8nctl workflows delete 42 is denied — including after
;, |, &&, an env prefix, a newline continuation, or when the binary is
invoked by path (./bin/n8nctl, /usr/local/bin/n8nctl). A different binary
that merely ends in n8nctl is not matched. The blocked set enumerates every
built-in alias spelling (wf delete, exec prune, dt delete-rows, …), and
quotes/backslashes are stripped first so de""lete-style obfuscation cannot
slip past. The MCP branch is an exact set-membership check on the tool basename,
so it covers any MCP server name and cannot false-match a near-miss tool name.
The hook (excerpt):
blocked_cmds=(
'workflows delete'
'workflow delete'
'wf delete'
'executions prune'
...
)
blocked_tools=(
'n8n_workflows_delete'
...
)
...
case "$tool" in
Bash)
raw_cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
cleaned=$(deobfuscate "$raw_cmd")
if bash_is_blocked "$cleaned"; then
deny "n8nctl agent guard: irreversible operation blocked."
fi
if api_is_blocked "$cleaned"; then
deny "n8nctl agent guard: destructive raw-api call blocked."
fi
;;
mcp__*)
base="${tool##*__}"
for t in "${blocked_tools[@]}"; do
if [ "$base" = "$t" ]; then
deny "n8nctl agent guard: irreversible MCP tool blocked (${tool})."
fi
done
;;
esac
The permission rules in .claude/settings.json are belt-and-suspenders: exact
per-command deny rules for the destructive operations (one per alias
spelling), ask for the writes, plus exact MCP tool names:
{
"permissions": {
"ask": [
"Bash(n8nctl workflows create:*)",
"Bash(n8nctl workflows update:*)",
"Bash(n8nctl packages import:*)",
"mcp__n8nctl__n8n_workflows_create"
],
"deny": [
"Bash(n8nctl workflows delete:*)",
"Bash(n8nctl wf delete:*)",
"Bash(n8nctl api DELETE:*)",
"Bash(n8nctl api POST:*)",
"mcp__n8nctl__n8n_workflows_delete"
]
}
}
Codex¶
For --host codex the guard emits ~/.codex/config.toml. Codex has no
per-command deny hook, so the read-only sandbox is the hard block and the
destructive MCP tools (which n8nctl annotates) already require approval:
sandbox_mode = "read-only"
approval_policy = "untrusted"
Under the default posture, writes pause for approval and reads run free; switch
sandbox_mode to workspace-write only if you accept unattended writes. With
--all-writes, the read-only sandbox means no n8nctl write can run without an
explicit approval.
OpenCode¶
For --host opencode the guard emits opencode.json permission rules. deny is
a hard block and ask prompts; the specific n8nctl patterns take precedence
over the * catch-all, which allows everything else:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"bash": {
"*": "allow",
"n8nctl workflows create": "ask",
"n8nctl workflows update": "ask",
"n8nctl workflows delete": "deny",
"n8nctl wf delete": "deny",
"n8nctl api DELETE": "deny",
"n8nctl data-tables delete-rows": "deny"
},
"n8n_workflows_create": "ask",
"n8n_workflows_update": "ask",
"n8n_workflows_delete": "deny",
"n8n_data-tables_delete-rows": "deny"
}
}
MCP-only is the strongest guarantee¶
The guard fences two surfaces, and they are not equally strong:
- The MCP-tool branch is a hard block. MCP tool names are structured and cannot be obfuscated, so the guard matches them exactly (by tool basename, covering any server name) and denies the destructive ones outright.
- The Bash branch is best-effort. It defeats quote/backslash obfuscation,
flattens newlines so a split verb cannot slip past the match, covers
path-invoked binaries and every built-in alias spelling — but it cannot
defeat variable indirection (
a=delete; n8nctl workflows $a 42), shell aliases, or user-definedn8nctl aliasexpansions.
The strongest configuration is therefore to run the agent MCP-only (no Bash
access to n8nctl) — or in a read-only sandbox — combined with the guard. That
way the only operations available are the MCP tools, with the
destructive ones hard-blocked. Because agent guard is itself excluded from the
MCP surface, an agent cannot disable its own rails.
Known limitations¶
- The
n8nctl apiescape hatch. The guard blocksn8nctl api DELETE/PUT/POST/PATCHat the method position on the Bash surface (aGETwhose path merely contains "delete" is not blocked), but it cannot enumerate arbitrary path arguments, and then8n_apiMCP tool cannot be classified by verb. Treat raw-api access with the same care as Bash access. - Conservative false positives. De-obfuscation strips quotes before
matching, so a quoted blocked string at a command position — e.g.
rg "n8nctl workflows delete" src/— is denied. This errs on the safe side; unquoted or regex-style search patterns are unaffected. - Variable indirection, shell aliases, and
n8nctl alias. These rewrite the command line outside the hook's view. MCP-only operation or a read-only sandbox is the hard guarantee.
See also¶
- MCP server — the tool surface the guard fences, its naming, and the read-only/write/destructive annotations the guard derives its rules from.