Omahub
← All plugins
N

Dockhand Control

by nag3sy

Dockhand control center in your bar: update dashboard, one-click container updates, host stats, and safe image cleanup

Security review

Review recommended · 1 finding

Deterministic scan — not a security guarantee

Low
Risk level
Low
Analyzed commit
5b6d54e
Scanned
1 month ago

Flagged patterns appear only in documentation files (README / docs) — descriptive examples, not executable code.

  • Docs external_hosts README.md:174

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

    git clone https://github.com/nag3sy/dockhand-control-omarchy-plugin \

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
5b6d54e
Reviewed
1 month ago

This is a QML bar widget that interacts only with the user-configured Dockhand API; the token/state file handling is hardened with 0600 permissions, O_NOFOLLOW reads, bounded sizes, and atomic writes, and no hidden installer or destructive code was found. The deterministic 'external host' finding points to a README git-clone example, not executable code.

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/nag3sy/dockhand-control-omarchy-plugin --enable
System #bar #quickshell #system

<img src="icon.png" width="40" align="top" alt=""> Dockhand Control

Dockhand Control

A bar-widget plugin for Omarchy that turns your Dockhand instance into a control center one click away: a live update badge, a dashboard of every Docker host Dockhand manages, one-click container updates, and safe image cleanup — without opening a browser tab.

