Omahub
← All plugins
N

OmaPosture

by Nathan Grobler

Webcam posture monitor. Turns the bar red and notifies you when you start slouching. Runs fully offline and never stores images.

Security review

Potentially dangerous behavior detected · 1 finding

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
e9e9b9d
Scanned
1 month ago

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

The plugin is a transparent posture monitor: it installs a systemd user service, prompts before installing python-opencv, accesses the webcam only to compute pose metrics, and writes state to the user's own state directory. The deterministic scan's high-risk finding refers to the service unit, which is the documented and intended mechanism for the monitoring feature, not hidden persistence. No obfuscation, network exfiltration, or destructive commands were found in the sampled code.

  • Installs and enables a systemd user service that starts with the graphical session and uses the webcam; this is the plugin's core function and is disclosed in the README.
  • Setup uses `omarchy pkg add` with a password prompt to install python-opencv; acceptable because it asks first and is clearly described.
  • The full daemon source was not exhaustively sampled, so the no-image-storage claim rests on the reviewed portions and author reputation; no contradicting code was seen.
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/CodeFoundryZA/omarchy-posture --enable
Productivity #bar #quickshell #ai

OmaPosture

Watches your posture through your webcam and tells you when you start slouching. The bar widget turns red and a critical notification fires when you drift from a baseline you calibrate yourself.

Runs entirely on your machine. No images are ever written to disk or sent anywhere: frames live in memory for the fraction of a second they are analysed and are then discarded. Only numbers are stored.

OmaPosture panel and history report

The panel shows how far each axis has drifted from your baseline, today's good-posture share, and a column per hour. posture history prints the same data in a terminal along with a seven day trend and your recent slouches.

Requirements

  • Omarchy 4 (Quattro) with the Quickshell shell
  • A webcam at /dev/video0
  • python-opencv, which pulls in python-numpy

Setup installs python-opencv for you, so the webcam and Omarchy 4 are the only things you have to bring. jq, v4l2-ctl, fuser and loginctl are also used and already ship with Omarchy.

Installation

omarchy plugin add https://github.com/CodeFoundryZA/omarchy-posture.git --enable

The widget arrives in your bar as a wrench, because cloning a repository cannot install an Arch package or a systemd user service. Click it and setup finishes the job in a terminal: it installs python-opencv, installs and starts the background service, and then hands you back a working monitor.

Nothing is installed without asking first, and the password prompt belongs to omarchy pkg add rather than to this plugin. If you would rather drive it from a terminal, that same script is the one the widget runs:

~/.config/omarchy/plugins/io.github.codefoundryza.posture/bin/posture-install

Then calibrate. This is required, not optional: absolute thresholds cannot work for an arbitrary camera height and desk, so the plugin does nothing until it knows what good posture looks like for you.

~/.config/omarchy/plugins/io.github.codefoundryza.posture/bin/posture calibrate

Sit the way you want to be reminded to sit, and it records a baseline from about ten samples. Recalibrate whenever you move the laptop or change desk setup, with that same command or a middle click on the widget.

Add bin/ to your PATH if you want to type posture instead of the full path.

Removal

The plugin installs one file outside its own directory, a systemd user service. Remove that first, otherwise it is left behind:

~/.config/omarchy/plugins/io.github.codefoundryza.posture/bin/posture-uninstall
omarchy plugin remove io.github.codefoundryza.posture

The uninstall script stops and deletes the service, then asks before touching your calibration and history in ~/.local/state/omarchy/posture. Answer no and a later reinstall picks up your existing baseline.

How it works

A systemd user service (omarchy-posture.service) samples the camera on an adaptive cadence: it opens /dev/video0, grabs a few frames, releases the device, and estimates head and shoulder geometry with BlazePose run through OpenCV's DNN module. The device is held only for the length of a sample, so video calls are never blocked.

A sample costs about 0.7s wall clock, nearly all of it the first read after opening the device, so the interval is what governs how fast a slouch is caught. Sampling therefore stays lazy while nothing is wrong and tightens the moment one bad reading appears:

Situation Interval Setting
posture is fine 6s intervalSeconds
a bad reading is pending confirmation 2s fastIntervalSeconds
nobody at the desk 15s awayIntervalSeconds

