Omahub
← All plugins
N

Ring Cameras

by ninepointlabs

Browse your Ring cameras: live view, snapshots, event history, and motion notifications from a bar dropdown.

Security review

Potentially dangerous behavior detected · 2 findings

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
9022a7f
Scanned
6 hours ago
  • high persistence install.sh:25

    Bundles a systemd unit file.

    [Unit]
  • Docs external_hosts README.md:96

    Downloads or connects to an external HTTP(S) host.

    git clone https://github.com/ninepointlabs/omarchy-ring-cameras

Automated analysis only — not a security guarantee.

AI advisory review

No obvious issues detected

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

Low
AI risk level
Low
Recommendation
install
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
9022a7f
Reviewed
5 hours ago

No malicious or obfuscated code was found in the sampled files. The deterministic scan's high rating comes from the systemd user service and a README clone URL, both of which are expected and clearly disclosed: install.sh creates and enables the daemon the plugin needs, and the README link is documentation only. The main residual risks are the plaintext Ring refresh token and third-party npm dependencies, which are normal for this kind of local integration.

  • install.sh creates and enables a systemd --user service; this is persistent by design and clearly documented, but users should expect a background daemon after install.
  • The Ring refresh token is stored in plaintext at ~/.local/share/omarchy-ring-cameras/creds.json with mode 0600; acceptable for a session token but worth knowing.
  • The deterministic scan's external_hosts finding is a README git-clone example, not executed code.
  • npm dependencies (ring-client-api, undici, and transitive packages) are a supply-chain consideration; no postinstall scripts were observed in the sampled package.json.
How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/ninepointlabs/omarchy-ring-cameras --enable
Widgets #bar #media #security

omarchy-ring-cameras

Local Ring camera browser for the Omarchy desktop. Lists your Ring cameras in a bar dropdown and opens a live view (in mpv), a snapshot, or recent event history (motion/ding/on-demand clips, also played in mpv) on demand.

This repo is the plugin — clone it straight into ~/.config/omarchy/plugins/ (or install via omarchy plugin add, see Setup below) and it's both the bar widget (Panel.qml, manifest.json, id tim.ring-cameras) and the daemon it talks to (src/, bin/).

Ring Cameras panel

How it works

  • src/creds.mjs — stores your Ring refreshToken in ~/.local/share/omarchy-ring-cameras/creds.json, mode 0600. Plain file, no passphrase vault: a Ring refresh token is a session credential (like an AWS/gh CLI token), not a signing key, so filesystem permissions are the chosen trust boundary rather than encryption-at-rest with an unlock step.
  • src/daemon.mjs — the long-running process. Wraps ring-client-api, exposes a control protocol over a Unix socket (~/.local/state/omarchy/ring-cameras/control.sock, mode 0600), and persists the refresh token every time Ring rotates it (onRefreshTokenUpdated) so restarts don't need a re-login. Both ends of that socket protocol (control-socket.mjs's server loop and ctl.mjs's client loop) cap how much they'll buffer waiting for a newline at 1 MiB — found by review: without it, a malfunctioning local peer, or Ring itself returning an oversized camera name/prompt/error, could grow the daemon's buffer, ctl.mjs's buffer, or the amount QML's StdioCollector retains without bound. Verified live against both directions: flooding the daemon's socket with 2 MiB and no newline gets it closed with message_too_large well before the full flood lands, and pointing real ctl.mjs at a fake server doing the same gets response_too_large instead of hanging or growing.
  • Live view: camera.streamVideo() spawns ffmpeg internally and hands the daemon raw MPEG-TS bytes via stdoutCallback, which get piped straight into an mpv - window. One live view at a time — starting a new one stops whatever was already playing.
  • Snapshot: camera.getSnapshot() writes a JPEG to ~/.local/state/omarchy/ring-cameras/snapshots/ and the panel opens it with xdg-open.
  • History: camera.videoSearch({ dateFrom, dateTo, order }) lists recent events (last 48 hours, capped at 30) with a directly playable URL per clip — no ffmpeg piping needed, just mpv <url>, since these are plain HTTPS mp4s. Those URLs are presigned and expire in ~15 minutes (Ring sets X-Amz-Expires=900), so the panel fetches a fresh list each time you open the history section rather than caching it.
  • Motion notifications: RingCamera auto-subscribes itself to Ring's push (ding/motion) stream on construction, and RingApi auto-registers the push receiver too — both using Ring's own embedded Android-app Firebase credentials, no setup needed on our end. The daemon listens on each camera's onNewNotification, and on a motion event fires notify-send -A "default=View clip" ..., which blocks and prints the chosen action to stdout — clicking the notification body plays the clip in mpv; dismissing/ignoring it does nothing further. Motion→clip resolution tries camera.getRecordingUrl(dingId) first (targets Ring's short-lived "recent dings" feed) and falls back to a videoSearch over the last 15 minutes matched by ding id if that 404s — verified live that getRecordingUrl really does reject an aged-out ding ("Url not found") and that the videoSearch fallback recovers a valid URL for the exact same id.
  • bin/ctl.mjs (installed as omarchy-ring-cameras-ctl) is a thin CLI over the control socket, same shape as the QML panel uses. Commands with secrets (link_start, link_2fa) take --stdin instead of a JSON argv payload — argv is readable by any other process on this machine via /proc/<pid>/cmdline for as long as the (short-lived) ctl process is alive, so the QML panel writes those to the child's stdin instead (stdinEnabled: true + write(), the same pattern Omarchy's own Wi-Fi panel uses for passphrase entry).
  • src/fetch-fix.mjs — forces the global fetch to undici's own implementation. ring-client-api builds its network Agent from its own npm-installed undici dependency but calls the global fetch to use it as a dispatcher; on Node v26 (this machine) that global fetch is backed by a different internal undici version, and the mismatch doesn't error — it silently never dispatches the request. Without this fix, every login and API call hangs forever with zero network activity. Imported first, before any ring-client-api import, in both daemon.mjs and bin/setup.mjs.

