Anvil — Protect Risky Calls (VS Code / Cursor)
Your code, and the AI agents you ship, take real actions in real systems: they move money,
call APIs, write to your database, run workflows. Without limits, one wrong step becomes a
real incident: a runaway bill, a deleted table, a leak you can't take back.
Anvil is the boundary those actions run through, and this extension brings it into your
editor. Put your cursor on a risky call, an agent action, or a workflow, press Protect,
and it's wrapped: safe to retry, wired to your control plane.
Every wrap starts in ghost mode: the real call runs, Anvil watches and records, nothing
is blocked yet. Flip it to enforce from your dashboard when the proof is obvious, and
the boundary holds: no double-charge, no overspend, no action it shouldn't take. Undo
anytime with Cmd/Ctrl+Z.
No account to start: install and wrap a call — the wrap is real, and Cmd/Ctrl+Z takes it back.
Watching it is the other half: a wrapped call isn't on a control plane until one is there, so
nothing is observing or enforcing it yet. In an Anvil project the editor can start a plane on your
machine for you (anvil.autoStartLocalPlane), and Anvil: Connect to your control plane points
it at your hosted or self-hosted one whenever you're ready.
Five ways to protect a call
All produce the byte-identical wrap (same codemod as the Anvil CLI and MCP server):
| Trigger |
When to use |
| CodeLens "Protect verb — why" above the call |
One click; appears automatically on recommended calls |
| Right-click → Protect with Anvil |
On the call your cursor is on (any call, not just recommended) |
Cmd+Alt+A (Ctrl+Alt+A on Windows/Linux) |
Keyboard. Recommended/write calls wrap instantly; read-looking calls (db.query, getUser …) confirm first so you don't silently wrap a read |
Cmd+Shift+P → "Anvil" |
Command palette |
Type anvil (or protect) on a call line |
IntelliSense offers an Anvil completion that wraps that line's call |
Lifecycle (the inline icon tells you the state)
| State |
Inline icon |
Meaning |
| Unprotected |
grey anvil, on the lens |
Anvil suggests protecting this; nothing's wrapped yet |
| Ghost |
a ghost, before the call |
Wrapped, watching only: it records duplicates and blocks nothing. Lens above offers "Enforce verb?" |
| Enforce |
a blue anvil, before the call |
Live and enforcing the boundary. Lens above offers "Back to ghost?" |
| Off |
none — the call is unmarked |
Switched off in Anvil: not blocking, not deduping, not recording. Lens above offers "Anvil is off for verb — turn back on?" |
| Removed |
none — the call is unmarked |
Removed in Anvil. Any wrapper still in the file is inert (it observes in ghost and cannot block). Lens above offers "Anvil removed this — take the wrapper out?" |
The blue anvil is the earned state: it appears only when Anvil is actually enforcing. The two
unmarked states are deliberately quiet — a call Anvil is not acting on should not wear a badge
that says it is.
Removing an action
Remove Anvil from this action (right-click, or the command palette) turns Anvil off for a call
completely and takes the wrapper out of your file. It's distinct from Off, which pauses Anvil
without touching your source. Your decision history and receipts are kept either way.
When someone removes an action in the Anvil dashboard, the leftover wrapper in your code is inert,
and the editor takes it out for you — saved to disk, with a toast naming the file and offering
Undo. It only ever touches a file this workspace actually wraps, found by the extension's own
scan and never by a path the server supplied. If the file has unsaved changes it stops and says so,
with Save and remove to finish it (pressing Cmd/Ctrl+S yourself works too); if it has
uncommitted changes it asks before editing. An action wrapped in more than one file is removed one
file at a time, and Anvil only reports it clear once no wrapper is left. Turn the whole behaviour
off with anvil.autoRemoveWrapper if you'd rather remove wrappers by hand.
Undo puts both halves back — the wrapper in your file and the action on your control plane, in
the mode it was in before (an enforcing action comes back enforcing). If your plane can't be
reached it says so rather than claiming success. The button lives on a toast, so if you miss it,
Anvil: Undo the last wrapper removal does the same thing from the command palette.
What it does on Apply
- Computes the wrap in memory and (for menu/lens) shows a side-by-side diff preview.
- Writes a single undoable edit (
WorkspaceEdit).
- Picks a durable idempotency key from the call's first argument (
orderId,
paymentIntentId, userId, …). If none can be inferred, it inserts a
// TODO(anvil) and lands your cursor on the key so you can type the real id.
- Folds the wrap boilerplate so the result is a clean, readable line.
- Offers to install
@useanvil/sdk if it's not already a dependency.
Never auto-wraps. Never writes without confirmation (except the keyboard shortcut on
already-recommended calls, which applies directly — Cmd+Z to undo).
Settings
anvil.enable — turn the lenses/actions on or off (default: on).
anvil.riskyActions — extra glob patterns to treat as risky, e.g.
["*.runTool", "queue.publish"] (added to the built-in money + destructive taxonomy).
anvil.includeComms — also flag email/SMS sends (off by default).
anvil.includeSideEffects — also flag generic side-effects: webhooks, queue publishes,
database writes, external API mutations (webhook.publish, queue.enqueue, api.post,
db.upsert). Off by default so Anvil flags only the highest-confidence calls (money,
destructive) with no false positives. On, it stays precise: strong external verbs flag
alone, ordinary mutating verbs flag only in an external-system context, and
collections/logs/reads (array.push, map.set, logger.write) never flag.
anvil.autoRemoveWrapper — after you remove an action in Anvil, take its leftover wrapper out
of your code automatically (default: on). This is the one setting that lets Anvil edit a file
you didn't ask it to touch in that moment, so it is deliberately narrow: only an action this
workspace wraps, never a file with unsaved or uncommitted changes, and always with Undo.
Turn it off to remove wrappers by hand from the lens instead.
anvil.controlPlaneUrl — origin of your Anvil control plane. One origin serves both the
dashboard UI (/app, opened by View in Anvil) and the API the editor talks to. Leave
blank to use the hosted Anvil control plane (the default). Point it at your own deployment
(e.g. https://your-app.up.railway.app) or http://localhost:7446 for a local control plane.
Commit it to .vscode/settings.json so your whole team targets the same control plane.
- Deprecated overrides (still honored for back-compat):
anvil.dashboardUrl overrides just
the View in Anvil page URL, and anvil.apiUrl overrides just the API base URL — useful
for a split dev setup (e.g. a dashboard mockup on :8088 with the API on :7446). Prefer
anvil.controlPlaneUrl; a value in either deprecated key wins over the derived origin.
Zero-config local plane
When anvil.controlPlaneUrl is blank and no token is set, the editor connects to a control
plane on your machine on its own — install, wrap a call, watch it get tracked, nothing to run
by hand. Three machine-scoped settings govern it:
anvil.autoConnectLocalPlane — detect a plane already running on localhost and read
ghost/enforce from it over http://127.0.0.1, no token (control reads are open on
loopback). On by default. Off keeps the editor on the optimistic ghost default until you
connect. Only auto-detect is affected — an explicit localhost anvil.controlPlaneUrl is
always honored.
anvil.autoStartLocalPlane — start one when none is running. In an Anvil project (the CLI
is installed) with no plane on the port, the editor runs anvil serve in the background; it
stays up as your local dashboard, single-instance, loopback only. On by default. Off keeps
the editor from ever starting a server. Anvil: Start local control plane works either way.
anvil.localPlanePort — the port to find and start the local plane on (default 7446).
Change only if you run yours elsewhere.
Control token (secure)
Pointing the editor at a control plane that requires auth — a self-hosted deployment
behind ANVIL_CONTROL_TOKEN — takes one command: Anvil: Sign in / Set control token
(Cmd/Ctrl+Shift+P → "Anvil"). The token is kept in the editor's secret storage,
never in settings.json, and is handed to the anvil CLI as ANVIL_CONTROL_TOKEN for
control-plane requests. Leave the box blank to keep the current token, or to remove it.
Without a token the editor stays local-only — it reads and writes your local control
plane exactly as before. A status-bar item tells you which: a key when a token is
configured, local only if you removed one. The token is never printed to logs.
For the hosted Anvil cloud there is nothing to configure by hand: run Anvil: Connect to
your control plane, leave the URL blank, and paste either the install token your dashboard
hands you (itk_…) or a project's existing ANVIL_API_KEY (anv_live_…). What gets stored is a
per-workspace, revocable key — never a shared operator secret, and never anything in
settings.json.
Connecting from your dashboard
Your Anvil dashboard can connect this editor for you: it opens a link your editor handles, the
editor exchanges it for your workspace credential, and you get "connected to <your workspace>". Nothing to copy, nothing to paste.
Because a link like that can be opened by any page, the editor treats it as untrusted:
- It will only ever talk to Anvil's own API, your
anvil.controlPlaneUrl, or your
anvil.apiUrl — and only over https (or http to your own machine). Any other address is
refused, and nothing is sent.
- It verifies the credential before storing it. The key is used once, against your control
plane, and only a successful read stores it and moves the status bar. A key that is refused,
revoked, or for a different workspace is discarded, and any credential you already had is left
exactly as it was.
- If this editor is already connected, it asks first, naming the plane it is currently on.
- The link is single-use and short-lived. Nothing about it is ever written to the output channel.
Troubleshooting
No lenses on a file you expected to be flagged? Open View → Output → "Anvil".
The channel logs slow parses (>250 ms), skipped large files (>250 KB), and codemod
errors with the file path so you can see why.
The keyboard shortcut opens a search bar? Cursor binds many Cmd+Shift combos to
its own search. The default is Cmd+Alt+A; rebind via Cmd+K, Cmd+S → search
"Anvil" → press your preferred keys.
Ghost/enforce toggle says it couldn't reach your control plane? Which path the flip
takes depends on where your control plane is. Against the hosted Anvil cloud (the default,
anvil.controlPlaneUrl blank) it goes over HTTP with your workspace key, so the fix is
usually Anvil: Connect to your control plane. Against a local or self-hosted plane it
goes through the anvil CLI, which needs that plane's store reachable from your machine.
Anvil: Refresh control state from Anvil reports which plane the editor is actually
talking to, which is the fastest way to tell the two apart.
Install
From the marketplace (recommended). Search "Anvil — Protect Risky Calls" in the
Extensions view, or from a terminal:
code --install-extension anvilcontrol.anvil-protect # VS Code
cursor --install-extension anvilcontrol.anvil-protect # Cursor / VSCodium (via Open VSX)
VS Code installs from the Visual Studio Marketplace.
VSCodium pulls the same build from Open VSX,
and Cursor resolves it through its own gallery — either way it's the identical artifact.
From a .vsix (offline / pre-release). Build with npm run package, then Extensions → …
→ Install from VSIX. Cursor runs VS Code extensions unmodified, so one build covers both.
Maintainers: RELEASE_CHECKLIST.md documents how releases are published.