Omahub
← All plugins
H

Self Control

by harisb2012

Block distracting sites for a fixed stretch. Arming is one click, ending it early is deliberately not.

Security review

Potentially dangerous behavior detected · 30 findings

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
6df9fa4
Scanned
1 month ago
  • Bundles a systemd unit file.

    [Unit]
  • high persistence tests/setup.test.sh:233

    Registers scheduled or boot-time system tasks.

    systemctl enable --now' "$by_hand/sudo.log")" 1
  • high persistence setup:142

    Registers scheduled or boot-time system tasks.

    systemctl enable --now "$unit"
  • high persistence uninstall:197

    Registers scheduled or boot-time system tasks.

    systemctl disable "$unit"
  • high persistence uninstall:184

    Bundles a systemd unit file.

    [Unit]
  • medium eval setup:115

    Shell sources dynamically generated content.

    source <(printf '%s\n' "$state") 2>/dev/null || return 0
  • medium eval uninstall:116

    Shell sources dynamically generated content.

    source <(printf '%s\n' "$state") 2>/dev/null \
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo or pkexec: `unshare --user --map-root-user` supplies the root uid
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo on the hand install path below, so the branch a
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo that decided to prompt would
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo is still the way to root
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and not otherwise" \
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo or pkexec: `unshare --user --map-root-user` supplies the root
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo on the hand run below, so the branch a person
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo is still the way to root
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and not otherwise" \
  • medium sudo setup:35

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

    sudo to ask on,
  • medium sudo setup:36

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

    sudo only by hand.
  • medium sudo uninstall:55

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

    sudo to ask on,
  • medium sudo uninstall:56

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

    sudo only by hand.
  • Registers scheduled or boot-time system tasks.

    systemctl disable`. Removes the `multi-user.target.wants` symlink and does
  • Registers scheduled or boot-time system tasks.

    systemctl disable --now`. The `--now` is a stop, and the stop is refused.
  • Bundles a systemd unit file.

    [Unit]
  • Bundles a systemd unit file.

    [Unit]
  • Command runs with sudo, elevating the process beyond the plugin environment.

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

    sudo pfctl -d`. Its own FAQ
  • Docs sudo README.md:30

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

    sudo systemctl stop` and
  • Docs sudo README.md:31

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

    sudo nft flush ruleset` both end a block, and no arrangement of systemd and
  • Docs sudo README.md:33

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

    sudo pfctl -d`, and it works anyway because the product is cost rather than
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo or install hooks. The daemon arrives

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

This is a transparent self-control plugin: the privileged setup, systemd service, nftables rules, and /etc/hosts changes are the advertised function and require explicit user consent through pkexec. The deterministic scan's high/persistence and sudo findings mostly reflect intended behavior and documentation, not hidden or malicious code. No obfuscation, credential theft, or destructive install actions were found.

  • Installs a root systemd service and modifies /etc/hosts and nftables; this is intended and clearly documented, but it is a significant privileged change that users should knowingly consent to.
  • The `source <(printf '%s\n' "$state")` pattern is flagged as dynamic eval, but it reads a root-owned state file written by the daemon, so it is not an unprivileged injection path.
  • The systemd unit uses RefuseManualStop=yes and Restart=always, making the daemon deliberately hard to stop; the uninstall script handles removal, and this is the product's stated purpose.
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/harisb2012/omarchy-selfcontrol --enable
Productivity #bar #quickshell #system

Self Control for Omarchy

Block distracting sites for a fixed stretch. Arming is one click. Ending it early is deliberately not.

A Linux port of SelfControl, the macOS app by Charlie Stigler and Steve Lambert, rebuilt as an Omarchy plugin plus the root daemon a plugin is not allowed to be. None of its code is reused: macOS enforces with a launchd daemon and pf, this enforces with a systemd service, nftables and /etc/hosts. What is carried over is the idea and the discipline, including the part everyone asks about, which is that there is no way to end a block early.

This project is not affiliated with or endorsed by the SelfControl authors.

What it looks like

Idle Running
The panel with no block running The panel counting down a running block

Pick a duration, edit the blocklist, press start. While a block runs there is a countdown, sites can still be added, and there is no control anywhere that ends it early.

The bar widget while a block is running

What this actually promises

Friction, not a boundary. You are in wheel, so sudo systemctl stop and sudo nft flush ruleset both end a block, and no arrangement of systemd and nftables changes that. SelfControl is the same on macOS, where an admin can sudo pfctl -d, and it works anyway because the product is cost rather than enforcement.

So the design target is seconds of deliberate sabotage:

  • no stop path in the widget, and none in the CLI's happy path
  • RefuseManualStop=yes, so the reflexive systemctl stop bounces
  • polkit actions for arm and list-set only, and none at all for disarm

See knowledge/this-is-friction-not-a-boundary.md for the full threat model, including the one variant that is a real boundary.