Measured end to end that is about 7.6s from slouching to the bar going red and 2.5s back to green, at roughly 3.8% of one core and an 11% camera duty cycle while posture is good.

The daemon owns the state machine and sends the notifications, and publishes everything to ~/.local/state/omarchy/posture/state.json. The Quickshell plugin only reflects that file, so posture monitoring survives omarchy restart shell.

Commands

posture status        # current state
posture calibrate     # record your good-posture baseline
posture pause         # suspend sampling
posture resume
posture sensitivity 1.4
posture history       # today, last 12 hours, last 7 days
posture history --days 30
posture history --json
posture once          # single sample as JSON
posture probe         # camera health check
posture logs

posture lives at bin/posture inside this plugin directory.

Bar widget

  • left click: details panel
  • right click: pause or resume
  • middle click: recalibrate

Inside the panel: C recalibrate, P pause or resume, H open the full history report in a terminal.

The icon carries the state as well as the colour, so the widget still reads correctly without relying on red alone:

State Icon
good md-seat_recline_normal, upright figure
bad md-seat_recline_extra, reclined figure
away md-seat_outline, empty chair
paused md-pause_circle_outline
blocked md-webcam_off
uncalibrated md-crosshairs_question
daemon stopped md-help_circle_outline

The widget uses WidgetButton.active, which paints it with the theme's urgent color, so red stays correct across theme switches.

Metrics

All distances are divided by shoulder width, so they survive small changes in how far you sit from the camera:

Metric Meaning
neckRatio vertical room between shoulder line and nose; collapses when you slouch or crane
earNeckRatio same, measured to the ears
scale apparent shoulder width; grows as you lean toward the screen
frameY height of the shoulder line in frame; grows as you sink down
shoulderTiltDeg side lean
headTiltDeg head tilt

Sensitivity

Calibration records what good posture looks like for you. Sensitivity decides how far you are allowed to drift from it before it complains.

Sensitivity Head drop allowed Side lean allowed Feel
0.5 28% 18° very forgiving, only bad slouches
1.0 14% 9° default
2.0 7% 4.5° strict, small movements trip it

Higher is stricter, because the tolerance is divided by it. Accepted range is 0.5 to 2.5; the CLI rejects anything outside that.

The panel exposes this as four preset chips (Relaxed, Normal, Strict, Strictest) rather than a slider. Sensitivity is a coarse preference, and a slider knob had to be reconciled against a value the daemon only echoes back a few seconds later, which made it spring back under the cursor. A chip cannot disagree with itself that way. - and + in the panel still fine tune by 0.1 for values between the presets, and 0 resets to 1.0; a stored value that matches no preset lights up the nearest one.

Per-metric tolerances live under tolerance in the config file if you want to loosen one axis on its own without touching the others.

Moving the laptop

The metrics split in two. neckRatio and earNeckRatio divide by shoulder width, so they measure your body against itself and survive the camera moving; measured across a real session they sat 0.4% from baseline while sitting well. scale, frameY and the two tilts describe where you sit in the frame, so nudging the screen moves them whatever your posture is doing.

So the daemon watches for the one pattern that can only mean the camera moved: the framing metrics shifting past their tolerance while the scale-invariant ones hold steady. When it sees that sustained over a dozen samples, it re-anchors only the framing baselines and keeps your calibrated neckRatio. No prompt, no recalibration, and the event is recorded in daemon.log.

The condition is deliberately asymmetric. If your body geometry had really changed, neckRatio would have moved too and nothing happens, which is what stops this quietly re-baselining a slouch into the new normal. Set drift.autoReanchor to false to turn it off.

That handles the laptop being nudged. A genuinely different seating setup is a different problem, because your body geometry really does change and the re-anchor correctly refuses to fire. Profiles cover that case.

Profiles

One profile is one calibrated seating setup.

posture calibrate --profile couch   # calibrate a second setup
posture profiles                    # list them, * marks the active one
posture profiles --use couch        # switch by hand
posture profiles --remove couch

The panel has the same controls: a chip per profile to switch, and a + button that asks for a name and then runs calibration in a terminal.

