Working with coding agents
n8n-decanter is built to let AI coding agents work on workflows safely. A scaffolded sync dir (init) contains everything an agent needs to behave:
AGENTS.md— the tool-agnostic contract for the repo: how code is stored here (placeholders,code/, markers), the file-ownership rules, the rename checklist, and how to verify changes. Codex and opencode read it natively; Claude Code reads it through a one-line import inCLAUDE.md.- Per-agent configs — Claude Code, Cursor, Codex, opencode — kept as thin
pointers to
AGENTS.md, so every agent follows the same rules. - Guard hooks — on Claude Code and opencode, edits that would break a
hard invariant are blocked before the write happens; a Claude Code
PostToolUse hook runs
preflight --offlineafter node edits. A second PostToolUse hook watches MCPupdate_workflowcalls and speaks up when arenameNodeleaves$('Old Name')references behind — n8n’s rename rewrites the node name and connections only, so those refs are the caller’s to repair (seepull). It scans for the old name rather than runningpreflight, because it fires before the background snapshot refresh, while every reference still resolves. The same rules are enforced by the CLI at push time regardless of who made the edit. Each hook finds the sync dir from its own installed location, so it behaves the same whether the agent was started in the sync dir or above it. On Claude Code these live in.claude/settings.json— project scope, meant to be committed, so everyone who clones the repo gets the same permissions and hooks..claude/settings.local.jsonstays yours for machine-specific rules: permission lists merge across the two and adenybeats anallow, so your local file can add to the policy but cannot unblock what the project denies.
Restart the agent after init. MCP servers, permission rules and hooks are
read at agent startup, and init is normally run from inside the very
session it configures — so that session has no n8n-instance tools and no deny
rules until it restarts (or /reloads). There is no hot-reload; init prints
the reminder when it first scaffolds those files, and the scaffolded AGENTS.md
tells the agent to ask for a restart rather than route around the missing guard.
But a restart is only half the diagnosis. “.mcp.json declares
n8n-instance and the tools aren’t there” has two causes, and they need
opposite answers:
| Cause | Symptom is | Fix |
|---|---|---|
The wiring is new — init ran inside this very session |
temporary | restart / /reload |
| The wiring is below the launch dir — nested sync dir, agent started at the repo root | permanent | start the agent in the sync dir, or wire the root (below) |
The discriminator is a path comparison: is the .mcp.json in question below
the directory the agent was started in? If it is, no restart can ever load it
— startup only reads the launch dir and its ancestors (next section), so
restarting reruns exactly the discovery that already missed the file. Advising a
restart there costs the user a session and teaches them nothing. The scaffolded
AGENTS.md carries both branches, so an agent that reads it can tell them
apart; init names the one you are actually in when it scaffolds.
Where the agent wiring loads from
init scaffolds .mcp.json, .claude/settings.json and the hook scripts
into the sync dir. When the sync dir is where you start the agent, that is
the end of the story. When it is a subfolder of a bigger repo, what actually
loads depends on where the agent was started — and the files disagree about
it (matrix verified against Claude Code 2.1.x):
| Agent started at | <syncdir>/.mcp.json |
<syncdir>/.claude/settings.json |
repo root’s .claude/settings.json |
|---|---|---|---|
| the repo root | not loaded | not loaded | loaded |
| the sync dir | loaded (merged with the repo root’s) | loaded | not loaded |
.mcp.jsonwalks up. Every ancestor of the launch directory is read and merged, nearest wins — so a nested one is additive for an agent started inside the sync dir, and unreachable from above. Nothing ever scans downward..claude/settings.jsonis launch-directory only — no walk in either direction. A nested one contributes nothing to a root-launched session: not permissions, not hooks, notenv. Nothing reports this; you get a hook that never runs and deny rules that were never in force..claude/settings.local.jsonis the one exception — it is read from the repository root, so the root’s local file is the one thing that still reaches a session started in a subdirectory, while thesettings.jsonbeside it does not. That makes it a trap when you test the nested case: a setup verified throughsettings.local.jsonlooks fine while the committedsettings.jsonnext to it is inert.initwrites no local file, so it is not a fix path either.--add-dirdoes not rescue it. It grants file access to another directory; it does not turn that directory into a settings source.${CLAUDE_PROJECT_DIR}is not a shortcut either — it expands to the agent’s project root, i.e. the parent, so in a root-level file it reads as if it pointed at the sync dir and never does. Write the sync-dir prefix out.- Hooks have no discovery of their own — they ride the
hookskey of those same settings files. Their scripts do locate the sync dir themselves (from their own installed path), so a hook that runs behaves identically whether the agent started in the sync dir or above it. What still has to be right is the command path in whichever settings file declares it: the scaffoldednode .claude/hooks/verify.mjsneeds aflows/prefix in a root-level file. - Permission patterns anchor at the settings file’s own project root, which
makes a verbatim hoist worse than a no-op:
Edit(workflows/**)then matches nothing, and — the sharp end —Read(.env)/Edit(.env)stop protecting<syncdir>/.env, the credentials file. Prefix every relative path pattern with the sync dir if you move the block up — theBash(…)rules and an already-**/-anchored one likeEdit(**/.decanter.json)carry over as they are.
Two shapes work, and the first is the recommendation: start the agent in
the sync dir (zero configuration — you only give up the parent repo’s root
settings.json), or wire the repo root deliberately. Root wiring means an MCP
entry carrying both N8N_DECANTER_DIR and a command that resolves from the root
— spelled out in
mcp connect
— plus the prefixing above for any hooks and permissions you hoist.
init prints
both options when it scaffolds into a nested directory.
The hard invariants
Violating these corrupts sync state, which is why they’re machine-enforced:
jsCodeinworkflow.jsonnever contains code — only//@file:placeholders.- Never write an
@ts-n8n sha256:…marker line — push writes it, on line 1 of the compiled output (// n8n-decanter · <source path> · do not edit here · @ts-n8n sha256:… · v<version> <commit> <time>). Older nodes carry the same token as a trailing// @ts-n8n sha256:…line; neither belongs in a source file, in either position. .decanter.jsonis machine state — never edit it, never “fix” a hash.
Two boundary rules sit next to them: Code-node source is authored as files
here and synced by decanter — never edited on the instance (not in the UI,
not via n8n’s MCP tools or skills); and workflow.json is a read-only
snapshot — structure changes go through n8n. n8n-decanter is built to pair
with n8n’s official skills pack: see Using n8n’s official skills
for how the MCP guard (mcp connect; mcp serve for URL-only harnesses) makes
that boundary safe by construction.
Who runs what
| Commands | Agent policy |
|---|---|
preflight --offline, node run, scenario |
Offline and safe — run freely (scenario create --scaffold is the exception; it needs MCP). Adding --simulate stays credential-free but boots a local Docker engine — minutes, not milliseconds, so opt in deliberately. |
preflight, diff, list --remote |
Read the remote, no writes — safe, but they do contact the instance. preflight is the gate (exit 1 when not ready); diff is the view and always exits 0. |
pull, push, watch |
Sync code with the instance. A push lands on the draft and never changes what is running, so it is part of finishing the work — code that only exists in the folder is not done. Say a word first if the workflow is published/active or a teammate is editing it. |
publish, unpublish, push --publish |
Change what is actually live — only when the user explicitly asks. Never fold going live into “finishing the work”. |
| Structure/lifecycle acts over n8n’s MCP (create, add/wire nodes — via the guard) | Building the structure a request describes is part of the work. Renaming or archiving something that already exists is not — ask first. After a structure act, pull reconciles the local mirror. |
test |
Grades the workflow’s draft on the instance. Bare it is a static check — dangling $('…') references, nothing executes, no capture needed. With --execution/--scenario it executes the draft (pinned trigger/network nodes, real logic nodes); the live version is never affected and non-interactive runs never write. Either way it is only meaningful after a push — before one, the draft holds the old code (or nothing). It is the post-push check in preflight → push → test → publish, and the static half is what publish refuses on. |
Archiving (MCP archive_workflow) |
Outward-facing — the workflow leaves the active list; a published one goes offline. Reversible only in the n8n UI. Never without an explicit instruction to archive that workflow. |
push --force |
Never without explicit instruction — it overrides the per-node drift guard protecting code edited on the instance. |
The default loop for an agent: orient → edit → verify
(preflight, or preflight --offline to stay
credential-free) → push → test (the draft now holds your code) → say
what landed and what the test showed. Stop before publish unless the user
asked for it. See The offline feedback loop.
Orienting is the same preflight, run before the first edit rather than
after the last one — it reads the instance and writes nothing. Its sync tier
answers the question an agent otherwise discovers too late: did someone edit
this code in the n8n UI while you were away (drift — then
pull and carry on), did both sides move (CONFLICT — stop
and show the user diff before either version is
overwritten), is a push from an earlier session still pending (parity).
Skipping it is not destructive — push refuses to overwrite remote edits — but
the work may have to be redone.