Omahub
← All plugins
T

Howdy Face Unlock

by tslove923

Windows Hello-style face unlock for IR cameras on Omarchy, via Howdy: mirrors the fingerprint flow (setup/remove scripts, Setup > Security menu entries) and works on the stock lock screen and Lock Screen Explorer, with a health check that warns if an update breaks it.

Security review

Review recommended · 38 findings

Deterministic scan — not a security guarantee

Medium
Risk level
Medium
Analyzed commit
5dcafaa
Scanned
4 days ago
  • medium sudo setup:46

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo mv "$file.tmp" "$file"
  • medium sudo setup:147

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo tee "$PAM_HOWDY" >/dev/null <<'EOF'
  • medium sudo setup:95

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo mkdir -p "$MODELS_DIR"
  • medium sudo setup:96

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo howdy config >/dev/null 2>&1 || true  # lay down howdy's default file first
  • medium sudo setup:100

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo howdy config` opens the
  • medium sudo setup:114

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo find /etc/linux-enable-ir-emitter -type f 2>/dev/null) ]]; then
  • medium sudo setup:127

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo linux-enable-ir-emitter --device "$ir_device" --verbose configure --no-gui --limit -1
  • medium sudo setup:143

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo howdy add
  • medium sudo setup:161

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo cp -a "$LOCK_QML" "$LOCK_QML_ORIG"
  • medium sudo setup:173

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo mktemp -d)
  • medium sudo setup:174

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo cp "$LOCK_QML" "$scratch_dir/Service.qml"
  • medium sudo setup:175

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo cp "$root/patch-lock-howdy.py" "$scratch_dir/patch-lock-howdy.py"
  • medium sudo setup:176

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo sha256sum "$scratch_dir/patch-lock-howdy.py" | awk '{print $1}')
  • medium sudo setup:178

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo rm -rf "$scratch_dir"
  • medium sudo setup:184

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo python3 "$scratch_dir/patch-lock-howdy.py" "$scratch_dir/Service.qml"
  • medium sudo setup:185

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo install -o root -g root -m 644 "$scratch_dir/Service.qml" "$LOCK_QML"
  • medium sudo setup:186

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo rm -rf "$scratch_dir"
  • medium sudo setup:205

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo timestamp being used to run tampered bytes
  • medium sudo setup:279

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit ----------------------------
  • medium sudo setup:291

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit as well? A typed password still"
  • medium sudo setup:293

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit? [Y/n] " reply || true
  • medium sudo setup:298

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit..."
  • medium sudo setup:303

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit just keep using your password.\e[0m"
  • medium sudo setup:330

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo authorization is still
  • medium sudo remove:18

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit..."
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo session warm from omarchy-update's own pacman prompt can't be made to
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo in this same
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo should still be authenticated here without a fresh
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo not available non-interactively; skipping repair (will notify instead)."
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo timestamp is
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit.
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit..."
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit prompts."
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and polkit..."
  • Docs sudo README.md:58

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo session is still authenticated, so face unlock
  • Docs sudo README.md:81

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo mktemp -d`, mode `700`) and running
  • Docs sudo README.md:146

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and
  • Docs sudo README.md:151

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo never prompts, so PAM is never consulted) -- polkit still

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
5dcafaa
Reviewed
4 days ago