Shape

Two halves, and the split is forced rather than chosen. omarchy plugin add clones files and never runs sudo, and plugins run unsandboxed inside the shared omarchy-shell Quickshell process with your permissions. Anything privileged has to live outside that process and arrive through a separate setup step.

BarWidget.qml     display only: reads a status file, shells out to arm
Model.js          pure JS, no QML imports, so a plain JS harness can test it
daemon/daemon     the privileged half: arm, enforce, status, disarm
daemon/lib/       deadline math and ruleset generation, both pure and tested
daemon/units/     the systemd service
daemon/polkit/    one action per privileged verb, and no action for disarm
lists/            blocklists, one domain per line
knowledge/        measured platform facts, one per file
tests/            everything checkable without arming a real block

The widget never touches nftables and holds no privilege. The daemon publishes one line of JSON to /run/omarchy-selfcontrol/status.json on every state change, and the widget reads that and nothing else.

How a block holds

Two layers, because either alone is defeated. /etc/hosts entries between markers handle the system resolver. An nftables table handles the addresses directly, and rejects DoT on 853 plus 443 to known DoH resolvers, because Chromium's secure-DNS auto-upgrade otherwise makes the hosts entry invisible mid-session. See knowledge/hosts-alone-loses-to-doh.md.

Two clocks, because either alone is cheatable. A block stores a CLOCK_REALTIME deadline and a CLOCK_BOOTTIME deadline. Remaining time is the maximum of the two, so winding the clock in either direction cannot end a block. The one hole it does not close is documented and pinned by a test: knowledge/reboot-with-a-wound-clock-ends-a-block.md.

Reassertion. The daemon reapplies the ruleset every 5 seconds, so a manual nft flush buys 5 seconds. Blocked domains are re-resolved every 15 minutes.

The IP layer is blunt, and that is inherent. Resolving reddit.com yields Fastly anycast addresses, x.com yields Cloudflare ones, and those ranges are shared with an enormous number of unrelated sites. Blocking them can take other sites hosted on the same edge with them. This is why DNS is the primary layer and addresses are only the backstop, and it is not a bug that can be fixed without a proxy that reads SNI. SelfControl has the same limitation.

Install

omarchy plugin add https://github.com/harisb2012/omarchy-selfcontrol.git --enable

Then open the widget. The daemon is not there yet, so the panel says so and offers an Install button, which runs setup through polkit and waits for the service to publish its first status. That is the whole install. It asks for your password, and it asks on purpose, because it is putting a root service and a polkit policy on the machine.

If you would rather read that script before running it, the panel also prints the path to setup with a Copy button beside it. Running it yourself in a terminal does the same work.

Updating runs omarchy plugin add again, which refreshes the plugin half only, so the daemon stays on the old code until setup runs again. The panel notices when the two halves report different versions and offers the same button under the label Install the update. It offers it only while idle, and refuses to pretend it can fix the skew during a block: setup will not replace a daemon that is enforcing one. See knowledge/plugin-updates-without-the-daemon.md.

Starting a block does not prompt for a password; changing a blocklist does, through Omarchy's own polkit dialog.

Uninstall

Removal has no button, and it is two steps in this order, because the daemon outlives the plugin directory that carries the script which removes it.

~/.config/omarchy/plugins/io.github.harisb2012.selfcontrol/uninstall
omarchy plugin remove io.github.harisb2012.selfcontrol

uninstall takes down the service, the polkit actions and the nftables table, and puts /etc/hosts back. It refuses while a block is armed, since an uninstall mid-block would be the cheapest bypass in the tool. Your blocklists survive in /var/lib/omarchy-selfcontrol/lists; add --purge to remove those too.

Running omarchy plugin remove on its own deletes the plugin files and leaves the root service running with no UI left to reach it, so run uninstall first.

Tests

tests/run

Nothing in there needs root and nothing touches the host firewall.

Step What it covers
omarchy plugin validate . the manifest checks the shell itself enforces
qmllint -I $OMARCHY_PATH/shell QML static analysis against the real shell modules
systemd-analyze verify unit keys, including which section they belong in
deno run tests/model.test.js display logic, evaluated out of Model.js
tests/deadline.test.sh clock tampering, and the list name that reaches root
tests/ruleset.test.sh the generated ruleset, applied for real in a private netns

The last one is the important one. unshare --user --map-root-user --net gives an unprivileged process real nftables in a throwaway namespace, so the ruleset is applied and queried with nft get element rather than checked with grep. It caught a syntax error on the empty-blocklist path within minutes of existing. See knowledge/netns-gives-unprivileged-nftables.md.

Not built yet

  • Wildcard subdomains, which /etc/hosts cannot express and dnsmasq can.
  • An allowlist mode, which is the only shape that survives a VPN.
  • The opt-in hard mode that drops the user from wheel for the duration.