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, wrap a call, watch it in ghost. Connect your control plane
(Anvil: Connect to your control plane) when you want ghost and enforce to sync live.
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?" |
The blue anvil is the earned state: it appears only when Anvil is actually enforcing.
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.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, and
"Sign in with Anvil" (Google) will land in the same command once hosted auth ships.
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 "couldn't reach control plane"? The toggle shells out
to the anvil CLI, which needs Redis. The lens still reflects state correctly; only
the flip needs the backend.
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;
Cursor and VSCodium pull the same build from Open VSX.
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.