Skip to content
buliwyf42 edited this page May 23, 2026 · 1 revision

Frequently Asked Questions

Distilled from CHANGELOG entries, ADRs, and real operator questions. If yours isn't here, file it as an issue using the Feature request template (label it docs or question) and the answer can land here.

Deployment

Why won't the container start? It says the encryption key is required.

Since v0.3.0, SHELLYADMIN_ENCRYPTION_KEY (or SHELLYADMIN_ENCRYPTION_KEY_FILE) is mandatory. The binary refuses to boot without it, because storing the encryption key next to the encrypted credentials it protects defeats the at-rest encryption (a volume snapshot would carry both halves).

Generate one once and reuse it for the lifetime of the deployment:

openssl rand -base64 32

Store it in your secrets backend (Docker secret, sops file, etc.), expose via SHELLYADMIN_ENCRYPTION_KEY_FILE for production. A new key orphans all previously stored device credentials, so a rotation is a database wipe — plan it as one.

See ADR-0013.

First boot drops me into a setup screen instead of asking for admin / my password. Why?

Since v0.4.0 (ADR-0017) the operator login lives in the database, not in an env var. A fresh instance with no admin_credentials row renders /setup and waits for you to create the account in the browser.

If you'd rather pre-seed (for unattended provisioning, CI, etc.), set SHELLYADMIN_PASS_HASH on the first boot — the hash is imported into the DB once, then ignored. Generate it with docker run --rm ghcr.io/buliwyf42/shellyadmin:latest hash-password '<plaintext>'.

To rotate the password later, use Settings → Operator Account in the UI. To recover from a forgotten password, docker exec shellyadmin shellyctl reset-auth --force clears the row and drops back into setup mode.

I'm upgrading from v0.2.x. What changed for the login?

Two things:

  1. SHELLYADMIN_PASS (plaintext) was removed in v0.2.0. Only SHELLYADMIN_PASS_HASH (argon2id PHC) works.
  2. v0.4.0 moved the credential into the database (see above). Your existing SHELLYADMIN_PASS_HASH will be imported into the DB on the first v0.4.0+ boot, then ignored. After that the DB is authoritative; you can remove the env var.

What's the difference between docker run mode and Docker Compose mode?

Functionally nothing — both produce the same single container. The Compose file in docker/docker-compose.yml is the recommended production shape because it carries the hardening flags (read_only: true, cap_drop: [ALL], cap_add: [CHOWN, DAC_OVERRIDE, SETGID, SETUID, KILL], no-new-privileges: true, pids_limit: 256, init: true) that a plain docker run example omits.

Device support

Why isn't Gen1 supported?

Gen1 devices speak HTTP-GET CoAP-style; everything Gen2+ speaks JSON-RPC 2.0 over POST /rpc. The bulk-action / provisioning / firmware-check architecture is built against the JSON-RPC surface, and the Gen1 protocol would need a parallel adapter for every feature. The project explicitly targets Gen2+ to keep the surface coherent.

Why does Shelly.GetDeviceInfo return a 404 error code?

Shelly devices use a non-standard JSON-RPC error code (404 instead of the standard -32601) when a method isn't supported on a specific model. ShellyAdmin handles both. If you see "Method Not Found" on something common like Shelly.Update, the device's firmware is probably older than 1.0.0 — bring it forward in the device's own web UI first.

Why are auto-update toggles in ShellyAdmin missing for some devices?

The Schedule-based auto-update mechanism (ADR-0009) needs firmware 1.2.0 or later on the device. Older firmware doesn't expose the Schedule.Create job shape we use. Update the device firmware first, then the toggle appears.

MCP server

How do I enable the read-only MCP server?

Two paths. Pick one:

  • Env var (precedence, recommended for headless deploys): set SHELLYADMIN_MCP_TOKEN=<random-string> in the container env. The HTTP listener binds on :8081 automatically.
  • Settings UI: navigate to Settings → MCP Server → enable, paste a token. Encrypted at rest. The env var wins if both are set.

Clients can authenticate with either Authorization: Bearer <token> or the URL-path form http://host:8081/<token>/ — convenient for mcp-remote which doesn't natively expose a headers field.

MCP HTTP vs stdio — when do I use which?

  • HTTP (default): remote clients over the network. Auth via the token. Use this for Claude Desktop on a different machine, Home Assistant integrations, scripts that hit the API.
  • stdio: only for Claude Desktop running on the same host as the container, via shellyctl mcp subcommand. No transport-level auth — the spawning process IS the trust boundary. Practical if you want the lightest possible local setup.

Why aren't there state-changing tools in MCP?

By design (ADR-0011, ADR-0014). State-changing tools (refresh, scan, firmware install, bulk actions) exist but are confirm-gated: every state-changing call returns a typed preview when called without confirm: true. The LLM is expected to summarise the preview to the operator and only re-call with confirm: true after explicit approval.

Auth

What's the difference between cookie sessions and Personal Access Tokens?

Cookie sessions are for browser users (SPA), bound by CSRF. Personal Access Tokens (PATs, since v0.3.0) are for headless callers (Home Assistant, cron, scripts) — bearer-token authentication that skips CSRF because the token itself is the proof-of-intent.

PATs have scopes (admin, devices:read/write, firmware:read/write, provision, settings:read/write) and individual revoke. Minting / listing / revoking PATs requires a cookie session — a PAT-authed caller cannot escalate by minting more PATs.

How do I enable 2FA?

Settings → Two-Factor Authentication → Enroll. Scan the QR code with any RFC 6238 authenticator (Authy, 1Password, Bitwarden, the OS Keychain, etc.). Ten single-use backup codes are issued at enrollment — download and store them somewhere out-of-band. Wrong codes increment the same lockout counter as wrong passwords.

Compliance and provisioning

{device_name} shows up literally in my compliance results instead of being substituted

The token only substitutes against the configured device name (Shelly.GetConfigsys.device.name), not the auto-generated shelly-plus1-XXXXXX hostname. If the device hasn't been named, the token has nothing to substitute and stays literal. Set a name on the device first.

Why doesn't my MQTT compliance rule match even though the value looks right?

Two common reasons:

  1. Empty vs missing. ssl_ca: "" and ssl_ca absent are equivalent on the device, but a rule with ssl_ca: "" won't match a device where the field is absent. Use op: ne with an empty value, or op: exists: false.
  2. ssl_ca accepts only four literal values: "", "*", "ca.pem", "user_ca.pem". Anything else (custom hostname patterns, etc.) is silently rejected by the device.

Where do gen2_rpc provisioning template entries go?

Direct passthrough to Shelly's JSON-RPC. Each key is a method name (Sys.SetConfig, Mqtt.SetConfig, etc.) and each value is the params object. Use this when a configuration surface isn't yet covered by a dedicated section in the guided form — Zigbee operations, exotic schedule entries, FW-2.0 betas. The provisioner handles standard 404 / -32601 method-not-found responses gracefully.