This plugin integrates Howdy face unlock into the Omarchy lock screen. It requires sudo to install packages and modify system files, but all operations are transparent, well-documented, and reversible. The code includes security measures like hash-pinning the patcher and using marker-based changes, and the setup script is run manually by the user, so the risk is low.

  • The setup script runs many commands with sudo, including installing AUR packages and modifying PAM configuration for sudo/polkit, which could be a security concern if not handled carefully, but the code uses reversible, marker-tagged changes and keeps pam_unix first.
  • The plugin patches a package-owned file (/usr/share/omarchy/shell/plugins/lock/Service.qml) and relies on a post-update hook to re-apply the patch after updates; if the patch fails, the health check notifies the user, but there is a small risk of breaking the lock screen if the patch is applied incorrectly.
  • The plugin optionally enables face auth for sudo and polkit, which could weaken security if the user's face is easily spoofed, but this is opt-in and clearly explained.
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/tslove923/omarchy-howdy-face-unlock --enable
System #system #security
<h1 align="center">Howdy Face Unlock</h1> <h3 align="center">Windows Hello–style unlocking for Omarchy — just look at the camera.</h3> <p align="center"> <a href="https://plugins.omarchy.org/plugin.html?id=io.github.tslove923.howdy-face-unlock">Omarchy Plugins</a> · <a href="https://github.com/tslove923/omarchy-howdy-face-unlock/issues">Issues</a> </p>

Face unlock for laptops with an infrared camera, built on Howdy and wired into the Omarchy lock screen. Open the lid, look up, and you're in — no password, no fingerprint pad. It mirrors the fingerprint flow as closely as Omarchy's plugin system allows: a dedicated PAM service, a face glyph where the fingerprint icon lives, and the same "already scanning when the lid comes up" feel.

Install

omarchy plugin add https://github.com/tslove923/omarchy-howdy-face-unlock
~/.config/omarchy/plugins/io.github.tslove923.howdy-face-unlock/setup

omarchy plugin add only clones files — run setup yourself afterward. It detects your IR camera, installs howdy-git and linux-enable-ir-emitter from the AUR (building python-dlib CPU-only unless it detects an Nvidia GPU, to dodge that AUR package's broken CUDA subpackage; the CPU-only build clones the AUR PKGBUILD pinned to a fixed commit SHA so upstream can't move under the build), configures the emitter, enrolls your face, and wires up the lock screen.

Setup and removal are also reachable from the Omarchy menu: Setup → Security → Face Unlock and Remove → Security → Face Unlock (both only appear once an IR camera is detected / Howdy is installed, respectively).

Remove

~/.config/omarchy/plugins/io.github.tslove923.howdy-face-unlock/remove
omarchy plugin remove io.github.tslove923.howdy-face-unlock

Why this is a plugin and not just a script

It isn't, entirely. Omarchy's lock screen (omarchy.lock) is a single first-party service-kind plugin with no hook for a third party to add a new PAM auth backend to it — plugins mount independently, they don't extend each other. So setup still has to hand-patch /usr/share/omarchy/shell/plugins/lock/Service.qml directly, the same as it would if this were a loose script. That file is package-owned, so an omarchy update can silently revert the patch.

What the plugin does buy: setup installs a post-update hook (omarchy hook install post-update ...) that runs during omarchy update, right after system packages and migrations — i.e. right after the patch may have just been overwritten. It re-patches the file there, silently, while omarchy update's own sudo session is still authenticated, so face unlock survives an update on its own; you don't need to notice anything or rerun setup yourself in the common case.

That repair can only fail if sudo isn't authenticated non-interactively at that point (e.g. an unusual update flow). For that case, this repo's Service.qml also runs as a real first-class Omarchy service and checks on every shell start whether the patch survived. If it's still missing, you get a critical desktop notification telling you to rerun setup, instead of face unlock just silently going dead until you notice at the worst possible moment (i.e., at the lock screen).

On a dev checkout (OMARCHY_PATH pointing at a source tree), the running shell loads the lock plugin from $OMARCHY_PATH/shell/plugins/lock/Service.qml rather than /usr/share/omarchy/..., so setup and this health check resolve that path too. A git pull that reverts it is the dev equivalent of an omarchy update overwriting the packaged copy.

Query its health directly: omarchy-shell howdy status → ok or broken.

How the patch step stays safe to run as root

