---
name: synology
description: "Control a Synology NAS via the DSM Web API + SSH backend (system, storage, shares, users, packages, FileStation, ACLs); writes gated --confirm; default device Cascades cascadesDS. Triggers: synology, diskstation, DSM, NAS, hyper/active backup, synology acl."
---

# synology — Synology DSM control (Web API + SSH)

Full control of a Synology NAS. Two complementary backends:

1. **DSM Web API** (`scripts/syno_client.py`) — the structured surface. Auth → discover the
   device's *own* API map → call any API method. This is how you "control all functions":
   `apis` shows everything the device exposes, `call <API> <method>` reaches any of it.
2. **SSH `syno*` CLI** (`scripts/syno-ssh.sh`) — the gaps the Web API doesn't cover: filesystem
   ACLs (`synoacltool`), share/user/group CLI internals, package CLI. We already use this for
   the Cascades share-permission inventory (`docs/migration/synology-permission-inventory.md`).

**Default target = Cascades `cascadesDS`** (`192.168.0.120`, DSM on :5000, admin) — credential
vaulted at `clients/cascades-tucson/synology-cascadesds.sops.yaml` (`host`/`port`/
`credentials.username`/`credentials.password`). ext4 filesystem (NOT Btrfs → no Active Backup
for Business). Shares: homes, Public, SalesDept, Server, Management. **On a private LAN — every
command needs the Cascades site VPN up** (a connect failure almost always = VPN down). Point at
any other NAS with `--vault clients/<x>/synology-...sops.yaml` (same field layout).

## Web API commands
```bash
PY="$CLAUDETOOLS_ROOT/.claude/scripts/py.sh"; S=".claude/skills/synology/scripts/syno_client.py"
# verify + discover
bash "$PY" "$S" test                  # login, print model/serial/firmware/RAM/uptime
bash "$PY" "$S" apis [filter]         # the device's full API map (e.g. `apis backup`, `apis drive`)
# reads
bash "$PY" "$S" sysinfo               # SYNO.Core.System — model/serial/RAM/temp/uptime
bash "$PY" "$S" util                  # live CPU/mem/disk/net
bash "$PY" "$S" storage               # volumes/disks/RAID/usage (SYNO.Storage.CGI.Storage)
bash "$PY" "$S" shares | users | groups | packages | services | connections
bash "$PY" "$S" ls [folder_path]      # FileStation: no path = shares; else list a folder
# generic power tool — ANY API method the device exposes
bash "$PY" "$S" call <API> <method> [k=v ...] [k:=json] [--version N] [--post]
bash "$PY" "$S" call SYNO.Core.System.Status get
bash "$PY" "$S" call SYNO.Backup.Task list           # Hyper Backup tasks (if installed)
# writes — ALL gated --confirm
bash "$PY" "$S" pkg-start <id> --confirm | pkg-stop <id> --confirm
bash "$PY" "$S" reboot --confirm | shutdown --confirm
bash "$PY" "$S" call <API> <set-method> k=v --confirm   # any mutating method needs --confirm
```
`call` params: `k=v` for strings, `k:=json` for typed/array values (e.g. `additional:='["size","owner"]'`).
A method whose name starts with a mutating verb (set/create/delete/start/stop/reboot/…) is auto-detected
and **refuses to run without `--confirm`**.