Setup

Either install through Omarchy's plugin marketplace:

omarchy plugin add https://github.com/ninepointlabs/omarchy-ring-cameras --enable
cd ~/.config/omarchy/plugins/tim.ring-cameras
npm install
./install.sh

...or clone it yourself anywhere and symlink it in:

git clone https://github.com/ninepointlabs/omarchy-ring-cameras
cd omarchy-ring-cameras
npm install
ln -s "$PWD" ~/.config/omarchy/plugins/ring-cameras
./install.sh   # symlinks omarchy-ring-cameras-ctl onto PATH, generates
               # and enables the systemd --user service

omarchy plugin add only clones and validates — it doesn't run npm install or install.sh for you, so that second step is required either way. install.sh resolves node from your PATH and the repo's actual location at install time (not hardcoded), so it works regardless of which Node version manager you use or where you cloned it.

Then open the "Ring Cameras" bar icon (right section). If no account is linked yet, the panel itself prompts for email/password (and a 2FA code if your account uses it) — no terminal step needed. bin/setup.mjs still exists as a CLI fallback with the same login logic, useful for debugging from a terminal with visible output.

Operating it

systemctl --user status omarchy-ring-cameras.service
tail -f ~/.local/state/omarchy/ring-cameras/daemon.log
omarchy-ring-cameras-ctl status
omarchy-ring-cameras-ctl list_cameras

Not done yet / caveats

  • Verified against a real account: login (both bin/setup.mjs and the in-panel form), list_cameras, snapshot, history, and live view have all been used successfully against a real Ring account (multiple start/stop cycles logged with no errors).
  • Motion notifications are unverified against a real event yet — the daemon starts cleanly with the new subscription code, notify-send fires correctly (confirmed structurally), and the click→play resolution logic was verified piece-by-piece against real historical data, but nobody has triggered a real motion event and actually clicked the resulting notification end to end.
  • Playing a history clip via the panel's Play button is likewise unverified — mpv <url> against a real presigned S3-style URL hasn't been watched end to end, only confirmed the URLs come back well-formed.
  • If login ever fails with Cannot use 'in' operator to search for 'error' in ..., that's Ring's edge/WAF returning a plain-text (non-JSON) body, which the library's error formatting doesn't handle — seen once with an obviously-fake test email, not with the real account.
  • Only one live view at a time; starting a second stops the first. Recording playback is independent of live view — each clip opens its own mpv window and isn't tracked or stoppable from the panel; close it like any other video window.
  • No light/siren toggle yet, even though RingCamera supports both (setLight, setSiren) — straightforward to add to daemon.mjs and the panel if wanted.
  • package.json pins ring-client-api@^13.0.0, which warns it wants Node 18/20/22; this was developed and tested on Node 26. The undici/fetch mismatch above was a direct consequence of that gap — worth assuming there could be others.
  • npm audit originally reported 7 transitive vulnerabilities, all several layers deep inside werift (the WebRTC library behind live view) and socket.io-client. Triaged: uuid and parseuri had patched versions, now pinned via package.json's overrides field, which fixed 4 of the 7 — live view re-tested afterward (start, confirmed streaming, stop) to make sure pinning them didn't break the WebRTC signaling path, since that's exactly where they sit. The remaining 3 are all the same ip package SSRF advisory (counted once per dependency path): it has no patched version at all (npm audit lists it as ip * — every published version matches), so there's nothing to update to. Low real-world risk here regardless, since it's only used internally against Ring's own relay servers, never attacker-controlled input.
  • The systemd unit sets LimitCORE=0, NoNewPrivileges=true, and PrivateTmp=true, but intentionally skips ProtectHome/ProtectSystem sandboxing (unlike a headless daemon) because it needs to spawn mpv against your live Wayland session, and doing that safely needs careful path allow-listing (Wayland/audio sockets under /run/user/<uid>, plus this project's own data/state dirs under ~/.local/) that hasn't been worked out yet.

License

MIT