Both setup and the post-update hook patch lock/Service.qml by staging a copy into a root-owned scratch dir (sudo mktemp -d, mode 700) and running patch-lock-howdy.py from there instead of straight out of this checkout. Staging alone isn't enough, though: by the time that patch step runs, sudo is already authenticated from earlier in the same script (or, for the hook, from omarchy update's own pacman prompt) — so anything running as the invoking user could still swap patch-lock-howdy.py in this user-writable checkout right up until root's cp reads it, and root would stage and execute those swapped bytes instead. Moving the read into a root-owned directory shrinks that window; it doesn't close it.

What closes it: both call sites pin patch-lock-howdy.py's sha256 as a constant and have root verify the staged copy against it before ever executing it, refusing on mismatch. That checks what is about to run rather than where it was staged from, so a swapped file is caught regardless of timing. Bump the pinned hash in both setup and hooks/post-update.d/repair-howdy-lock.hook if you ever modify patch-lock-howdy.py.

Lock Screen Explorer support (see below) does not patch any file, so the hash pin above applies only to patch-lock-howdy.py. It writes its own PAM service, module symlink, and model symlinks directly (via bin/howdy-lock-face-adapter), all as root-owned system paths it creates itself rather than as a patch to someone else's package-owned file — so there is no third-party file whose bytes root has to trust the way the stock lock/Service.qml is.

