Safety
arcane-mcp can do anything the credential it holds can do in Arcane, which includes deleting volumes, pruning images, restoring backups and managing users. These controls decide how much of that the assistant gets, and when it has to ask first. They live in the catalog and in the one dispatcher every surface calls, so they apply identically on the dynamic, meta and individual surfaces. The deployment side (who holds which credential, how the HTTP transport is hardened, the supply chain) is on Security.
Every call goes through the same steps, in this order:
- refuse an action the instance’s Arcane release is known not to have yet (see An action needs a newer Arcane);
- validate the arguments against the action’s input schema;
- for a destructive action without
confirm: true, ask the user through MCP elicitation when the client supports it, otherwise refuse; - in safe mode, answer a mutating action with a preview;
- run the action under the action deadline and render the result;
- write one audit record.
What counts as mutating and destructive
Section titled “What counts as mutating and destructive”Each action is classified when the catalog is generated:
- Read — Arcane
GETroutes, plus the live-stream actions that only listen (logs, stats, the event feed). - Write (mutating) — any other method: create, update, start, stop, pull, deploy…
- Destructive —
DELETEroutes and actions whose verb removes or irreversibly changes state: delete, remove, prune, kill, destroy, restore, purge, reset, wipe.
api/actions.yaml overrides the rule either way where the verb misleads; redeploys, for instance, are marked destructive there because they recreate containers, and the image update checks (image_update.check_ref, image_update.check_by_id) are GET routes marked mutating because they store their results, so read-only mode hides them and safe mode previews them. The tool reference shows each action’s kind.
Confirmation of destructive actions
Section titled “Confirmation of destructive actions”A destructive action does nothing until it is approved. There are two ways to approve it: the call carries confirm: true, or the user accepts a confirmation question the server sends through MCP elicitation.
confirm is a top-level argument on every surface — beside action and params on dynamic and meta, beside the parameters on individual — so it cannot be smuggled in as an Arcane parameter. The tools’ MCP annotations also set destructiveHint, which lets clients that support it ask the user before the call is even made.
When it depends on the arguments
Section titled “When it depends on the arguments”A few Arcane routes take the operation, or an option that destroys, as an argument, so one action mixes harmless and destructive calls. For those, only the destructive calls need approval; the rest run like any write:
| Action | Needs approval when | Runs without it |
|---|---|---|
podman_pod.lifecycle | action is kill | start, stop, restart, pause, unpause (as container.stop and friends) |
podman_quadlet.lifecycle | action is stop or restart (a .container quadlet’s unit deletes its container and anonymous volumes when it stops) | start |
podman.kube_play | replace is true (existing pods of the same names are removed first) | replace absent or false |
podman.auto_update_run | dryRun is absent or false | dryRun: true (it only reports) |
updater.run | dryRun is absent or false | dryRun: true (it only reports) |
project.up | recreateVolumes, forceRecreate or removeOrphans is true (volumes recreated with their data lost, containers recreated or removed, as project.redeploy) | a plain up (all three absent or false); on a GitOps-managed project Arcane removes orphans on every deploy, so a plain up still removes them without approval |
environment.update | regenerateApiKey is true (the agent using the old key is cut off) | every other change (name, URL, enabled…) |
swarm_node.agent_deployment_create | rotate is true (the old token is deleted) | reusing or creating the node’s environment |
environment.pair_agent | rotate is true (a manager using the old token loses access) | reading the local agent token (created if none exists) |
The judgement is made on the validated arguments, the same way on every surface, before elicitation, safe mode and the audit log see the call. An absent argument counts as the value Arcane uses by default, and a value the rule cannot compare fails safe: it needs approval. What does not change: such an action still carries destructiveHint (a hint must not promise less than a call can do), its individual tool keeps the confirm argument, read-only mode still hides it (it changes state either way), and its usage note and the tool reference say which values need confirm. A refused call says so: “podman_pod.lifecycle is destructive when action is kill, as in this call”.
Clients that support elicitation
Section titled “Clients that support elicitation”When the client declared the elicitation capability (form mode) at initialization, a destructive call without confirm: true does not fail: the server asks the user directly, in the client’s own interface, before anything reaches Arcane. The question names the action, what it acts on and what accepting means:
Run volume.delete (Delete a volume) on volumeName="data", environmentId="0" (default), force=true?This is destructive: it deletes or replaces state in Arcane and may not be reversible.Accept to run it now; decline to do nothing.-
Accept runs the action and returns its result as usual.
-
Decline or dismiss returns a result that is not an error and says nothing was done, so the model does not retry on its own:
The user declined to run volume.delete (Delete a volume) when asked to confirm it.Nothing was done. Do not retry it unless the user asks again. -
The question shows what will be sent: every argument, required ones first, then the environment (marked
(default)when the call relies on it), at most six with+N morefor the rest. Objects list their fields (volumes={enabled=true}), lists their length (resourceIds=[3 items]). -
Values are shown so they cannot rewrite the question: each string is folded onto one line, stripped of terminal sequences and control and bidi characters, cut at 80 characters and quoted, with any remaining invisible character escaped. File contents (
composeContent,envContent…) and strings over 200 bytes are shown as their size (envContent=(124 bytes)). -
The value of an argument whose name suggests a credential (containing key, token, secret, password, recovery, credential or passphrase, such as
recoveryKeyorstepUpToken) is shown as(hidden). Booleans are always shown (rotateManagerToken=truesays what happens, not a secret), and names ending inIdorName(secretId) identify something and are shown. -
confirm: truestill skips the question: the model was told to ask the user before setting it. -
If the client does not answer within 5 minutes, or the elicitation request fails, the server falls back to the refusal below with a note saying why, and nothing runs.
-
On protocol 2026-07-28 and later, where a server may not send requests of its own while serving a call, the question travels in the call’s result as an input request (multi round-trip requests) and the client retries the call with the answer. The answer is bound to the action and its arguments: it does not approve a different call.
-
ARCANE_MCP_ELICITATION=offturns the question off; every client then gets the refusal.
Clients without elicitation
Section titled “Clients without elicitation”Without the capability (or with elicitation turned off), the server answers:
container.kill is destructive (Kill container). Nothing was done.Repeat the call with confirm: true once the user has approved it.The model then asks the user in the conversation and repeats the call with confirm: true.
Notes that come with a confirmation
Section titled “Notes that come with a confirmation”When a call does more than its arguments show, its action carries a note that is added, word for word and the same on every surface, to the confirmation question, to the refusal above and to the safe-mode preview (as confirmNote). It appears only for calls that need approval, and only when its condition holds:
| Action | Shown when | Note |
|---|---|---|
updater.run | dryRun and automatic are absent or false | Containers that opted out of updates (label com.getarcaneapp.arcane.updater=false or the autoUpdateExcludedContainers setting) and have a pending update are recreated too, with or without resourceIds; pass automatic=true to leave them alone. |
updater.container_update | always | The container is updated even if it opted out of updates (same label or setting). |
Arcane v2.17.1 made manual updater runs ignore those opt-outs; the note keeps an approval from silently covering containers the user excluded.
Read-only mode
Section titled “Read-only mode”ARCANE_MCP_READ_ONLY=true (or --read-only) removes every mutating action from the catalog before any surface is built. The assistant does not see them: they are not in arcane_find_action results, meta tool enums, the individual tool list or arcane://tools. If the model names one anyway, on the dynamic surface (arcane_execute_action) or the meta surface (arcane_image with action prune), it is told why it is unavailable (on the individual surface the tool simply does not exist, so the client gets its unknown-tool error and no audit record is written):
action "image.prune" is not available: the server runs in read-only mode (ARCANE_MCP_READ_ONLY)Use it for assistants that should observe and diagnose, never change. Pair it with an Arcane API key of a user whose role cannot write either, so the limit holds even outside this server.
Safe mode
Section titled “Safe mode”ARCANE_MCP_SAFE_MODE=true (or --safe-mode) keeps mutating actions visible but does not send them. A mutating call — after validation and confirmation — is answered with a preview of what would have been sent:
{ "safeMode": true, "executed": false, "destructive": false, "action": "container.restart", "params": { "containerId": "web-1" }, "request": "POST /api/environments/0/containers/web-1/restart", "note": "Safe mode is on (ARCANE_MCP_SAFE_MODE or --safe-mode): this mutating action was not sent to Arcane."}This is the structured content. The text the model reads renders the same preview for people: a “Safe mode preview” heading, a bold NOT EXECUTED line, the request line, the query, whether the call is destructive and its confirmation note if any, then the parameters and the body as JSON blocks:
## Safe mode preview: Restart a container (container.restart)
**NOT EXECUTED.** Safe mode is on (ARCANE_MCP_SAFE_MODE or --safe-mode): this mutating action was not sent to Arcane. Nothing changed.
- **Request**: `POST /api/environments/0/containers/web-1/restart`- **Destructive**: noJSON bodies (up to 2,000 characters) and query strings are included, with credential-like values (passwords, keys, tokens, a secret’s data) shown as (hidden), as in the confirmation question. Read actions run normally. Safe mode never asks the user through elicitation: a destructive call without confirm: true is refused exactly as on a client without elicitation, and with confirm: true it is previewed. Safe mode is useful for trying prompts against a production Arcane, and for reviewing what an assistant would do before granting it write access.
Exclusions
Section titled “Exclusions”ARCANE_MCP_EXCLUDE_TOOLS (or --exclude-tools) removes chosen actions, as a comma-separated list of any of:
| Entry | Removes |
|---|---|
image.prune | one action, by canonical ID |
arcane_image_prune | one action, by individual tool name |
swarm_node | a whole domain |
arcane_swarm_node | a whole domain, by meta tool name |
ARCANE_MCP_EXCLUDE_TOOLS=user,role,api_key,settings,volume.deleteExcluded actions disappear from every surface and from arcane://tools, exactly like read-only mode, and a call that names one on the dynamic or meta surface is answered with excluded by the server configuration (ARCANE_MCP_EXCLUDE_TOOLS).
Credentials and Arcane’s own permissions
Section titled “Credentials and Arcane’s own permissions”Everything above narrows what the server offers. The outer limit is always the Arcane credential: the server acts as the user who owns the API key, and Arcane enforces that user’s roles on every request. A 403 comes back with a hint pointing at role assignments. Give the assistant a key from a user with the least role it needs.
The credential travels only where Arcane needs it. Public routes (login, token refresh, OIDC and passkey logins, version, health, app images and avatars) are called without it, since Arcane authenticates nobody there; settings.public keeps it, because Arcane checks the caller before forwarding to a remote environment. environment.pair and notification.dispatch send only the environment key passed in their apiKey parameter, never the server’s.
Per-client API keys in HTTP mode
Section titled “Per-client API keys in HTTP mode”In Streamable HTTP mode one server can serve several people, each acting as themselves:
- A client may send its own Arcane key in the
X-Arcane-API-Keyheader. The server then builds a separate Arcane client for that key (one per distinct key, reused across requests), and every action runs with that key’s permissions. - A per-client key is never mixed with the operator’s session: when a client brings its own key, the server’s
ARCANE_USERNAME/ARCANE_PASSWORDare not used for that client. - Without the header the server falls back to
ARCANE_API_KEY(or the configured username and password). If none is configured, the request is refused — so a deployment with no server-wide credential serves only clients that bring their own. ARCANE_MCP_HTTP_TOKENguards the endpoint itself: every request to/mcpmust carryAuthorization: Bearer <token>, compared in constant time./healthzstays open for probes.ARCANE_MCP_HTTP_AUTH=apikeyreplaces both: the Bearer token is the caller’s own Arcane key, checked against Arcane before it is served, with no shared token and no server-wide credential to fall back to. In both modes a client address that keeps failing authentication is refused for a while, and each key can be rate-limited. See HTTP authentication.
The listener speaks plain HTTP; terminate TLS in front of it before the bearer token and Arcane keys cross any network you do not trust.
Action deadline
Section titled “Action deadline”Each action runs under an overall deadline, ARCANE_MCP_ACTION_TIMEOUT (30m by default, or ARCANE_MCP_TIMEOUT when that is longer; an explicit value shorter than ARCANE_MCP_TIMEOUT is refused at startup). Ordinary requests are already bounded by ARCANE_MCP_TIMEOUT (60s), which stays in force inside the deadline; the deadline catches what that does not: progress streams (image pull and build, project up, pull and redeploy), downloads and actions that make several requests. A call that runs out of time is cancelled and answered with an error that names the deadline; Arcane may still finish work it had already started, so check its state before retrying.
Logs and audit trail
Section titled “Logs and audit trail”Logs go to standard error only, at ARCANE_MCP_LOG_LEVEL (info by default). Standard output carries the stdio protocol and nothing else.
Every tool call leaves one audit record at info level, on every surface:
level=INFO msg="tool call" surface=meta tool=arcane_volume action=volume.delete outcome=ok confirmed=user duration=412mslevel=INFO msg="tool call" surface=dynamic tool=arcane_execute_action action=container.get outcome=error duration=35ms status=404 key=3f9a1c0b7e21| Field | Meaning |
|---|---|
surface, tool, action | Where the call arrived and the catalog action it resolved to (for a call refused as read_only or excluded, the withheld action; absent when the name matched no action) |
outcome | ok, error (the action failed), refused (not run), declined (the user said no), previewed (safe mode), pending (a confirmation question was sent in the result; the retried call leaves its own record), panic (the action panicked; the server’s recovery answers the call and logs the panic) |
reason | Why a call was refused or declined: invalid_args, needs_confirmation, elicitation_failed, unknown_action, read_only and excluded (an action read-only mode or ARCANE_MCP_EXCLUDE_TOOLS withholds), bad_arguments, older_arcane (the instance runs an Arcane release older than the action), decline, cancel; or why it failed: action_deadline (ARCANE_MCP_ACTION_TIMEOUT), request_timeout (one request’s ARCANE_MCP_TIMEOUT), cancelled (the client cancelled) |
detail | With elicitation_failed, why asking failed (the elicitation error, which can come from the client) |
destructive | Only for an action whose confirmation depends on its arguments (see When it depends on the arguments): whether this call counted as destructive |
confirmed | How a destructive action that ran (including one that failed or panicked) was approved: flag (confirm: true) or user (elicitation); absent on previews |
duration, status | Time spent, including the wait for the user; Arcane’s HTTP status when it answered with an error |
key | In HTTP mode, a fingerprint of the Arcane API key the call ran with (the first 12 hex digits of its SHA-256), never the key |
Argument values are never logged. ARCANE_MCP_AUDIT_LOG_ARG_NAMES=on adds the names of the arguments passed (args=containerId,force); ARCANE_MCP_AUDIT_LOG=off turns the records off, as does a ARCANE_MCP_LOG_LEVEL above info.