Upgrading from Dockhand Updates? Your connection and token migrate automatically on first launch — nothing to reconfigure. (Remove the old plugin first so two widgets don't overlap.)

Works with a Dockhand instance reached by local IP (http://192.168.1.50:3000) or by a domain you've exposed publicly (https://dockhand.example.com) — it's just a URL. If Dockhand manages more than one Docker host, every environment it knows about is included.

Features

Overview — a card per Docker environment:

  • Container counts: running/total, pending updates, unhealthy — with an online/offline status dot.
  • Image, volume, and stack totals.
  • Live CPU and memory bars from Dockhand's own host metrics collector.
  • Host line: platform, architecture, CPU count, Docker version, image disk usage, and uptime.

Updates — everything the badge counts, now actionable:

  • One-click update per container, or Update all in one shot. Updates run through Dockhand's own batch-update endpoint — the same tested recreate path its web UI button uses; the widget only ever sends container IDs, never a hand-built recreate payload. Recreating a container briefly interrupts it, so the action asks for confirmation first (configurable).
  • Live per-row progress and a result summary — including per-container failure reasons when an update doesn't take.
  • Changelog links — one click through to Dockhand's resolved GitHub/Gitea/Forgejo release notes, for images on a pinned version.
  • Open in Dockhand — deep link to that container on Dockhand's own Containers page.
  • Check now — triggers a real registry check on demand, separate from the cheap automatic poll.

Cleanup — reclaim disk space, safely:

  • See where Docker's bytes live per environment: image layers on disk, container layers, build cache, volumes.
  • Prune dangling images — removes only untagged images nothing references.
  • Prune all unused images — also removes tagged images no container is currently using. Dockhand still skips everything in use and shields its own scanner/backup helper images; this is the one action that always asks for confirmation, even with confirmations turned off.
  • Result feedback: how much space each prune reclaimed.

Settings — everything configurable lives in one tab:

  • Connection (URL + API token), with an inline cleartext-HTTP warning.
  • Refresh interval: 5/10/15/30/60 minutes.
  • Confirmations on/off for updates and prunes.

Screenshots

Every tab, one click from the bar:

Overview Updates
<img src="screenshots/overview.png" width="380"> <img src="screenshots/updates.png" width="380">
Cleanup Settings
<img src="screenshots/cleanup.png" width="380"> <img src="screenshots/settings.png" width="380">

(The Updates screenshot shows demo data; everything else is the real UI. The connect flow is the first thing you see — it's the Settings tab before a connection is saved.)

Setup

  1. Click the widget in the bar.
  2. Enter your Dockhand URL.
  3. If your instance has Dockhand's own login enabled, log into Dockhand in a browser, click your avatar at the bottom of the sidebar → Profile, find the API tokens card, click Generate token, and paste the value in. Leave the token blank if the instance has no authentication.
  4. Save.

Dockhand has no scoped/read-only token type — a token carries whatever permissions its owning user has (containers, stacks, everything). That was already true of the read-only version of this plugin; now that the widget can actually update and prune, it matters more. Create a dedicated user for this if you want to limit the blast radius of a leaked token; otherwise treat the value the same as your Dockhand password.

If you've put your Dockhand instance behind a reverse-proxy SSO gate (Authelia, Cloudflare Access, Pomerium, etc.) instead of, or in addition to, Dockhand's own login, a Dockhand API token alone won't get past that gate — the proxy redirects the request to its own login page before Dockhand ever sees it, and the widget will show ! ("Couldn't read Dockhand's response" — it got login-page HTML back instead of JSON). Either add a bypass rule for /api/* on that proxy host (Dockhand's own token auth still protects it once you're past the proxy), or point the widget at Dockhand's local IP instead, which skips the public-facing proxy entirely.

What the badge means

  • A number — how many containers across every environment have a real image update pending (Dockhand's own hasImageUpdate flag: a newer digest exists for the image tag you're running).
  • Nothing shown — everything's up to date.
  • ! — the widget couldn't reach Dockhand or the token was rejected; open the popup for the specific error.

The "Newer version tags available" section (when present) covers Dockhand's advisory semver-tag detection for pinned versions — informational only, never counted in the main badge, never auto-applied by Dockhand or this plugin.

Refreshing

  • Automatic, every 5–60 minutes (your choice, default 10): a read of Dockhand's own cached state (GET /api/containers/pending-updates and GET /api/dashboard/stats) — no registry calls of their own, so this is cheap and safe to poll.
  • On popup open: the above, plus per-environment host facts and Docker disk usage (GET /api/host, GET /api/system/disk) — static-ish values that don't need a timer.
  • "Check now": triggers a real check against each container's registry (POST /api/containers/check-updates) across every environment, then re-reads the cached result. This is the expensive path — same as clicking "Check for updates" in Dockhand itself — so it's manual-only, never on the poll timer.

Toggling from a keybind

The widget registers an IPC handler, so you can open it without the mouse:

omarchy-shell dockhand-control toggle   # or: open / close
omarchy-shell dockhand-control tab updates   # jump straight to a tab: overview | updates | cleanup | settings

For example, in Hyprland config:

bind = SUPER, D, exec, omarchy-shell dockhand-control toggle

Migrating from Dockhand Updates

omarchy plugin remove io.github.nag3sy.dockhand-updates
omarchy plugin add https://github.com/nag3sy/dockhand-control-omarchy-plugin --enable

On first launch the new plugin reads the old one's settings file (~/.local/state/omarchy/settings/dockhand-updates.json) and re-persists it under dockhand-control.json — your URL, token, and connection carry over untouched. The old file is left in place; delete it yourself for a clean slate.

Install

Dependencies: none beyond a stock Omarchy install. The state-file helpers use only the system perl (read side) and python3 (write side) — both present on a default Arch/Omarchy system — and the widget talks to your Dockhand instance over HTTP(S) directly from Quickshell. No packages, build steps, or bundled binaries.

omarchy plugin add https://github.com/nag3sy/dockhand-control-omarchy-plugin --enable

Or manually:

git clone https://github.com/nag3sy/dockhand-control-omarchy-plugin \
  ~/.config/omarchy/plugins/io.github.nag3sy.dockhand-control
omarchy-shell shell rescanPlugins
omarchy plugin enable io.github.nag3sy.dockhand-control --section right

Remove

omarchy plugin remove io.github.nag3sy.dockhand-control

This stops all network activity from the plugin. It does not delete ~/.local/state/omarchy/settings/dockhand-control.json (your URL, token, and preferences); delete that file yourself for a clean slate.

How it works

Dockhand API endpoints, called directly via QML's XMLHttpRequest — no helper scripts for networking, no eval/shell usage anywhere in the plugin:

Endpoint Used for Called
GET /api/environments List the Docker hosts Dockhand knows about Every poll, and before "Check now"
GET /api/containers/pending-updates?env={id} Cached result of the last check — the source of the badge count Every poll (5–60 min)
GET /api/dashboard/stats Per-environment counts, sizes, and the host CPU/memory sample Every poll (5–60 min)
GET /api/host?env={id} Platform, CPUs, Docker version, uptime On popup open, and after a prune
GET /api/system/disk?env={id} Docker image layer disk usage On popup open, and after a prune
POST /api/containers/check-updates?env={id} Runs a fresh registry check Only from "Check now"
POST /api/containers/batch-update One-click updates — Dockhand's own recreate path; the body is only ever { containerIds: [...] } Only from a confirmed "Update"/"Update all"
POST /api/prune/images?env={id}&dangling= Image cleanup; with Accept: application/json Dockhand answers synchronously with the final result Only from a confirmed prune button
GET /api/containers/{id}/version-notes?versions=…&env={id} Resolved GitHub/Gitea/Forgejo release notes for a newer-version-tag suggestion Only from a row's "Changelog" link

Plus browser navigations, Qt.openUrlExternally, for a row's "Open in Dockhand" link and a row's "Changelog" link.

An optional Authorization: Bearer <token> header is sent with every API request when a token is configured; requests go out with no auth header at all otherwise. Every request goes only to the base URL you typed in — never a redirect target or anything derived from a response.

Security

This plugin's v1 was self-reviewed against the Omarchy plugin marketplace's security baseline. v2 keeps every discipline below and adds actions, so the review notes around those are expanded.

  • No command execution for networking. Every Dockhand call is a plain XMLHttpRequest; there is no sudo, pkexec, doas, eval, or shell invocation anywhere in the plugin. The only processes ever spawned are two fixed-argument helper scripts acting on the plugin's own settings file — the read-side and write-side helpers described below — and a one-argument allowlisted IPC method. No helper takes attacker-influenced input.
  • Every entry point is mouse, timer, or local IPC — and the IPC surface only drives the popup. Besides the bar pill's click/hover and the poll timer, the widget registers one Quickshell IpcHandler (omarchy-shell dockhand-control toggle|open|close|tab <name>) so the popup can be bound to a key. Its methods take no arguments except tab, whose argument is compared against a fixed allowlist of tab ids before it selects anything; no IPC method performs a network request, an update, or a prune on its own — toggle/open just open the popup and refresh data exactly as a click would.
  • One user-supplied host, validated before use. The base URL must match ^https?:\/\/[^\s/]+ before Save is even enabled, and every request is built from that stored value — never a redirect target, header, or response body. Every response is aborted mid-transfer the moment it crosses an endpoint-specific byte cap — checked against the declared Content-Length as soon as headers arrive, and again against bytes buffered so far as the body streams in — rather than downloaded in full and only then discarded. Every request also has an explicit timeout (up to 5 minutes for update/prune actions, which legitimately pull images or delete layers; 10 seconds for reads).
  • A real credential, handled like one — protected from the moment it hits disk, and bound to the object it was verified against. Unlike most Omarchy plugins, this one can hold a genuine bearer token (Dockhand's own per-user API tokens — there's no scoped/read-only variant). Writes go through a helper script (scripts/write-state-file, python3, stdlib only) that opens the temp file exactly once and holds that descriptor for the whole lifecycle: mode 0600 and ownership are verified with fstat on the descriptor before any byte is written; the content arrives on stdin (the token never crosses process argv or environment) through a bounded incremental copy that aborts mid-stream past the 64KB cap; fsync, fchmod and re-verification all target the same descriptor; and the publish is descriptor-bound too — the validated inode is hardlinked via /proc/self/fd (the kernel resolves that to the open file, never re-resolving the replaceable temp pathname) and the hardlink is renamed over the destination, followed by an identity check that the published object is the exact file that was written and verified. Anything else — a swapped temp path, a non-regular destination, a payload over the cap, a destination directory that isn't a real user-owned directory — fails loudly and the old state file stays intact. Reads re-tighten too: any file carrying group/other bits (e.g. written by an older version) is reduced to 0600 on the open descriptor before its contents are read, failing closed if that can't be completed. The token field in the popup is masked by default with an explicit Show/Hide toggle, never logged, and only ever sent as an Authorization: Bearer header to the one configured host.
  • Every outbound browser link is scheme-checked at the point it opens. Both Qt.openUrlExternally call sites — "Open in Dockhand" and a row's "Changelog" — go through Model.isSafeExternalUrl, which requires an http(s):// prefix immediately before the call. That matters most for "Changelog": that URL comes from Dockhand's version-notes response (resolved from GitHub/Gitea/Forgejo, not typed by you), so it's the one link target this plugin doesn't fully control the shape of. A non-http(s) value there is refused and shown as "Couldn't open changelog" instead of ever reaching Qt.openUrlExternally.
  • A cleartext-token warning, not just documentation. If the URL in the setup form is http:// to something outside a private IP range (RFC1918/loopback) while a token is entered, the form shows a warning inline — Model.isCleartextRisk — rather than only mentioning the risk in this README. Non-blocking (some setups have legitimate reasons for plain HTTP on a private network this check doesn't recognize), but it's in front of you at the moment it matters.
  • Actions go through Dockhand's own tested paths, and nothing destructive is silent. Updates are POST /api/containers/batch-update with a list of container IDs Dockhand already knows — the widget never constructs the recreate payload (image, env, ports, volumes, secret resolution) that a raw container update would need, because getting that subtly wrong from outside Dockhand risks breaking a running container. Prune targets images only — never volumes, never containers — and Dockhand's own protections (in-use checks, scanner/backup helper shielding) apply on top. Every update and prune passes through an in-popup confirmation dialog first; "prune all unused images" and container updates ask even when confirmations are turned off in settings. No action ever fires automatically, on a timer, or without a click.
  • Local storage is one file. Your URL, token, and preferences live in ~/.local/state/omarchy/settings/dockhand-control.json. Removing the plugin doesn't delete it; see Remove. It's read through a helper script (scripts/read-state-file) that opens the path with a single O_NOFOLLOW|O_NONBLOCK descriptor, validates regular-file type and size against that same descriptor, and only then reads up to a 64KB cap — no separate check-then-open window for a symlink/FIFO/oversized-file swap to land in. Quickshell's FileView is used only to watch the path for external edits — it never reads the file (it has no size-limited or streaming read of its own) and never writes it (writes go through the 0600-from-creation helper above). The v1 migration reads the old dockhand-updates.json through the same helper, once, only when the new file doesn't exist yet.
  • Dockhand-sourced text is never treated as rich text. Container, image, and environment names come from your own Dockhand API and are rendered with textFormat: Text.PlainText everywhere they're shown, so a container named to look like markup can't be interpreted as one. The one sink this plugin doesn't render itself — the shared ConfirmDialog's message label — is fed through the same sanitizer (Model.plainText, which strips </> entirely) before anything Dockhand-sourced reaches it, so no confirmation string can carry markup into it either.
  • No privileges. Never requests sudo/pkexec, never touches system configuration, and never reads or writes any file other than its own settings file above.

The one residual risk worth naming: QML's XMLHttpRequest has no exposed API to restrict redirect targets or protocols, so a DNS-hijack or compromised-cert scenario against your configured host isn't something this plugin can defend against at its own layer — that's a platform limitation, not something specific to this code. If your Dockhand is only reachable locally, prefer http:// to a private IP over exposing it publicly; if you do expose it via a domain (as this author does), put it behind HTTPS and Dockhand's own authentication.

License

MIT — see LICENSE.