Reolink Camera Operator
Overview
Primary surface: reolink-cli (JSON stdout by default). Don't start the MCP server unless asked. When the user needs non-CLI access (browser, curl, other languages), point them at the gateway's POST /api.
Slash commands (Claude Code only) — prefer these for fixed/single-action intents; skip the intent-mapping overhead:
| Slash | Equivalent | When to use |
|---|---|---|
/reolink-cli:status | reolink-cli status | user asks about current state / dashboard / "how are things" |
/reolink-cli:features | reolink-cli features | user asks what the plugin can do / "what can it do" |
/reolink-cli:scan | reolink-cli discover | scan local network / "scan" |
/reolink-cli:devices | reolink-cli device list | list registered cameras |
/reolink-cli:cache-clean | reolink-cli cache clean dry-run→apply | clear old snapshots / "clear cache" |
/reolink-cli:update | reolink-cli self-update | upgrade to the latest release (checks GitHub, no-op if current) |
/reolink-cli:uninstall | reolink-cli setup --uninstall --purge --no-interactive | complete uninstall |
Claude Code runtime: Must use the slash command when the user's intent matches one of these seven exactly — faster (no LLM latency), deterministic, and discoverable via / autocompletion.
Other agents (Codex / Cursor / Copilot / Gemini): these surfaces don't support plugin-defined slash commands, so the "slash-first" rule doesn't apply — skip straight to running the equivalent reolink-cli … CLI command directly. Skill-driven intent mapping handles everything else.
All agents: Fall back to skill-driven CLI chaining for compound / fuzzy intents ("announce when a person is detected", "make it brighter") that aren't covered by the slash list / seven fixed actions above.
The gateway is mandatory for almost every control command. The CLI routes through 127.0.0.1:9000 by default (set in config.toml under the platform config dir — ~/.config/reolink-cli/config.toml on Linux, ~/Library/Application Support/reolink-cli/config.toml on macOS, %APPDATA%\reolink-cli\config.toml on Windows — as gateway-addr). If you see gateway connect failed: Connection refused, the gateway isn't running — start it once:
reolink-cli gateway start --addr 127.0.0.1:9000 &
Works without the gateway: device add|list|update|remove|resolve|show, config init, discover, features, doctor, cache status|clean — everything that reads local config or talks to the network directly. (Verified by running each with no gateway listening.) Everything else (ping, login, info, config get/set, image, osd, ptz, detect, light, audio, preview, snapshot, vod, events, users, system reboot, system upgrade) needs the gateway up.
Envelopes — parse .ok on CLI, .status on gateway:
| Surface | Success | Error |
|---|---|---|
| CLI | {ok:true, command, protocol, data} | {ok:false, error:{code, message, retryable}} (stderr) |
| CLI batch | {command, summary, results:[{camera, host, ok, data, error}]} | per-target ok |
Gateway /api | {status:"success", data} | {status:"error", code, message} (HTTP status == code) |
references/ has topic-scoped recipe files. Must load only the one matching your task — references/index.md is the router. references/troubleshooting.md has diagnosis decision trees for failures.
Ground Truth (Binding)
Forbidden stating or implying as factual: device reachability, login state, configuration values, capabilities, detection/AI types, event counts, VOD file existence, user accounts, or any observable camera state — except when derived from the JSON output of an executed reolink-cli command or gateway POST /api response within the current task.
Forbidden: demo-style lists, guessed layouts, synthetic values, fake success after errors/timeouts, prose/JSON mimicking CLI output without a real response, or "the device has X" when no get/info/inventory/capabilities has run.
Must when data is missing: state what's missing and the cause (no camera registered, gateway down, auth failed, device offline), then take one concrete recovery step (from Error Recovery table) or ask a single clarifying question. Forbidden padding with plausible-sounding defaults.
Intent Interpretation
Resolve ambiguity before picking a command. If still unclear, ask with options.
| User says | Command | Why / caveat |
|---|---|---|
| add a user | users add | Device account, NOT a CLI camera entry |
| add a camera | device add | CLI camera entry (~/.config/reolink-cli/aliases.toml), NOT a device account |
| change password | users passwd (default) OR device update (CLI creds) | Ask which |
| blinking light | light statusled | Body LED |
| night vision | light ir | Invisible IR LED |
| spotlight | light spotlight (manual) OR light whiteled (alarm-triggered) | Same bulb, different wiring |
| too dark | image tune --bright first, check light ir get if night | Image tuning, not spotlight |
| upside down | image flip set --flip | Vertical |
| mirrored | image flip set --mirror | Horizontal |
| mask | privacy mask | Read current regions first |
| rename | osd set --name (on-screen) OR config set device-name (app) | Ask which |
| restart / reboot | system reboot | 30–60s offline |
| upgrade / flash firmware / OTA | system upgrade <file.pak> | ⚠️ bricking risk. Match model+hw_ver via info; GATEWAY reads the LOCAL file; auto windowed/stop-and-wait. Detail in references/admin.md |
| factory reset | — standalone; system upgrade --factory-reset resets during a flash | Standalone reset not in CLI; ask user to use Reolink app |
| kick off | users remove (permanent) OR users passwd + system reboot (evict session) | Active sessions survive users remove until reboot |
| snapshot | snapshot [--file PATH] | JPEG. Parent directories are auto-created by the binary (since v0.5.0) — do NOT pre-mkdir -p the destination. Same for vod download and preview capture --file. |
| slow / how long / time taken / performance / latency / benchmark | benchmark [--iterations N] [--phases ...] [--reuse-session] | Per-phase p50/p95/p99: connect, login, info, snapshot. --reuse-session for warm-path. Detail in references/index.md. |
| cpu / memory / device load / is the camera overloaded / stuttering | config get performance | Live reading from the device: cpuUsedPercent, codeRate, netDataRate. Read-only and instantaneous — two calls a second apart legitimately differ. Not every model implements it; those answer a device-level rejection. Distinct from benchmark, which times the client round trip, not the camera. |
| rtsp / rtmp / flv address / stream URL / HA / Frigate / go2rtc / VLC | stream url [--kind rtsp,rtmp,flv] [--stream main,sub,ext] [--with-auth] | Default --kind rtsp --stream main. --with-auth only when user explicitly wants one-shot pasteable URL. NVR: device expand then --tag <nvr>. Detail in references/media.md. |
| nvr with 8 channels / RLN sub-cameras / channel N | device expand <nvr-name> [--yes | --names A,B,C] | NVR only. Registers one entry per channel, tagged with parent name. After: --tag <nvr-name> fans out. |
| command rejected 400 / "model doesn't support it" / battery camera on a hub / first command after a pause | Nothing — the gateway already retries. attempts in the answer says which try got through (3 is normal after a pause, 1 during a burst) | A battery child behind a hub sleeps and then rejects commands with a bare 400, indistinguishable from unsupported. When it sleeps is not predictable from idle time alone (one day's readings said ~2 min; later overnight windows found it awake), so do not try to pre-empt it. What is consistent: a sleeping camera costs a flat 3 attempts (~1.6 s) to wake, and the refused request is itself the wake. If it still fails after the budget, info → channel.loginState = standby explains it — but that field lags, so it explains a failure, it cannot pre-flight one. |
| dual-lens / two lenses / bullet+PTZ in one camera / wide and telephoto / second lens | info first (channel.views, channel.dualLens), then --view N on config commands | --view is a third axis: --channel picks the camera, --view picks the lens, --stream picks that lens's encoding profile. Verified per-view: encode, privacy mask, detect motion, PTZ. Not per-view: snapshot (device answers 400 for view≥1), preview/stream url (protocol has no selector — these now reject --view N rather than silently handing back view 0), image/ISP (shared). Default 0 = the only view an ordinary camera has. |
| preview / take a look / watch N minutes / watch live | preview play (opens ffplay window) | DEFAULT to play, not capture. User wants a live window, not a file. |
| record N seconds / save a clip / save it | preview capture --packets ≈N×25 | Only when user explicitly says save / record / export. Packet count, not seconds; ~25 fps main, ~10 fps sub |
| 60-second preview (ambiguous!) | If user says "preview for N seconds/minutes" → preview play --packets N×fps. If user says "record N seconds" → preview capture. Ask if truly unclear. | Default to play when the verb is "preview/watch", capture only when the verb is "record/save/export" |
| volume | audio volume set (master) | Ask if they mean mute: audio mute |
| mute alarm | audio mute | Alarm audio only, not preview |
| siren / alarm sound / sound the alarm / scare them off / make it beep | audio siren play [--duration N | --times N] [--wait] | Loud + side-effectful — confirm first. cmd 263 manual ring-down (the app's siren button); device self-stops after --duration s, or audio siren stop. --wait holds for the duration then stops explicitly and reports stoppedAt + stopAcknowledged — use it when you must be able to say the siren stopped, not just that it started. A 400 from a battery camera behind a hub is not evidence about the hardware — a sleeping child answers 400 exactly like an unimplemented command. (Separately: on one Home Hub 2 the siren was measured not to sound at all even when the command succeeds — see verified: false below. Never claim it sounded.) The gateway retries (measured: a camera idle >2 min needs 3 attempts, ~1.6 s), and attempts in the answer tells you which try got through. If it still fails, info → channel.loginState = standby explains it — but that field lags, so it is for explaining a failure, not for checking before one. Never tell a user their camera lacks a feature on the strength of a 400; capabilities is what answers that. Same rule for light spotlight. "siren when motion is detected" is different — that is audio siren task linkage (often 405 on newer models). |
| arm / disarm / away / at home / i'm leaving / scene mode / stop recording while i'm in | scene set <id> (scene list for the ids, scene set --schedule to hand back to the timetable) | Hub/NVR only. One switch re-arms every channel — it does not touch per-camera detection config. scene show first: currentSceneId 0 means the timetable is driving. Detail in references/scene-mode.md. |
| resolution / bitrate / fps / frame rate / h265 / h264 / stream quality / make the stream smaller | encode set --stream main|sub|third ... | Run encode capability first — a value the device does not advertise comes back as a bare 400. stream url is unrelated (that only builds a playback URL). |
| person detection | detect ai --type person | Check device inventory --capabilities first |
| vehicle | detect ai --type vehicle | Not all models |
| turn on recording | record schedule set --enable | |
| record config / recording params / pre-record seconds / clip size | record config get | Read-only; cycle / pre/post record / package time |
| SD card / storage card / capacity / free space | storage status | Read-only; totalGB / remainGB / formatted / mounted |
| manual record / record now / start recording | — | Not supported on Reolink IPCs; fall back to record schedule set --enable/--disable |
| download yesterday / download recording | vod search --since 24h → vod download NAME | |
| download 09:45–09:55 / just that clip / a time range | vod download --from 2026-09-02T09:45:00 --to 2026-09-02T09:55:00 | Camera-side cut — do NOT download the whole hour and trim |
| any alarms / any alarms today | events query --since 24h | Requires gateway |
| event history further back than the live buffer / what happened last week / hub event log | --channel N events history --since 7d [--types people,motion] (hub/NVR only) | Reads the device's recorded event log (cmd 516/517), not the ~500-entry live ring. Standalone IPCs 400. Empty list = no matching events; a hub can go briefly quiet after heavy use, retry after a pause. |
| voice alert / voice announcement / announce when someone arrives / play voice when a person is detected | audio talk (see references/voice-alert.md) | PCM16 LE mono only; needs capabilities.audioTalk=1 |
| detect X do Y / automation / event trigger / detect-then-do | Must events monitor init/check/run with TOML rule. Forbidden ad-hoc bash while + events query loops — see references/event-monitor.md. | |
| is the monitor running / has a rule fired / monitor status / what fired recently | events monitor status (pid + cursor + lastFires + per-rule expired/secondsRemaining) or events monitor history --last N [--rule NAME] [--since 30m] | Inspect the running daemon without restarting. Filters apply before --last, so --rule watch --last 20 is that rule's last 20 fires, not whatever survives the global tail. |
| watch this camera for the next 30 minutes / temporary rule / bounded monitoring task | add a rule with expires_at = "<RFC 3339 or Unix second>", events monitor reload, then read back with events monitor history --rule NAME | Absolute time only — a relative "30m" would restart its countdown on every reload. Expiry stops the rule firing; it does not rewrite the rules file, and captured history is kept. |
| apply edited rules / reload rules | events monitor reload (Unix SIGHUP) | Re-reads TOML without dropping cursor. Invalid TOML keeps old rules + logs warn. On Windows, stop + run again — SIGHUP is Unix-only. |
what can it do / what features / what's supported / what can this CLI do / what's new | reolink-cli features (add --output text for human view) | Lists installed commands + highlights since v0.2.8 + XDG paths. Must run this first to gate which subcommands exist on the user's binary |
| how much disk used / cache size / cache usage | reolink-cli cache status | Inspect ~/.cache/reolink-cli/ by category (snapshots/audio/captures/downloads/logs) |
| current status / how are things / system health / overview / dashboard | reolink-cli status | Fleet + gateway + events-monitor + cache. --camera X to include recent events. |
| is the gateway running / gateway status | reolink-cli gateway status | 500ms TCP probe; on [DOWN] prints exact gateway start to run. |
| check / diagnose / doctor / what's wrong / sanity check | reolink-cli doctor | 9 offline checks (binary, dirs, config, registry, perms, gateway TCP). Use FIRST when user says "broken". |
| tab completion / shell completion | reolink-cli completions {bash|zsh|fish|powershell|elvish} > <path> | Re-run after self-update. |
| first-time setup / configure camera / set up reolink / how to use after install / how to get started | reolink-cli config init then device add | config init writes default gateway addr; device add <alias> --host <ip> --user admin registers the first camera (password via prompt or --password-stdin). Then --alias <name> login to verify. |
| uninstall / delete / remove reolink | reolink-cli setup --uninstall --purge --no-interactive | Confirm with the user before running this — --purge deletes their camera registry and cannot be undone. Without a terminal on stdin — which is every command you launch — the uninstall refuses unless --no-interactive is present; that flag is the declaration that the deletion was intended, so do not add it reflexively to make an error go away. Removes binaries + config/cache/state + cross-agent skill dirs + Claude Code plugin registry. Drop --purge to keep config/cache/state (alias list preserved) — there is no --keep-config flag. The agent skill dirs are global (~/.claude/skills/…, ~/.agents/skills/…): they are removed regardless of REOLINK_PREFIX, so uninstalling one copy of a side-by-side install unlinks the skill for both. If the customer still has the extracted tarball, ./uninstall.sh in there is the symmetric alternative (it forwards straight to this same command; the uninstall flow only honours --purge, --no-interactive and --prefix). (Note: npx skills remove reolink-cli is rarely needed — reolink-cli setup --uninstall already wipes the agent skill dirs.) |
| skill stale / plugin cache stale / refresh skill | reolink-cli plugin refresh | Auto-detects agent (Claude Code / Codex / Cursor / Copilot / Gemini). Run when features reports cache_state != in_sync. |
| clean up / clear cache / delete old snapshots / clean cache | cache clean [--older-than 7d] [--category X] [--apply] | Dry-run by default — always preview first, run --apply only after the user sees the list |
Workflow
Default: just run the command. The gateway daemon caches auth across CLI invocations, so the agent doesn't pay ping+login per command. Most user intents map to one CLI call (info, get, apply, snapshot, stream url, …).
Forbidden pre-fetching <subcmd> --help to "check what flags exist." The Intent Interpretation and Command Reference sections below, plus references/<topic>.md, already list every subcommand and its key flags; reolink-cli features enumerates the installed surface at runtime. Must dispatch the operative command directly. Only read --help if the command actually errored with an unknown-flag clap message. Each unnecessary --help round adds one full agent turn (~2–3 s of perceived user latency) for zero information gain — the user feels it, the CLI doesn't.
Must batch a single user intent into a single shell invocation when it needs multiple CLI commands. For sequences like "PTZ full sweep" (right→down→left→up→stop), "snapshot before/after a move", "info + capabilities + storage status", or "health check + benchmark" / "doctor + benchmark" pairs, chain them with && (or ; if you want continue-on-error) inside one Bash call — do not spawn N separate tool calls. Each extra tool call adds one full agent turn (~2–3 s). N=5 commands as one Bash = ~3 s perceived; N=5 commands as five tool calls = ~12 s perceived. The rule applies even when the commands are semantically distinct (e.g. doctor is local, benchmark hits the device) — the user asked for "both", so dispatch both in one shell line: reolink-cli doctor && reolink-cli --camera X benchmark --iterations 3. Independent prep checks for unrelated devices can still be parallel tool calls; the rule is about one intent → one invocation.
MCP server is the fastest path for repeated calls. reolink-cli mcp-server speaks JSON-RPC 2.0 over stdio (MCP protocol 2025-11-25, ~39 tools): identity (camera_{ping,login,info,capabilities,discover}), PTZ (camera_ptz_{move,stop,presets,preset_goto,preset_set,preset_delete}), light (camera_light_{ir,statusled}_{get,set}, camera_light_spotlight_set), audio (camera_audio_{volume_get,volume_set,mute}), detection (camera_detect_{motion,ai}_{get,set}), recording/storage (camera_record_config_get, camera_storage_status, camera_vod_search), events (camera_events_query), image/OSD (camera_image_{flip,tune}_get, camera_image_flip_set, camera_osd_{get,set}), users (camera_users_list), system (camera_system_reboot), plus camera_config_{get,set}, camera_preview_capture, camera_snapshot (JPEG to a file), and camera_raw. Benchmarked locally: 5 sequential camera_info calls take 52 ms via MCP vs 108 ms via 5 Bash spawns — ~52% faster, saving ~11 ms per call by avoiding process startup. When to suggest MCP wire-up: the user is going to do >3 ops in one conversation, or runs the agent in a tight loop. How to wire it in Claude Code (claude.json or settings):
"mcpServers": {
"reolink-cli": {
"command": "reolink-cli",
"args": ["mcp-server"],
"env": { "REOLINK_GATEWAY_ADDR": "127.0.0.1:9000" }
}
}
Pass alias, host, or uid per tool call to target a specific camera; the server validates input schemas and returns {structuredContent, isError} payloads. Caveat: the gateway must be running separately (MCP routes through it the same way the CLI does).
apply recipes are one-shot. For "set X to Y" intents, the *** apply subcommand does get → compare → (skip if idempotent) → set → verify internally and returns {before, after, changed, verified}. Forbidden running get → set → get manually, and forbidden running a separate get after an apply — the verify is already inside.
| Recipe | Replaces |
|---|---|
light ir apply --state auto|on|off | manual ir get/set/get |
image flip apply [--flip|--no-flip] [--mirror|--no-mirror] | manual flip get/set/get |
osd apply [--name] [--datetime] [--name-overlay] | manual osd get/set/get |
detect motion apply [--enable] [--sensitivity N] | manual motion get/set/get |
detect ai apply --type T [--sensitivity N] | manual ai get/set/get |
ping and login are NOT pre-steps. They are diagnostics. The first command you actually need (info, get, apply, snapshot, …) does its own connect + auth via the gateway daemon. If that command's JSON has error.code == "auth_required", re-run login once and retry. If it has error.code == "connection_refused" / "timeout" / "no_route", that IS the same signal ping would give — no need to run ping again. Forbidden running ping followed by login followed by the real command on every turn; that's 3 round-trips for the work of 1. Only call ping standalone when the user explicitly asks "is camera X reachable?" or "why can't I connect?".
Verify only when needed. Re-get after a write only on capability-gated config SETs that can silently keep the old state (light ir/whiteled/spotlight set, image flip/tune set, audio config, detect motion/ai set, osd set, encode set, generic config set). Forbidden verifying after apply (already verified), ptz move/stop/preset, system reboot, system upgrade (confirm via info after the device reboots, not a re-get), snapshot, preview *, stream url, audio mute/unmute/talk, audio siren play/stop (a one-shot action, nothing to re-read), vod download, users passwd, privacy mask set. Full table in references/troubleshooting.md#verify-rules.
Other safety rules (kept verbatim — these break things if violated):
- Target selectors are global options (pre-subcommand). Forbidden positional. Priority:
--camera>--host>--uid> env. Batch:--tag,--cameras A,B,--all-devices. Must pass--channel Nfor NVR. - Credential safety: Forbidden
--password PLAINon argv. Must use--camera <name>(fromaliases.toml0600),REOLINK_PASSWORDenv, or--password-stdin. If no camera registered and op isn't trivially read-only, ask user to rundevice addfirst. - Stored passwords are encrypted (
RLENC1:…, AES-256-GCM) with the key incredentials.keybeside the file that holds the password — besidealiases.tomlin every layout, including a registry redirected with--cameras-file/REOLINK_CAMERAS_FILE, and beside a redirectedconfig.tomlif a password lives there. Separate registry files therefore have separate keys. A plaintext config is converted automatically on first use. Do not try to read a password out ofaliases.toml— it is ciphertext, and there is no command that reveals it. Backing up or moving a config means copying the file and thecredentials.keynext to it as a pair, from the same directory; with only one of them the passwords are unrecoverable and must be re-entered viadevice update <camera> --password-stdin. If a command reports a password that "cannot be decrypted", the key file is missing or mismatched — that is not a wrong-password problem, so do not retry with guesses. (Installs from before 0.12.4 kept a single key in the default config directory; it is still read as a fallback until the next write moves it beside its file.) - Pre-write read: Must
getcurrent value before any write that's NOT covered by anapplyrecipe; confirm side effects from the table below; verify NVR channel. - Protocol: leave it unset — it defaults to
v20, which the whole supported fleet speaks. A v30 camera declaresprotocol = "v30"in its entry (or--protocol v30for one call).--protocol autoasks the gateway to detect it from the device's response magic and costs an extra TCP round trip; use it only to re-detect a camera whose entry names the wrong protocol. - Battery devices on LAN (transparent wake): when
--uidis given without--host, the gateway automatically runs the BC3.0 §F.2 wake handshake on UDP 2026 before login (in customer / no-P2P builds). Forbidden callingreolink-cli wakeas a precondition — it is a hidden diagnostic command. Must just callinfo/preview/ptz/ etc. with--uidand accept that the first call may take 2–8 s while the device boots; subsequent calls within the gateway session TTL reuse the live session and have no wake overhead. - Events monitor exclusivity: when intent pairs trigger + action ("announce when a person is detected", "car detected → log"), Must use
events monitor init/check/runwith a TOML rule. Forbidden writingwhile true; do events query…; doneshell loops — the rule engine already has debounce, parallel dispatch, SIGTERM draining, cursor bootstrapping, action-timeout, retry. Seereferences/event-monitor.md.
Global Options
--host/--uid/--camera/--cameras A,B/--tag/--all-devices, --user/--password/--channel/--view, --protocol auto, --output json|text, --gateway-addr HOST:PORT, --config-file/--cameras-file. Env equivalents: REOLINK_HOST/UID/ALIAS/USER/PASSWORD/CHANNEL/VIEW/PROTOCOL/GATEWAY_ADDR/CONFIG_FILE/ALIASES_FILE. Run reolink-cli --help or <subcmd> --help for exact flag shapes.
Batch Operations
- Must keep the same selector throughout a workflow — the batch framework handles fan-out.
- Forbidden enumerating with
--tagthen switching to--camerainside the same workflow. - Batch output has per-target
ok; a single failure doesn't fail the batch. Must checksummary.failed > 0and iterateresults[].
Degenerate case: when a batch selector (--tag X, --cameras A, --all-devices) resolves to exactly one device, the CLI emits the single-target envelope ({ok, command, protocol, data}), not the batch report. Must test "summary" in response to detect which shape you got. Forbidden assuming results[] exists.
Error Recovery
| Error | Cause | Next step |
|---|---|---|
reolink-cli: command not found | Binary not installed (installing the skill does not install the binary) | Run the install snippet in references/setup.md → Install the reolink-cli binary, then retry |
gateway connect failed: Connection refused | Gateway not running | reolink-cli gateway start --addr 127.0.0.1:9000 & then retry |
ping returns reachable:false, reason:"timeout" | IP not routable (cross-subnet / VPN not up / host off / firewall) | Tell user: verify VPN / check subnet; do not try login — it would hit the same timeout |
ping returns reachable:false, reason:"refused" | Host reachable but port 9000 closed | Wrong port / wrong IP / gateway service not running on device |
ping returns reachable:false, reason:"no_route" | No routing entry at all | Check netstat -rn / VPN |
ping returns skipped:true, reason:"uid only..." | UID-only target | Expected — proceed directly to login |
device unreachable (retryable) | Net drop / device off / wrong port | Re-ping; check sibling |
auth required / invalid credentials | Wrong creds | device update; admin-reset via Reolink app if locked out |
invalid params | Caller bug | Fix request, don't retry |
unsupported | Firmware lacks feature | device inventory --capabilities to confirm; skip |
Connection reset after system reboot | Expected | CLI swallows this one; re-ping after 30–60s |
| Remote (UID) login timeout | First-connect warm-up | Retry with --timeout-secs 15+ |
wake timed out after 3 attempts: no response from device with uid X | Battery device not reachable on the local broadcast domain | Verify device is on the same /24 as the gateway host, UDP 2026 not firewalled, device not depleted; do not retry tightly — each attempt already takes ~3 s and the device may genuinely be off-network |
device acknowledged wake (booting) but did not report ready within 30s | MCU woke but Linux main failed to come up | Physical-side issue (battery low, firmware stuck); user must check the device |
no active session (gateway) | Token expired | Re-login via set auth.login |
| HTTP 401 (gateway) | Missing Authorization: Bearer | Add header |
| HTTP 403 (gateway) | Token invalid/expired | Re-login |
HTTP 410 on /api/login or /api/request | Old v0.1.1 endpoint | Use POST /api + Bearer |
users remove admin fails | Device refuses removing last admin | Don't |
users passwd permission error | Non-admin changing someone else's | Log in as admin |
Command Reference
Signatures only — run <cmd> --help for flag details; see references/<topic>.md for per-feature examples.
Discovery / Registry: discover, device list|resolve|show|add|update|remove|import|inventory|analyze|expand, config init
NVR multi-channel: device expand <nvr-name> [--yes | --names A,B,C] [--drop-parent]. RLN-series only; one entry per channel auto-tagged with parent name.
Identity: ping, login, info, capabilities
Config paths (config get/set PATH with merge semantics):
led, device-name, language, time-zone, time-format, network, osd, osd-format, system-general (get-only).
(The paths image / audio / alarm / alarm-policy / ai were removed — they were stubs. Use dedicated commands below.)
Preview / Snapshot: preview capture|play|start|stop, snapshot [-o FILE] [--stream]. --stream sub (the default) is the fast path — a small ~900x512 JPEG, ~2s on a woken camera; --stream main is a full-resolution frame at ~5s on a 4K model. A battery camera behind a hub sleeps (when is not predictable from idle time — see the 400 row above), and the first capture after it does waits for its whole video pipeline to boot: measured 5s (sub) to 23s (4K, three channels waking at once) before the device sends a single byte. That wait is device-side and unavoidable — it is not a hang, and the timeout budget covers it. A capture during a burst is sub-second; treat the first one after any gap as a warm-up rather than trying to predict which one pays the wake.
Benchmark: benchmark [--iterations N] [--phases connect,login,info,snapshot] [--reuse-session] [--warmup M] — per-phase p50/p95/p99/mean/stddev. Read-only.
Doctor: doctor — 9 sanity checks (binary, dirs, config.toml, registry, perms 0600, legacy cleanup, gateway TCP). Non-zero exit on fail. Use first for "broken" reports.
Completions: completions {bash|zsh|fish|powershell|elvish} — emits to stdout; redirect into shell's completion dir. Re-run after self-update.
Gateway status: gateway status — 500ms TCP probe of the resolved gateway-addr. Text shows [LISTENING] / [DOWN] / [UNCONFIGURED] + the gateway start command on down.
Gateway log files (since 0.12.5): the gateway always writes a compact single-line log to <cache-dir>/logs/gateway-<pid>.log (~/.cache/reolink-cli/logs/ on Linux, ~/Library/Caches/reolink-cli/logs/ on macOS, %LOCALAPPDATA%\reolink-cli\logs\ on Windows) at info level; files older than 14 days are pruned at startup, and cache clean --category logs manages them like any other cache category. To collect evidence after a failed run: zip the logs/ directory — no pre-configuration needed. For deep traces re-run with RUST_LOG=debug (adds per-round LAN probe results, every TCP connect attempt with address+outcome, uid-cache hits with age). REOLINK_LOG_FILE=off disables the file; REOLINK_LOG_FILE=<path> redirects it (user-managed, no pruning). Credentials never appear in logs; entries do contain hosts/UIDs/SSIDs, so redact before posting publicly.
Stream URLs: stream url [--kind rtsp,rtmp,flv] [--stream main,sub,ext] [--with-auth] — defaults to rtsp main. --with-auth embeds creds (use only when user wants pasteable URL); without it ships sibling user/password JSON fields. NVR: device expand then --tag <nvr> stream url. RTSP path is /Preview_{NN}_{main|sub|ext}. Detail in references/media.md.
Light: light {ir|statusled} {get|set --state auto|on|off}, light ir apply --state auto|on|off (recipe; prefer for "set X"), light spotlight set --enable|--disable [--duration], light whiteled {get|set [--enable|--disable] [--brightness 0-100] [--alarm-mode] [--detect-type T,...]}. Caveat: E-series can ok:true on light ir set while silently keeping old state — apply recipe surfaces this in verified:false; check device inventory --capabilities if write ignored.
Image: image flip {get|set|apply [--flip|--no-flip] [--mirror|--no-mirror]} (apply is the idempotent recipe), image tune {get|set [--bright N] [--contrast N] [--saturation N] [--hue N] [--sharpen N]} — all 0–255, 128 = neutral. If a user says "60%" that's ~160.
OSD: osd {get|set|apply [--name NAME] [--datetime|--no-datetime] [--name-overlay|--no-name-overlay]} (apply is the idempotent recipe)
System: system reboot; system upgrade <file.pak> [--factory-reset] [-y] — flash firmware (gateway reads the LOCAL file; ⚠️ bricking risk, match model+hw_ver; auto windowed/stop-and-wait by capability; detail in references/admin.md)
Users (device accounts): users list, users add NAME --level admin|user, users remove NAME, users passwd NAME. Password via TTY prompt or --password-stdin — never --password PLAIN. Name + password each 1–31 chars. Non-admin users can only change their own password.
Audio: audio config {get|set}, audio volume {get|set LEVEL} (0–100), audio mute, audio unmute, audio replies, audio talk {--file PATH | --stdin} [--sample-rate N]. audio volume is master; audio config.volume is per-profile — ask which. audio talk pushes PCM16 LE mono to camera speaker via talkback (cmd 201 open + 202 stream). Gate on capabilities.audioTalk=1.
Siren: audio siren play [--duration 1-600 | --times 1-100] [--wait] (default 2 repetitions ≈ 6s; device self-stops after the last one), audio siren stop (silence now). The device sounds in whole repetitions of ~3.2s, so --duration rounds up to --times and the answer reports both, plus speakerVolume — if the user says they heard nothing, read that first (a measured case sat at 7/100 and was inaudible; the command succeeded regardless). Up to 0.14.3 the siren never actually sounded — the CLI sent playMode 1 (by duration), which this firmware accepts with a 200 and ignores; playMode 0 (by times) is the one that makes noise, confirmed against the camera's own microphone. --wait blocks for the duration, then sends an explicit stop and adds waited/stoppedAt/stopAcknowledged to the answer; the firmware timer is still sent, so a killed CLI does not leave the siren running. Without --wait the answer is accepted: true, verified: false and nothing more — v20 has no GET for siren state, so never report that the siren sounded on the strength of it. This is the app's manual-siren button = v20 cmd 263 NET_MANUAL_RING_DOWN (<audioPlayInfo> continuous/switch mode), verified on real RLC-823/810. Gate on capabilities / JSON GetAbility.supportAudioAlarm. Also audio siren task {get|set --on|--off} (which alarms trigger it) + audio siren enable {get|set [--on|--off] [--channel-on|--channel-off]} — two switches: device-wide mainSwitch and a per-channel channelMask bit. A hub can report master-on with every channel bit zero; --on alone cannot fix that, which is why --channel-on exists — v20 cmds 306/307, 316/317, but this whole v3.x generation returns 405 for them; only manual play/stop is universal. A sounding device pushes cmd 547 → events stream shows siren.on/siren.off (no GET for siren state). NB: do NOT confuse with cmd 349 (play a stored audio file) — it 400s on cameras with an empty audio store, i.e. almost all of them.
Scene mode (hub arming profiles): scene show (active scene + which task bits this hub honours + the ids), scene list (every scene with its per-channel tasks), scene status, scene set <id> / scene set --schedule, scene edit <id> [--tasks record,ftp,email,push,audio,linkage,speaker,track] [--channel N] [--iot-tasks linkage,device] [--name ...] [--delay N], scene schedule get|set --scene <id> [--days 0,1] [--from H] [--to H], scene alarm get|set --task REC --types people,vehicle [--channel N], scene options [--key-enable|--key-disable] [--private-mode|--no-private-mode].
A scene is a named set of per-channel task bits, so switching scenes re-arms the whole hub at once — that is the app's Home / Away / Disarm control. audio in the task list is the siren, not the microphone. Scene id 0 is not a scene: it hands control to the weekly schedule, and scene show reports that as followSchedule: true. --tasks replaces the scene's set rather than adding to it, and without --channel it applies to every channel in the scene. The schedule is 7 days × 2 half-hour slots × 24 hours; --days takes the device's own 0-6 numbering.
Encoder: encode get (all three streams), encode capability (what the device accepts — read this before setting), encode set --stream main|sub|third [--resolution WxH] [--fps N] [--bitrate KBPS] [--codec h264|h265] [--rate-control vbr|cbr] [--profile default|baseline|main|high] [--gop N] [--smart-h265|--no-smart-h265] [--smart-h265-level 0-2]. The wire SET (cmd 57) rewrites all three streams at once, so the CLI always re-reads first and patches only the flags you pass. Changing resolution restarts the stream — any live preview/RTSP consumer drops. GOP is pinned by firmware when H.265+ is on (level 0/1 → 2, level 2 → 4), so --gop is ignored in that case.
Privacy: privacy mask {get|set [--enable|--disable] [--json JSON]}. Get first to see current regions (device-pixel {x,y,width,height}).
PTZ: ptz move DIR [--speed 1-64] [--duration-ms], ptz stop, ptz preset {list|goto ID|set ID NAME|delete ID}, ptz {zoom|focus} {get|set --pos N}, ptz focus auto --enable|--disable, ptz patrol {list|start|stop}, ptz guard {get|set|snapshot|goto}, ptz autotrack {get|set [--enable|--disable] [--mode N] [--detect-type T,...] [--priority T,...]}. Directions: left|right|up|down|left-up|left-down|right-up|right-down. Speed 1–64 (not %). If user says "speed 50" ask if it means % or literal.
Recording: record schedule {get|set [--enable|--disable] [--fps 1-15] [--pre-time] [--plan-type daily|weekly] [--week-table BITMAP] [--start-hour H --start-min M --end-hour H --end-min M]}, record config get (read-only: cycle / pre-record / post-record / packageTime). --week-table is 7-bit bitmap (Mon=bit0 … Sun=bit6); weekdays=31, Mon-Sat=63, all=127. Manual start/stop not supported — cmd 277/278/587/588 return 405.
Storage: storage status (read-only SD/HDD capacity + mount state). Forbidden format/init ops — direct users to the Reolink app if they need to format.
VOD: vod search [--from ISO --to ISO | --since DURATION] [--type T,...] [--stream main|sub] [--limit N], vod download NAME [-o FILE], vod download --from ISO --to ISO (time range, the camera cuts it — one request per recording segment, joined; output is an Annex-B .hevc/.h264 elementary stream; add --audio clip.aac for the sound — it is a second request and a second file, ADTS AAC, so ffmpeg -i clip.hevc -i clip.aac -c copy clip.mp4 remuxes losslessly; unsupported models answer 400). Time must be naive local ISO (YYYY-MM-DDTHH:MM:SS, no TZ, no ms). Cross-month windows are handled for you (the gateway splits at month boundaries and merges); limit applies to the merged list, and truncated tells you it cut short. Filenames case-sensitive.
Types: manual|sched|io|md|people|vehicle|face|dog_cat|visitor|other|package
Detection: detect motion {get|set|apply [--enable|--disable] [--sensitivity 0-100] [--use-pir|--disable-pir]}, detect ai {get|set|apply} --type TYPE [--sensitivity 0-100] [--stay-time SECS]. apply is the idempotent recipe — prefer it for "set sensitivity to N"-style intent. AI types: person|vehicle|dog_cat|package|cry (subset varies by model).
Notifications: notify push {get|set [--interval SECS] [--rich N] [--consent N]}
Events (gateway required): gateway start [--addr HOST:PORT] once, then events {query [--last N] [--after] [--since] [--types T,...] | history [--from ISO --to ISO | --since] [--types] [--limit N] | stream [--timeout] [--types] | monitor {init|check|run|reload|status|history}} with --gateway-addr HOST:PORT. monitor is the declarative rule engine — see references/event-monitor.md. Event --types vocabulary is different from VOD --type vocabulary — events use motion|people|vehicle|face|dog_cat|visitor|package|cry (no md, sched, manual, io, other). md is VOD-only. --since on events query accepts <N>m|h|d only (NOT seconds) — use 1m minimum. events history is a separate store from events query: query/stream read the gateway's in-memory ring of live pushes (recent ~500, lost on restart), history asks the device (hub/NVR) for its recorded event log by time window (cmd 516/517, per-sub-device UID resolved from --channel); standalone IPCs 400.
WiFi: wifi get — diagnostic: read current SSID / authMode / encryptType / channel / countryCode. The key field is always redacted to "" by the device firmware. Use to confirm a previous wifi set actually applied (sometimes Phase 4 rediscover times out cross-subnet but the SET succeeded — wifi get confirms). wifi set --ssid <SSID> {--psk <PSK> | --psk-stdin} [--no-test] [--no-update-registry] [--rediscover-timeout-secs <N> (default 90)] [--hidden] — push new SSID+PSK; 4-phase orchestrator: (1) capability probe, (2) WifiTest pre-check, (3) commit, (4) BCDI rediscover + registry auto-update. Exit codes follow the CLI-wide error classes: 0 success / 1 input / 2 network (device unreachable, P2P/TCP timeout — retryable) / 3 auth (device rejected credentials or commit) / 4 device error / 5 protocol (includes "wifi did NOT switch" read-back verify failure). Branch on the JSON error.code string when possible; the numeric exit is the class, not a wifi-set-specific code. Safety: device on wireless with wifiTestAtWireless=0 cap will refuse without --no-test; the device itself also enforces this server-side. Recommended path: wired device → wifi set --ssid X --psk-stdin then echo PSK in. Credential safety: Must use --psk-stdin (pipe PSK via stdin) rather than --psk PLAIN on argv — same rule as passwords.
Wake (diagnostic, hidden): wake --uid <UID> [--host <HOST>] — send BC3.0 §F.2 UDP wake packet without logging in. Broadcast if --host is omitted. Marked hide = true in top-level help — appears only when explicitly queried (wake --help). Normal workflow does not need this; info/login/ptz/etc. auto-wake battery devices via the gateway. Forbidden calling wake as a pre-step before any of those commands.
Raw: raw CMD [--body-xml|--ext-xml|--body-file|--ext-file].
Self-description: reolink-cli features [--output json|text] — prints the installed version, the list of subcommands this binary supports, feature highlights with their since version, and the XDG file layout. Must run this (or rely on AGENTS.md Part 5) before dispatching subcommands you haven't confirmed exist on the user's binary — skill docs track master, the binary may lag.
Cache management: reolink-cli cache {status|clean}. status reports per-category size + oldest/newest file timestamps under ~/.cache/reolink-cli/. clean [--older-than 7d] [--category audio|snapshots|captures|downloads|logs|all] [--apply] lists candidates by default; add --apply to delete. --older-than 0 matches every file. Empty date-bucket subdirs under snapshots/ are auto-pruned.
Gateway HTTP API
Single entry POST /api with Authorization: Bearer <token> and body {action, path, params}. Login is itself a call: {action:"set", path:"auth.login", params:{username, password, host, protocol?, channel?}} → {status:"success", data:{token, info}}. Streaming endpoints (/api/events, /api/preview/video, /api/snapshot, /api/vod/download) take ?token= in the query for browser-native elements; the token supplies the device credentials server-side, so never put user/password in a URL (requests without a valid token are rejected 401/403). See references/gateway-http.md for curl recipes and the full spec.
Side-effectful commands
Must confirm with the user before running any of these. Forbidden executing them opportunistically or as part of a read-only inspection flow:
| Command | Side effect |
|---|---|
| `ptz move | goto |
light spotlight --enable, light whiteled --enable | Bright visible light |
audio volume set (high), audio replies triggers | Loud output |
audio siren play | Very loud alarm audible outside the property |
scene set / scene edit | Re-arms every channel on the hub at once. scene edit --tasks replaces the task set, so a scene edited down to nothing records and notifies nothing (references/scene-mode.md) |
encode set (resolution change) | Restarts the encoder — live preview / RTSP / NVR consumers drop |
privacy mask --disable | Unmasks previously hidden regions |
system reboot | 30–60s downtime; aborts in-flight |
| `users remove | passwd` (others) |
record schedule --disable | No new recordings |
| `detect motion | ai --disable` |
cache clean --apply | Permanent file deletion under ~/.cache/reolink-cli/ — always preview with a dry-run first |
Gotchas
- NVR: Must pass
--channel N; wrong channel silently returns wrong config. Must verify viadevice resolvewhen uncertain. - Capability variance:
detect ai --typesupport differs per model. Battery cams may bepersononly. Must rundevice inventory --capabilitiesbefore scripting AI calls across devices. - Empty
{}response ≠ error on paths the device doesn't expose — do not treat as failure. - Argv wins over env when both are set for
--password. - Remote (UID) first connect is 10–15 s; Must bump
--timeout-secs 15+when first-connecting a UID target. preview capturewrites BC framing, not raw H.264 — ffplay needs-f hevc|-f h264.preview playalready handles that.- Forbidden running
events query/events streamwithout--gateway-addr— it will fail fast with a clear error, but don't attempt it. - Gateway tokens don't auto-refresh. Must re-login after long pauses or on HTTP 403.
- Batch cannot prompt mid-run. Must register cameras (with stored creds) before any batch operation.
- Siren =
audio siren play(cmd 263), never anything about "audio files". The manual siren is a one-shot ring-down; a device 400 here almost always means the model genuinely lacks a siren, not a bad call.audio siren task/enable(the alarm-linkage config) is 405 on most current v3.x models — if config-get 405s, manualplay/stopstill works; don't conclude "no siren". encode setis a full-stream rewrite (cmd 57). The CLI already does GET→patch→SET so untouched streams are safe, but a value the device doesn't list is rejected with a bare 400 — Must runencode capabilityfirst and pick a listed resolution/fps/bitrate. Changing resolution restarts the encoder, dropping any live preview/RTSP/NVR consumer.
File locations (XDG layout)
Single parent per category — easy to purge, easy to find. Must prefer these paths in rules, examples, and command suggestions. Forbidden scattering artifacts across /tmp/*.pcm, /var/reolink/..., /etc/reolink/....
| Category | Path | Managed by |
|---|---|---|
| Persistent config (CLI, aliases, monitor rules) | ~/.config/reolink-cli/ | CLI (config init, device add, events monitor init) — 0600 perms |
| App state (pidfile, cursor, debounce, trigger history) | ~/.local/state/reolink-cli/ | events monitor daemon — monitor-state.json + monitor-history.jsonl + monitor.pid |
| Disposable artifacts (snapshots, PCM clips, captures, VOD) | ~/.cache/reolink-cli/{audio,snapshots/<date>,captures,downloads}/ | user / rule authors — Must use this prefix |
| Claude Code plugin cache | ~/.claude/plugins/cache/reolink-cli/ | Claude Code (/plugin update refreshes) |
events monitor expands ~/ in path and pcm fields and auto-mkdir -p's the snapshot parent dir — safe to write rules with ~/.cache/reolink-cli/snapshots/{date}/... without pre-creating anything. Bulk purge: rm -rf ~/.cache/reolink-cli.
Launcher
Prefer the installed binary: reolink-cli …. Plugin-bundled:
${CLAUDE_PLUGIN_ROOT}/scripts/run-cli.{sh,cmd}.
References
Topic-scoped recipe files. Must load only the one matching your task; Forbidden reading the full set.
| File | Read when |
|---|---|
references/setup.md | Discovery, registry, identity, generic config get/set |
references/media.md | Preview / snapshot / VOD |
references/controls.md | Light / image / OSD / audio / privacy |
references/ptz.md | Any ptz … |
references/detection.md | Motion / AI detection (detect motion, detect ai) |
references/recording.md | Record schedule + core config (record schedule, record config get) |
references/storage.md | SD card / HDD read-only status (storage status) |
references/notifications.md | Mobile push (notify push) |
references/events.md | Gateway event bus — events query / events stream, event shape |
references/event-monitor.md | events monitor rule engine — TOML-driven automation (preferred over shell watchers) |
references/voice-alert.md | Detection-triggered voice announcement via audio talk (shell-watcher legacy) |
references/admin.md | Device user accounts, system reboot, raw debug |
references/gateway-http.md | Gateway POST /api from curl or non-CLI clients |
references/troubleshooting.md | Something failed; or full intent map |
Other docs (in the repo, not this skill):
docs/cli-reference.md— generated flag reference