Once more than one exists, the daemon switches automatically to whichever profile fits the readings clearly better than the current one. Your existing single baseline is migrated to a profile named default, so nothing is lost.

There is no location awareness of any kind. The only question it asks is which stored baseline describes the body the camera is currently looking at. profile_fit() scores each profile as a weighted deviation in units of each metric's own tolerance, weighting neckRatio and earNeckRatio most heavily because they are the ones that survive the camera moving. Below about 1.0 means the readings sit inside what that profile calls normal. A rival profile has to score better than 0.6x the active one before it wins, because slouching and a different chair are on the same continuum: measured against a desk baseline, slouching at that desk scores 0.61 and a couch-like posture scores 1.39.

Choosing a profile by hand pins it for ten minutes, so the matcher cannot immediately revert your choice. The pin lapses early if the pinned profile stops fitting at all, so a wrong pick still recovers on its own.

If no profile fits, and posture has read bad for five minutes with not one good sample in between, that is a stale baseline rather than a person who has been slouching continuously. The widget switches to a stale state, says so, sends one notification and then stays quiet instead of nagging you every five minutes about a setup it cannot judge.

States

State Meaning
good inside tolerance
bad out of tolerance for 3 consecutive samples, about 8 seconds
away no person detected for 3 consecutive samples
paused manually paused, session locked, or camera in use by another app
blocked camera streams frames but the sensor sees nothing, so it is covered
uncalibrated no baseline recorded yet

Entering bad sends one critical notification and repeats every 5 minutes while it holds. Recovery is silent.

History

The daemon credits each cycle's elapsed wall time to whichever state was in force, so totals stay honest despite the adaptive interval. Two files hold it:

  • summary.json: hour and day buckets per state, plus the last 500 episodes. This is what the panel reads. Buckets are persisted directly, so nothing has to be replayed on restart.
  • history.jsonl: raw metric rows written at most once a minute, for longer term trends or rebuilding.

Retention is 90 days of daily buckets, 72 hours of hourly buckets, and 20k raw samples. The panel shows today's good-posture share, slouch count and longest slouch, plus a 12 hour column chart where a hollow column means the monitor was not watching that hour. posture history prints the same data plus a 7 day trend and recent slouches.

Only good and bad time counts toward the percentage. away and paused are tracked but excluded, so stepping out does not inflate your score.

Implementation notes

The upstream person detector is unusable here. BlazePose normally locates its region of interest with a full-body person detector. That detector is trained on images containing most of a body, and on a desk webcam showing only head and shoulders it fired on 3 of 14 frames at confidences around 0.5. A desk camera has fixed geometry, so the region of interest does not need detecting at all: PoseEngine._roi synthesises one that models the sitter's hips below the bottom of the frame. That measured 6 of 6 frames at confidence 1.00 with about 1 percent metric noise, and it dropped a 12MB model from the hot path. The roi block in the config tunes it if the camera is mounted unusually.

OpenCV 5.0 cannot run the pose model on its classic engine. The classic engine rejects the model's NHWC convolutions outright, so the pose model must use the default new engine. This is why the harmless setPreferableTarget Targets are not supported by the new graph engine warning appears in the log on every start.

Privacy

No frame is ever written to disk, encoded, transmitted, or logged. Each sample opens the camera, reads a few frames into memory, computes six numbers from the landmark geometry, and releases the device. Only those numbers reach disk.

The camera is held for well under a second per sample and released between samples, so video calls are never blocked. If another application is using the camera the plugin skips that sample entirely rather than competing for it.

What is stored is still a behavioural record: when you were at your desk, how you were sitting, and for how long. On a shared machine that should not be readable by other local accounts, so ~/.local/state/omarchy/posture is created 0700 and every file in it is written 0600. The service additionally runs with UMask=0077, so nothing it creates can depend on the session umask, and the daemon repairs the permissions of any file written before this policy existed. The unit also runs with ProtectSystem=strict, ProtectHome=read-only and NoNewPrivileges, writing only to its own state directory.

License

MIT, see LICENSE.

The bundled BlazePose model and its decoder are Apache-2.0 from opencv_zoo; see THIRD_PARTY_NOTICES.md.