## SSH commands (the syno* CLI surface)
```bash
X=".claude/skills/synology/scripts/syno-ssh.sh"
bash "$X" info | df | shares | users | groups | packages | services   # reads
bash "$X" acl <share>                     # synoacltool -get /volume1/<share> (e.g. acl Server)
bash "$X" reboot --confirm                # synoshutdown -r (use when the Web-API reboot 103s)
bash "$X" shutdown --confirm              # synoshutdown -s
bash "$X" run "<command>" --confirm       # arbitrary command (gated); privileged auto-sudo -S
```
Requires SSH enabled on the NAS (DSM → Terminal & SNMP). Privileged recipes feed the vaulted
admin password to `sudo -S`. Auth: sshpass if present, else OpenSSH SSH_ASKPASS (on Windows the
fallback needs MSYS/Git-bash `ssh` on PATH — system OpenSSH can't exec a shell askpass).

## Auth model (DSM Web API)
`SYNO.API.Auth` login (`session=DSM`, `enable_syno_token=yes`) → `_sid` (session cookie) + a
`synotoken` (CSRF). Both are attached to every subsequent call (the synotoken as `X-SYNO-TOKEN`
header + `SynoToken` param) so writes pass CSRF. 2FA: pass `--otp <code>` or `SYNO_OTP`. The client
always logs out at the end. Env overrides for ad-hoc use: `SYNO_HOST/SYNO_PORT/SYNO_USER/SYNO_PASS`,
`SYNO_HTTPS=1` (→ :5001, cert verify off).

## Full API reference
**`references/dsm-api.md`** is the consolidated, sourced map (auth, system/power, storage/shares,
users/groups/security, packages, Log Center, Drive, backup, network/services + the SSH `syno*`
surface) with per-row confidence and the 5 "golden rules". **Read it before any write** — the key
ones: discover versions via `apis` (don't hardcode), CSRF is mandatory for writes (handled),
**setters are read-modify-write** (`get` → mutate → push back; never construct from scratch), and
when a setter's param keys are uncertain run it on-box via `synowebapi --exec` (validates params).

## Why both surfaces (don't reach for SSH first)
The Web API is the default and covers system/storage/shares/users/packages/services/FileStation/
backup/connections. Use SSH only for what the API genuinely lacks — chiefly **filesystem ACLs**
(`synoacltool`): the Web API tells you a share is in Windows-ACL mode but NOT the per-file ACE list.
That ACE data is exactly what the Cascades cutover needs (`docs/migration/
synology-permission-analysis-2026-04-22.md`).

## Open Cascades work this skill serves (verified call recipes)
- **Confirm model/RAM/DSM version** (wiki TODO) — `test` answers it in one call.
- **Log Center syslog collector** (`docs/network/network-logging-plan.md`) — the NAS-as-syslog-SERVER
  is `SYNO.LogCenter.*` (needs the Log Center package): `apis logcenter` to confirm it's installed,
  then `call SYNO.LogCenter.RecvRule list` (receive rules), `call SYNO.LogCenter.Log list` (received
  logs). NOTE `SYNO.Core.SyslogClient.*` is the *forward-own-logs-out* side, not the collector.
  Schedule the snapshotter as a root task via `SYNO.Core.TaskScheduler.Root create`.
- **Synology Drive Team Folder migration** (`PROJECT_STATE` pending) — `apis synologydrive`, then
  `call SYNO.SynologyDrive.TeamFolders list` / `... Tasks list` / `... Connection list` to inspect
  before the CS-SERVER cutover. (`SYNO.C2FS.*` is C2 cloud — not this.)
- **Hyper Backup offsite** (phase4 §6.4) — `call SYNO.Backup.Task list` / `... Repository list` once
  configured. (NOTE: Active Backup **is** installed + running on this ext4 box — the Btrfs requirement
  is only for ABB dedup/self-healing features, not the package. `call SYNO.ActiveBackup.Task list`.)

## Error logging (mandatory)
On a GENUINE functional failure (DSM login fail, API error, SSH connect/auth rc=255) both scripts
log via `.claude/scripts/log-skill-error.sh` before surfacing. Handled conditions (missing cred,
VPN-down connect error surfaced to the user, a method refused for lack of --confirm) are NOT logged.

## Status / verification
- **[VERIFIED 2026-06-25 — HOWARD-HOME, Cascades VPN up]** Full read-surface exercise against the
  device. Device is **DS718+, DSM 7.2.1-69057 Update 11, 6 GB RAM, serial 1920PEN537202**; ext4,
  2x WD10EZEX 1 TB (volume1 913G, 43% used). 10 shares (incl. hidden `pacs`, `Activities`, `chat`,
  `Sandra Fish`); 30 packages running incl. **Active Backup for Business 3.1.0, Synology Drive
  Server 3.5.0, Chat, VPN Server, Hybrid Share**. 41 users, 4 groups (administrators, http,
  MainOffice, users).
- **Command-by-command results (Web API):** `test` `apis` `sysinfo` `util` `storage` `shares`
  `users` `groups` `packages` `services` `connections` `ls` (share roots) `call` — **all OK**.
  Auth (sid + synotoken CSRF), API discovery, version-by-maxVersion routing, and the generic `call`
  power-tool all confirmed live.
- **SSH backend (`syno-ssh.sh`):** `info` `df` `run` confirmed OK (incl. privileged `sudo -S` and
  on-box `synowebapi --exec` at `/usr/syno/bin/synowebapi`). This is the path for real file browsing.
- **`ls <folder>` (FileStation `list`) is DENIED for the admin account on this box** — returns 407
  on-box (SYSTEM_ADMIN) / 400 via Web API even for valid subfolders; the built-in admin lacks
  FileStation file privileges. `ls` (no path / share roots via `list_share`) works fine. For actual
  file/folder browsing use the SSH backend: `syno-ssh.sh run "ls -la /volume1/<share>/<subpath>"
  --confirm`. (`ls <folder>` now catches this and prints the SSH hint.) Windows note: a bare
  `ls /Public` arg is also rewritten by MSYS path-conversion — prefer `MSYS_NO_PATHCONV=1` or the
  `call ... folder_path=/x` form, but the 407 denial blocks it regardless.
- **WRITE/SETTER PATH VERIFIED 2026-06-25.** Live round-trip on cascadesDS: `SYNO.Core.Share create`
  (hidden throwaway share) -> confirmed in `shares` -> `SYNO.Core.Share delete` -> confirmed gone.
  Both mutating calls passed CSRF (synotoken) end-to-end and returned success; device left pristine.
  The mutating-verb gate correctly blocks without `--confirm` and allows with it. (Session-expiry
  re-login 106/119 still not forced live, but the auth/CSRF/setter plumbing is now proven.)
  Share create signature confirmed: `name=<n> 'shareinfo:={"name":..,"vol_path":"/volume1",..}'`.
- Built originally from a 5-agent scan of the DSM 7 help tree + authoritative API sources (kwent
  `_full.json`, N4S4/synology-api, Synology Web API guides) — see `references/dsm-api.md`.
- **Live fixes from this run (2026-06-25):** `services` used `SYNO.Core.Service.list` which 103s on
  DSM 7.2.1 — corrected to `get` (returns `{"service":[...]}`). `apis | head` raised a
  `BrokenPipeError` traceback — now caught and exits cleanly. `ls <folder>` now degrades to an SSH
  hint on the FileStation 400/407 denial. **`--confirm`/`--vault` after the subcommand were rejected
  by argparse** (every documented gated-write example, e.g. `call X set k=v --confirm`, would have
  failed) — moved to a shared parent parser so both flags now work before AND after the subcommand.
- **SSH backend fully verified + fixed (2026-06-25):** the `priv` (sudo) recipes — `shares`,
  `users`, `groups`, `acl` — were ALL broken: `sudo`'s `secure_path` drops `/usr/syno/{bin,sbin}`,
  so `synoshare`/`synouser`/`synogroup`/`synoacltool` returned "command not found" (the non-sudo
  `plain` recipes worked because the admin login PATH has those dirs). Fixed by injecting
  `SYNO_PATH` and running `priv` via `sh -c` (so shell operators survive). Also: `synouser`/
  `synogroup` use `--enum local` (not `--list`); `acl` quotes the share path (handles "Sandra
  Fish"); `services` repointed to the Web API (`synoservice` doesn't exist on DSM 7.2, and
  `synosystemctl` has no list-all). Verified live: `info` `df` `shares` `users`(41) `groups`(4)
  `packages` `acl Server`(real Windows ACEs) `acl Public`(Linux-mode) all OK. `acl` on a
  Windows-ACL share is the SSH backend's unique value (the per-file ACE list the Web API can't give).
- **Code-review hardening (2026-06-25, /code-review high):** `SynoError` now carries the DSM `code`
  + a `handled` flag; `call()` no longer logs eagerly — the top-level handler logs only genuine
  unhandled failures, so the handled FileStation denial (and VPN-down connect errors) no longer
  pollute `errorlog.md`. FileStation-denial detection is numeric (`code in (400,407)`), not a
  substring match, and the SSH hint now also fires on the generic `call` path (not just `ls`).
  `services` falls back `get`->`list` on 103 for older DSM builds. BrokenPipe flush moved inside
  the try so small piped output can't leak a shutdown traceback.
- **DSM 7.2.x reboot/shutdown 103:** the Web-API `reboot`/`shutdown` can return error 103 on some
  builds. Fallback is the SSH `reboot|shutdown --confirm` recipe (`synoshutdown -r|-s`).
- **Param-schema caveat:** method names + versions in the reference are device-authoritative, but many
  *setter param keys* are community/inferred. For a production write where keys are uncertain, capture
  the matching `get` and round-trip it, or run on-box via `synowebapi --exec`.