What setup actually changes

  • Installs howdy-git, linux-enable-ir-emitter, v4l-utils, python-dlib
  • /etc/howdy/config.ini — tuned for IR (dark_threshold, certainty, max_height), pointed at the camera by its stable /dev/v4l/by-id/ path rather than a bare /dev/videoN (the kernel renumbers those across reboots and USB re-enumeration, which would break face auth silently), workaround = off (Howdy's default input workaround tries to fake an Enter keypress via /dev/uinput to unblock a legacy simultaneous-password-prompt flow this lock screen doesn't have — with a dedicated PamContext per auth method, it just hangs), and made world-readable (the lock screen's PAM module runs unprivileged, in-process, with no root daemon to broker access the way fprintd has, so it has to be able to read its own config as that user)
  • /etc/pam.d/omarchy-lock-howdy — a PAM service dedicated to Howdy, independent of omarchy-lock-fingerprint
  • lock/Service.qml — a parallel startHowdy()/howdyPam/howdyCheckProc path, wired in alongside the existing fingerprint one. When the lock screen engages — e.g. when you close the lid — Howdy starts authenticating right away, retrying every 250 ms on failure. So face unlock is already live when you open the lid: just look at the camera. A failed attempt only keeps retrying while there's been recent activity (the lock just engaged, or a wake signal — mouse move, key press, a password attempt — within the last 10s); otherwise retries pause instead of burning through attempts against an empty room while nobody's there. Any wake signal while paused (or even after full lockout) resets the attempt count and re-arms a fresh attempt immediately, so face unlock is live again the moment you're actually back — not still shaking off a lockout from the last time it scanned an empty desk. Only 5 failed attempts in a row with someone actually present trips the fallback to password-only for the rest of that lock. A retry never starts while a password submission is already in flight, so typing your password doesn't risk a fresh camera scan landing right as it succeeds.
  • Before trusting Howdy as configured, the lock screen checks that /etc/pam.d/omarchy-lock-howdy, your enrolled face model, and pam_howdy.so are all root-owned and not group/world-writable — the same way it already trusts nothing it can't verify for fingerprint/PAM. Session code able to rewrite any of those could otherwise enroll a face everyone matches or swap in an auth module that always succeeds.
  • Optional (asked during setup, default yes): face auth for sudo and polkit. pam_unix stays first with try_first_pass, so a typed password authenticates exactly as before and only an empty Enter falls through to a face scan; howdy itself declines over SSH and when the lid is closed. Undone by remove. Note: this is inert where the user has blanket NOPASSWD in sudoers (sudo never prompts, so PAM is never consulted) -- polkit still applies, since GUI auth prompts for a password regardless.
  • Optional (asked during setup): a facelock-shaped compatibility surface so Lock Screen Explorer's own face UI runs Howdy — see Lock-screen replacement plugins below. Nothing inside Explorer's own files is modified.

Known rough edges

  • linux-enable-ir-emitter configure doesn't manage to save anything on every camera — some just work under plain capture, and the tool's own "already working" pre-check exits non-zero for that. setup treats this as informational, not fatal.
  • Omarchy does not ship face unlock yet. An earlier Howdy PR (#5212) was closed as a duplicate; the current open one is omacom/omarchy#8336 ("Add face authentication (howdy) to the lock screen"). It takes a similar approach to the stock-lock half of this plugin -- the same omarchy-lock-face PAM service -- but patches Omarchy core rather than shipping as a plugin. This project is tracking #8336: if it merges, the stock-lock patch here becomes redundant, but the Lock Screen Explorer shim would still be needed, since #8336 only covers the stock lock screen. Until then, this plugin is how you get face unlock today.

Lock-screen replacement plugins (Lock Screen Explorer)

Plugins that replace the lock screen entirely declare "omarchy": {"clonedFrom": "omarchy.lock"} in their manifest. Enabling one makes Omarchy disable the stock omarchy.lock service and load the replacement's own Service.qml for the lock IPC target instead — Omarchy only ever loads one lock-targeting service at a time, so whichever one isn't currently enabled is dormant.

Lock Screen Explorer (io.github.sirjul1337.lock-explorer) is one such plugin, and it ships its own native face-unlock UI. Unfortunately it can only drive a facelock-shaped backend, and none of that is configurable:

  • it hardcodes PamContext { config: "omarchy-lock-face" }, and
  • its check-face-auth.sh only offers the face UI when it finds pam_facelock.so in /etc/pam.d/omarchy-lock-face, the module /usr/lib/security/pam_facelock.so, and a model under /var/lib/facelock/models/*.onnx.

Omarchy has no plugin-to-plugin auth hook, so the only way to run Howdy behind Explorer's face UI is to provide that facelock-shaped surface ourselves — backed by Howdy, entirely outside Explorer's tree:

file purpose
/etc/pam.d/omarchy-lock-face the PAM service Explorer calls; runs pam_facelock.so
/usr/lib/security/pam_facelock.so a symlink to pam_howdy.so
/var/lib/facelock/models/*.onnx symlinks to your Howdy face models

In other words: Explorer's "facelock" face path resolves to Howdy. The facelock names are Explorer's vocabulary; everything they point at is Howdy. Nothing here claims facelock is installed to anything but Explorer's own detector, and a real facelock install is never shadowed — the adapter backs off entirely if it finds one.

This is implemented in bin/howdy-lock-face-adapter (install / remove) and is opt-in: setup asks before writing these files, explaining exactly what they are. Skipping it changes nothing for the stock lock screen, which is patched regardless.

Why provide a surface instead of patching Explorer's Service.qml (the earlier approach, now removed): patching was fragile — Explorer's anchors drift on every release — and it injected a second face stack next to Explorer's own native one. Providing the surface instead is:

  • install-order independent — it doesn't matter whether Explorer is installed before or after this plugin; if Explorer is added later it finds the surface already in place, and if it's absent the surface is simply inert;
  • update-proof — omarchy plugin update rewrites Explorer's files, but none of ours live there, so there is nothing to revert;
  • non-fatal — if the adapter can't be installed, the rest of Howdy's install still succeeds.

omarchy-shell howdy status reports whether face unlock is healthy for whichever lock screen is actually live (the adapter surface when Explorer is enabled, the stock patch otherwise). A broken verdict is only trusted after it repeats across several spaced checks well after startup, so a transient misread — a still-starting shell, a plugin mid-reload — cannot raise a false alarm. The post-update hook refreshes the adapter after omarchy update if it was previously set up.

Backup note: never keep a copy of a plugin directory inside ~/.config/omarchy/plugins/ (e.g. a foo.bak-.../ sibling). The shell scans every subdirectory with a manifest, so a second copy carrying the same id can be the one it loads — which will silently serve stale code.