Handle a confirmation-required refusal
What you’ll have at the end: a correct three-step response to a
confirmation_required refusal — understand the envelope, preview the exact
request with --dry-run, then execute deliberately with --confirm and a
pinned --idempotency-key.
1. Recognize the envelope — and why the refusal is correct
Section titled “1. Recognize the envelope — and why the refusal is correct”Call a gated mutation without confirmation and Anvil refuses with a structured envelope, never a raw upstream error:
anvil run generated/payments refunds create --payment-id pay_123 --amount 500 --currency USD --json{ "error": { "code": "confirmation_required", "message": "This operation creates an irreversible financial mutation.", "retryable": false, "safe_to_retry": false, "operation": "payments.refunds.create", "trace_id": "trace_5adcfc5e-a11e-4800-b041-997c55fa981d", "required_flags": ["--confirm", "--idempotency-key"] }}The exit code is 3 (needs-flags — the stable exit-code taxonomy is: 2 input,
3 needs-flags, 4 auth, 5 policy, 6 upstream state, 7 upstream availability).
The refusal is correct behavior, not an obstacle. createRefund is an
irreversible financial mutation; the contract says explicit refusal beats
accidental execution. Everything needed to proceed deliberately is in the
envelope: message says why, required_flags says exactly what to add, and
safe_to_retry: false says re-sending the same command blindly is not an
answer. If you are an agent reading this envelope: stop and check the user’s
intent — --confirm asserts their intent, not yours.
2. Preview with --dry-run first
Section titled “2. Preview with --dry-run first”Add the required flags plus --dry-run to see precisely what would happen
without any side effect — the dry-run short-circuits before auth and before
anything leaves the machine:
anvil run generated/payments refunds create --payment-id pay_123 --amount 500 --currency USD \ --dry-run --confirm --idempotency-key refund-pay_123-2026-07-13 --json{ "operation": "payments.refunds.create", "method": "POST", "url": "https://payments.internal.example.com/payments/pay_123/refunds", "headers": { "accept": "application/json", "content-type": "application/json", "Idempotency-Key": "refund-pay_123-2026-07-13" }, "body": { "amount": 500, "currency": "USD" }, "idempotencyKeyPresent": true, "retryPlan": { "enabled": true, "maxAttempts": 3 }, "confirmationRequired": true}Check the plan: right URL, right amount, idempotency key present and injected into the declared header, retries enabled only because the key makes them safe.
The whole flow, executed against the repo’s payments example:
# [docs-tested]WORK=$(mktemp -d)node packages/cli/dist/bin-anvil.js compile examples/payments/openapi.yaml \ --manifest examples/payments/anvil.yaml --service payments \ --out "$WORK/payments" --root "$WORK"# 1. The bare call refuses with the structured envelope, exit code 3.set +eREFUSAL=$(node packages/cli/dist/bin-anvil.js run "$WORK/payments" refunds create \ --payment-id pay_123 --amount 500 --currency USD --json 2>&1)STATUS=$?set -etest "$STATUS" -eq 3echo "$REFUSAL" | grep -q '"code": "confirmation_required"'echo "$REFUSAL" | grep -q -- '--idempotency-key'# 2. Dry-run with the required flags: a full request plan, zero side effects.PLAN=$(node packages/cli/dist/bin-anvil.js run "$WORK/payments" refunds create \ --payment-id pay_123 --amount 500 --currency USD \ --dry-run --confirm --idempotency-key refund-pay_123-2026-07-13 --json)echo "$PLAN" | grep -q '"idempotencyKeyPresent": true'rm -rf "$WORK"3. Execute — deliberately
Section titled “3. Execute — deliberately”Only when the user intends the effect, drop --dry-run and keep everything
else:
anvil run generated/payments refunds create --payment-id pay_123 --amount 500 --currency USD \ --confirm --idempotency-key refund-pay_123-2026-07-13Pin the idempotency key yourself and make it meaningful
(refund-<payment>-<date> beats a random UUID in an audit log). Reusing the
same key lets the durable ledger detect the same request and lets the upstream
carrier enforce its own idempotency contract. A completed replay returns the
stored result while its retention window is active; a concurrent replay is
refused as in_progress. This is deliberately not an exactly-once claim: if the
upstream succeeds and the runtime crashes before recording completion, Anvil
leaves the reservation in progress for operator reconciliation instead of
guessing that a retry is safe. A new key is a new refund. A
required_request_key operation refuses when the key is absent; Anvil only
derives a deterministic request-fingerprint key for the separate
key_supported strategy, where the caller key is optional.
If it refuses again: read the new envelope — the code will have moved on.
idempotency_required means the operation demands a key and none was supplied
or derivable (required_flags: ["--idempotency-key"]). auth_required
(exit 4) names the credential env var to set. policy_denied (exit 5) means
the upstream host is not allowlisted for this environment — a deployment
decision, not a flag. None of these are retry-until-it-works situations;
each envelope tells you the one thing it needs.
- The same gates guard every place the operation shows up (CLI, MCP, hooks).
Over MCP, the equivalent arguments are
confirm: true,idempotency_key, anddryRun: true— and the harness hooks deny a missing flag before the call even leaves the harness. - Operations marked
human_approvalcannot be cleared with--confirmat the harness layer at all — see Require human approval.