#Ways to protect an action
Anvil acts on a call once that call is wrapped, or once Anvil is in its path. Four routes get you there, and the question that actually decides between them is the one people ask first: does this change my code?
Two of them do, and that is the honest cost of naming a call precisely — you point at one line and say this is the one that must not run twice. Two of them don't. All four end in the same place: one action on your dashboard, in ghost, with a decision log filling up underneath it.
#The four routes
| Route | Changes your code? | What it needs | What it covers |
|---|---|---|---|
| Your editor — the Protect lens | Yes — one undoable edit | The extension, connected | The call in front of you |
The dashboard — + Protect new | No — it registers the action and sets its mode | A code map, or a runtime Anvil is already in | The mode outright; otherwise it hands you the wrap |
The terminal — anvil protect <action> | Yes — a codemod writes the wrap | A call site it can parse | Any matching call it finds in your project |
A startup flag — --import @useanvil/sdk/init | No | Node 20+ and one of four covered packages | Eight calls across stripe, @octokit/rest, twilio and resend — no source read at all |
Which to reach for. If the call is on your screen, use the editor — you already have the context the wrap needs. If you'd rather not touch the repo at all and it's a Stripe money call, use the flag. If you're scripting setup or working in CI, use anvil protect. And once the first action is in, most teams add the rest from the dashboard, because that's the window they're already in.
#Your editor
Open a file and every risky call picks up a Protect lens above it. Click it, read the side-by-side diff, apply — the wrap lands folded to a single line, the action registers in ghost, and the call itself picks up a marker showing its live mode. One Cmd+Z undoes it.
Reach for this one when the call is in front of you, because the hardest part of a wrap is the dedup key — what makes two of these the same call — and that judgment is freshest while you're looking at the code. A tool can guess orderId; only you know whether a retry means the same refund.
The step people skip: signing into the dashboard does not connect your editor. Your browser session and your editor are different clients, and the extension needs its own credential. Until it has one, wrapping still edits your file correctly and Anvil tells you plainly that the registration didn't happen — you get wrapped, but not registered with a button to fix it, rather than a silently empty dashboard. Full walkthrough: Protect from your editor.
#The dashboard
+ Protect new, bottom-right of the sidebar in /app. Type an action name and Anvil searches your project for it, then does whichever of three things is true.
If Anvil is already in that call's path — because the startup flag covers it — there is nothing to write. Pick Ghost or Enforce and you're finished; it takes effect on the next call. If the call is in your code but unwrapped, Anvil stages the mode and hands you the wrap three ways: open it in your editor, copy the anvil protect command, or copy a prompt for an AI agent. And if the search can't read your project at all, it says unsearchable rather than guessing — the third answer, and the one covered just below. That second case is the one to understand, because it is where the expectation usually breaks — the dashboard does not reach into your repository and edit it. Anvil only ever acts on a wrapped call, so the wrap still has to happen; the dashboard just makes it one click away.
It is also careful about what it doesn't know. Unsearchable is not "not found" — they are different answers, and only one of them means the call isn't there. On a hosted workspace it is usually not a failure at all: the dashboard holds no copy of your source, because your code lives on your machine and not on our server. Publish where your calls are with anvil sync from the project, and the search starts answering — the same command fixes a map gone stale after you move or rename files.
Ghost has ten slots. A workspace holds ten ghost actions at a time; at ten the Ghost button goes flat with "Ghost slots are full — graduate or remove one to free a slot. You can still Enforce." Enforce never takes a slot, and a removed action gives its slot back. It is a product rule about how much unfinished work is useful to hold at once, not a capacity limit — so the way through is to graduate one to enforce, which is the direction you wanted anyway.
#The terminal
npx -y @useanvil/sdk@latest protect refund.issue
Unattended, in CI, it is:
CI=1 npx -y @useanvil/sdk@latest protect refund.issue --dir . --yes --json
--yes takes the single-project prompt, --dir pins the project, and --json puts a machine-readable envelope on stdout. Without --yes the command waits on a prompt no runner can answer.
A codemod finds the call, previews a one-line change, and writes it. This is the route Quickstart walks, the only one that works unattended in a script, and the surest thing to reach for when a search has already failed you — here you can name the callee exactly rather than hoping a label matches.
For money actions the wrap carries an amount, in integer cents, so spend rules have something real to read. When the codemod can't find one to wire up it says so and declines to advertise the enforce flip, rather than leaving you with a cap that silently never fires.
#A startup flag — protection with no code edit
For the calls Anvil ships an adapter for, you don't wrap anything. You add one flag to the command that already starts your app:
node --import @useanvil/sdk/init app.js
Anvil loads before your application does and puts itself in the path of the calls it recognizes, by patching each covered package's own methods in place — Stripe's resource-class prototypes, Octokit's endpoint methods, and the equivalent in Twilio and Resend. That reaches every client your app constructs — including ones built before Anvil loaded — without reading, parsing, or editing a single file of yours.
What it covers today, and this is the whole list — eight calls across four packages:
| Package | Tested versions | The call | The action it becomes |
|---|---|---|---|
stripe | 12–18 | stripe.refunds.create | payment.refund |
stripe.charges.create | payment.charge | ||
stripe.payouts.create | payment.payout | ||
stripe.transfers.create | payment.transfer | ||
@octokit/rest | stable 21–22 | octokit.rest.repos.delete | github.repo.delete |
octokit.rest.git.deleteRef | github.ref.delete | ||
twilio | stable 5–6 | twilio.messages.create | sms.send |
resend | stable 5–6 | resend.emails.send | email.send |
The flag needs a key. --import puts Anvil in the call path; it does not tell Anvil whose dashboard to ask. The process reads ANVIL_API_KEY from its own environment and polls the plane for each covered action, so a service started without that variable is instrumented and reporting to nobody. That is the one piece of configuration this route has — and on a production deploy it will not come from .env, which the SDK does not read when NODE_ENV=production.
A wrap and the flag on the same call is safe. Anvil notices both, uses your wrap(), ignores the instrumented layer for those calls, and warns once per action per process. Nothing is double-counted. The explicit wrap wins because it is the specific, intentional protection — someone chose that action name at that call site — and both resolve their mode from the same config, so deferring changes how many times a decision is applied, never which decision. Remove the wrap and let the flag cover it, or keep it; either is fine.
For money calls, the route changes what a spend cap can see. A wrap carries an amount you point at, in integer cents, so spend rules have something real to read. The flag reads whatever the provider call happens to carry, and when it finds nothing it marks the call as amountless rather than guessing — which means an amount-based rule has nothing to compare. If the action moves money and you want caps on it, wrap it.
It is inert until you configure it. This is the part worth reading twice, because "covered" and "protected" are not the same thing. A covered method with no mode set does nothing — a map lookup and a direct call to the original. No evaluation, no receipt, no counter. Your app behaves exactly as it did before you added the flag, and it costs you nothing. Protection starts when you pick Ghost or Enforce for that action in the dashboard, and it starts on the next call — no restart, no redeploy, typically within a few seconds of the click.
It refuses rather than guesses. Each adapter names the majors it is tested against, in the table above. Outside that range it does not patch at all and reports the version it found as unsupported. A wrong patch is the one failure that could break your payment code, so a truthful not covered is the cheaper mistake — it costs a row in the dashboard instead of a production incident. The same applies if a patch throws: everything for that module rolls back and your calls run unmodified.
One version note beyond those ranges. The ESM half of the hook needs module.register(), which arrived in Node 20.6. Below that a CommonJS app is still covered while an ESM app is reported as covering nothing — Languages has the full runtime-support picture.
How fast a flip reaches it. An instrumented process re-reads its modes every 5 seconds. ANVIL_INSTRUMENT_REFRESH_MS tightens or relaxes that, clamped between 1 and 60 seconds.
What it cannot see. It patches those four packages and nothing else. A call that doesn't go through one of them — a raw fetch to the Stripe API, a provider with no adapter, your own internal service — is invisible to it, and no amount of configuration will change that. Those you wrap. Partial coverage you can name is worth having; coverage you assume is worse than none.
To see what a running process actually has:
anvil doctor
The Auto-instrumentation section names each action as live via <package>@<version> — live via stripe@14.25.0 — or gives the reason it isn't covered — and reminds you that live means in-path and inert until a mode is set. To turn instrumentation off without changing your start command, set ANVIL_NO_INSTRUMENT=1.
Enforcing money needs a database. Ghost works right now with nothing configured, whichever route you take. But Anvil has to record that a charge already ran before it can refuse the second one, and that record has to outlive a Redis restart — so an enforce-mode money call needs durable storage. On Anvil Cloud that is ours and there is nothing to do; a self-hosted runtime needs ANVIL_POSTGRES_URL. See Do I need a database?.
#What to expect afterwards, whichever route you took
It starts in ghost. A newly protected action watches and records what it would have caught while your code runs exactly as before. Nothing is held, blocked, or intercepted until you flip it, which is why wrapping something in production is a safe thing to do on a Tuesday afternoon. Ghost → Enforce, safely walks the flip when you're ready.
It shows up in one place. Open /app → Actions, and your action is a row with a live decision log under it. If it isn't there, the usual cause is not a broken wrap — it's that the thing doing the wrapping and the dashboard you're looking at belong to different workspaces. Your app reports to whatever ANVIL_API_KEY is in its environment, and pairing a machine with anvil login does not rewrite an existing .env. Check that they agree before you go looking for anything subtler.
It comes back off cleanly. anvil remove <action> — or removing it from the dashboard — takes the wrapper back out of your source, byte for byte, and the extension does the same on its next refresh if you have it installed. Reviving an action restores the mode it had rather than guessing. Nothing here is a one-way door, which is the point: you should be able to wrap freely, and let a week of ghost tell you which actions were worth it.
#When the codemod can't find your call
The most common dead end, and it has three exits. The search reads .ts, .tsx, .js and .jsx source, so it misses calls in an .mjs or .cjs file entirely, along with calls that are generated, bundled, transpiled, unusually shaped, or simply named something other than what you typed.
- Name the callee exactly.
anvil protect refund.issue --call stripe.refunds.create— a lowercased action label can't match a camelCase call, and this is why. - Use the flag instead. If the call goes through
stripe,@octokit/rest,twilioorresend,--importnever searches your source at all. This is precisely the case it exists for. - Wrap it by hand.
safe.protecttakes a single call and needs no codemod — see In code.
If your application isn't JavaScript or TypeScript, start at Languages for the honest answer about what is and isn't covered — before you install anything.