Omahub
← All plugins
J

Oma2FA

by Jon Kinney

Privately collect recent SMS verification codes and paste one on demand.

Security review

Potentially dangerous behavior detected · 1 finding

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
3379c65
Scanned
3 weeks 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
3379c65
Reviewed
3 weeks ago

Oma2FA is a privacy-focused 2FA code picker that stores only derived one-time codes, never raw SMS bodies. The systemd unit that triggered the deterministic high finding is not installed automatically; it is only created when the user explicitly opts into the Tailscale webhook setup, and it is hardened with user-scope and restrictive sandboxing. No obfuscated code, credential theft, or destructive install-time commands were found.

  • The systemd unit flagged by the scan is a legitimate, user-initiated feature for the optional phone webhook; it is not installed by default and is well-hardened.
  • The webhook exposes an authenticated endpoint on the Tailscale network only when explicitly enabled, which is appropriate for the stated purpose.
  • The plugin copies verification codes to the clipboard with wl-copy --sensitive and expires them after 60 seconds, which is a reasonable privacy safeguard.
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/jondkinney/oma2fa --enable
Productivity #bar #quickshell #security

Oma2FA

Oma2FA is a privacy-first Omarchy picker for short-lived verification codes. It recognizes likely one-time codes in messages received locally, keeps only a minimal expiring record, and copies or pastes a code only after you select it.

This is an alpha. The Omarchy picker, deterministic detector, runtime store, BlueFerry adapter, and authenticated phone webhook are usable now. It is not a replacement for passkeys, security keys, or authenticator apps; prefer those when a service offers them.

Oma2FA selecting a recent verification code beside a bank login

Quick start

This path installs Oma2FA from GitHub and uses an authenticated iPhone Shortcut over Tailscale. BlueFerry users can install the plugin the same way and then skip to BlueFerry for iPhone.

1. Install the plugin

You need a current Omarchy installation and Python 3.12 or newer. In a terminal, run:

omarchy plugin add https://github.com/jondkinney/oma2fa.git --enable

The plugin appears in the right side of the Omarchy bar. This Git-managed installation does not add a keyboard shortcut or start a network listener. Click the Oma2FA bar icon to open it. If the icon does not appear, run:

omarchy-shell shell rescanPlugins
omarchy plugin list

2. Connect the computer and phone with Tailscale

Install Tailscale on both devices, sign in to the same tailnet, and verify that both appear online. The computer must have a working tailscale command and tailscale ip -4 must print its 100.x.y.z address. See the official Tailscale quickstart if either device is not connected.

Oma2FA deliberately does not expose its HTTP listener over ordinary Wi-Fi, Ethernet, port forwarding, or the public internet. The setup button is enabled only when it detects an active Tailscale IPv4 address.

3. Let Oma2FA configure the computer

  1. Open Oma2FA from the bar.
  2. Click the transport summary in the upper-right corner.
  3. Choose Manage phone webhook….
  4. Choose Set up securely with Tailscale.
  5. Wait for the status to read Ready.

That one action creates a private bearer token, writes the webhook environment file, installs the hardened oma2fa-webhook.service user unit, reloads the user service manager, and enables and starts the listener. Everything runs at user scope. The manager can later copy the connection values, disable or re-enable the service, and rotate its token. Its built-in iPhone walkthrough provides a separate Copy action for each value that must be typed or pasted. Token rotation requires confirmation and the phone must be updated afterward.

After the webhook becomes ready, Oma2FA opens the iPhone setup guide automatically after initial provisioning. You can also open it from the connected picker's empty state with iPhone setup guide, or from the connection screen with iPhone setup guide →. Manage phone webhook… always opens the connection controls.

The guide has persistent navigation for Preparation, Create shortcut, Configure request, and Automation & test. Returning from the connection controls restores your reading position. Header and JSON names and values are grouped together; Copy changes to Copied ✓ on the field you copied. Menu choices appear as arrow rows, and the Shortcut Input variable is highlighted separately. Use Enlarge image to inspect screenshots, then collapse them to continue. The final section includes a test message and troubleshooting steps.

4. Create the iPhone Shortcut and automation

Apple's Message automation can filter by sender or text, and Message automations can run without asking. Names can vary slightly between iOS releases. Keep Oma2FA's Phone webhook screen open while you build this: the walkthrough there shows these same screens and gives every value that must be typed or pasted its own Copy button. iOS menu choices remain visible as instructions without unnecessary Copy actions. Copy a field on the computer, then send it to the phone with LocalSend, KDE Connect, or another local transfer tool.

4a. Build Send to Oma2FA

  1. In Shortcuts → Library, tap + and name the new shortcut Send to Oma2FA.
<p align="center"> <img src="assets/shortcut-library.png" width="500" alt="Shortcuts Library showing the plus button and the pink Send to Oma2FA shortcut card"> </p>
  1. Configure its input block as Receive Text from Nowhere. For no input, choose Stop and Respond.
<p align="center"> <img src="assets/shortcut-input.png" width="500" alt="Top of the Send to Oma2FA shortcut showing Receive Text from Nowhere and Stop and Respond"> </p>
  1. Add Get Contents of URL. Paste the value from the walkthrough's URL row directly into that action, expand its options, and select POST. Apple documents the action in its Shortcuts API guide.

  2. Add these headers. The Header 1 · value Copy button supplies the complete Bearer <generated token> value, so there is no prefix to assemble by hand:

    Header Value
    Authorization Bearer <generated token> from Header 1 · value
    Content-Type application/json
  3. Set the request body type to JSON and add exactly:

    Key Value
    sender Fixed text SMS
    body The blue Shortcut Input magic variable—not the literal words
    source The fixed text ios-shortcuts

    sender and body are required. After creating the body row, tap its value and insert the blue Shortcut Input variable. That row is intentionally not copyable because Shortcut Input must be selected as a magic variable rather than pasted as plain text. timestamp and message_id are optional; omit them unless the automation has trustworthy values. The complete action should look like the screenshot below. Its address and credential are deliberately replaced with placeholders; use the values copied from your own Oma2FA installation.

<p align="center"> <img src="assets/shortcut-configuration.png" width="500" alt="Send to Oma2FA shortcut configured to POST JSON with Authorization and Content-Type headers"> </p>
  1. Save the shortcut and confirm that its pink Send to Oma2FA card appears in the Library as shown in step 1.

4b. Attach it to incoming Messages

  1. Open Shortcuts → Automation, tap +, and add a personal automation using the Message trigger.
  2. Set Message Contains to code. Add a Sender filter only if you know every sender that delivers your codes. You can create additional automations for phrases such as verification or one-time if needed.
  3. Select Run Immediately (or disable Ask Before Running, depending on the iOS version), continue, and choose Send to Oma2FA.
  4. Save the automation and confirm that Tailscale remains connected on the phone. The Automation tab should look like this:
<p align="center"> <img src="assets/shortcut-automation.png" width="500" alt="Shortcuts Automation tab showing a Message containing code running Send to Oma2FA"> </p>

All copied setup values use a sensitive desktop clipboard offer that expires after about 60 seconds, so copy a row again if necessary. The raw-token button is retained for troubleshooting, but normal setup should use the complete Header 1 · value row.

5. Verify receipt

Trigger the automation with a test message containing an obvious phrase such as Your verification code is 123456. A generic desktop notification should appear, the bar icon should show a badge, and the code should appear in the picker. The notification intentionally contains no sender, service, message, or code.

On the computer, these commands confirm the listener without printing its token:

systemctl --user status oma2fa-webhook.service
journalctl --user -u oma2fa-webhook.service --since today

How it works

BlueFerry (Bluetooth MAP) ─┐
Phone webhook (VPN) ───────┼─> local detector ─> short-lived runtime store
Manual/test ingestion ─────┘                         │
                                                     ▼
                                  Omarchy service + bar icon + picker
                                                     │
                                      chosen code only: clipboard
                                                     │
                                verified prior window: optional paste

The Omarchy shell keeps Service.qml loaded and starts bin/oma2fa-bridge. The bridge speaks JSON Lines over its private stdin and stdout; it is not a network service. Picker.qml displays the minimal records returned by that bridge. The UI/BlueFerry path needs no separate systemd unit, which avoids two bridge processes competing for the same transport. An independent user unit, provisioned by the setup UI, runs only the standalone phone webhook so messages can arrive while the picker is closed.

Code detection is deterministic and local. It scores 4–8 digit and supported alphanumeric candidates against phrases such as “verification code” and “one-time password,” while rejecting common order numbers, phone numbers, dates, prices, and URLs. It does not send message content to an LLM or remote classifier.

Choosing sources

Every transport except manual entry has an on/off switch. Open the transport summary in the picker's title bar and use the switch on each row, or from a terminal:

oma2fa sources                       # list
oma2fa sources --disable blueferry   # persist a change
oma2fa sources --enable tether

Choices persist in ~/.config/oma2fa/sources.json (mode 0600; only the values you set are written), and a running bridge applies an edit on its next maintenance tick (15 s) without a restart. oma2fa bridge --disable-source NAME and --enable-source NAME pin a source for one run and win over the file; --no-blueferry remains as an alias for --disable-source blueferry. The phone webhook's switch controls its systemd user unit, exactly like the Enable webhook button in its manager, so it answers "Set up the phone webhook first" until the webhook exists.

Defaults: BlueFerry and Blip on (each only does anything when its bridge is installed), Tether and KDE Connect off until explicitly enabled.

Transport status

BlueFerry for iPhone: available now

BlueFerry connects a paired iPhone over Bluetooth Message Access Profile (MAP). Oma2FA's current adapter consumes BlueFerry's /usr/bin/blueferry-quickshell-bridge and blueferry.client locally. A bounded Events1 receive query is the authoritative code path; conversation history is an independent compatibility fallback. Both paths immediately reduce matching messages to a code, service label, source, timestamps, and confidence. Full SMS bodies are not written to Oma2FA's store.

The adapter has been exercised end to end against pristine BlueFerry v0.7.7 and upstream commit dee0b097. Those releases omit receive-only short codes from their conversation projection, but retain them in Events1, where Oma2FA detects them. Local BlueFerry changes that display short codes in BlueFerry's own UI are useful but are not required by Oma2FA. If Events1 is unavailable, Oma2FA reports the transport as degraded instead of claiming it is ready.

BlueFerry is the practical local iPhone experiment today, but it is still experimental. Pair and verify your phone in BlueFerry before troubleshooting Oma2FA. Granting MAP access allows the paired computer to read messages, not just verification codes, while the connection is active.

Blip for iMessage: available now

Blip puts iMessage in the Omarchy bar by polling a Mac you own over ssh. Oma2FA does not poll the Mac a second time: Blip's collector hands each new inbound message to a message_hook= program, and Oma2FA ships that program. Add one line to ~/.config/blip/bridge.conf:

message_hook=/home/you/.config/omarchy/plugins/io.github.jondkinney.oma2fa/bin/oma2fa-blip-hook

(an absolute path — Blip parses the file and expands neither ~ nor $HOME). From the next poll, every new inbound message — every sender, not only Blip's toast allowlist, which is the point — reaches oma2fa blip-hook with the text on stdin and the sender, timestamp, and chat.db row id in BLIP_HOOK_* environment variables. The hook runs the same detector as every other source, stores only the derived record, dedupes on the row id, raises the usual desktop notification when a code is found, and exits quietly otherwise. The picker's Blip row reads Ready once the hook line is present, Hook not configured while it is missing, and Not installed without Blip's bridge.conf. Turning the Blip source off makes the hook a no-op without touching Blip's configuration.

The picker learns about a hooked code on the bridge's next maintenance tick (within 15 s); the notification is immediate. Blip reports the Mac's local time, which the TTL treats as this computer's local time.

Tether: experimental

Tether bridges an iPhone to Linux over Bluetooth MAP and mirrors messages to a local daemon. Oma2FA's adapter subscribes to tetherd's event feed on $XDG_RUNTIME_DIR/tether/tetherd.sock ({"command":"subscribe"}), reduces bt_message and bt_messages events as they arrive, and re-reads threads active inside the code TTL on connect so a code that landed while Oma2FA was down is still collected. The adapter was written against tether's source rather than exercised against a running daemon, so it ships off; enable it with oma2fa sources --enable tether once tether is paired and report what its row says.

iPhone

Apple does not provide Messages for Linux, an iCloud Messages web client, or a public SMS inbox API. Apple's Text Message Forwarding support targets Apple devices, so entering iCloud credentials into an unofficial Linux client is deliberately out of scope.

The authenticated, disabled-by-default webhook is the network alternative for iOS Shortcuts: the phone can filter a received-message automation and send only a candidate to the computer. See the CLI help and webhook section below for the listening address, token, and payload contract.

Android: KDE Connect SMS

Oma2FA can now collect incoming Android SMS through KDE Connect. Open Android setup guide in the picker or its connections menu for the setup steps and an enable/disable button. This connection is off by default and requires no new Python dependency. The desktop needs KDE Connect and systemd's busctl; the plugin does not install packages, start KDE Connect, pair phones, or change phone permissions for you.

  1. Install and open KDE Connect on this computer and your Android phone.
  2. Put both on the same reachable network and pair them, accepting the request on the phone.
  3. Allow KDE Connect's SMS and Phone permission prompts on Android. Enable its SMS/messages plugin on the computer. Notification-reading permission is not needed for this connection.
  4. Enable KDE Connect (Android) in Oma2FA's connections list or guide, or run oma2fa sources --enable kdeconnect.
  5. Send the phone a fresh SMS such as “Your Example verification code is 123456”. Oma2FA should show a generic notification; clicking it opens the picker.

The source reports Not installed, No paired devices, Phone not connected, or SMS unavailable when a setup step is missing. Connected means that a paired reachable device exposes its SMS plugin; a test SMS confirms that Android's permissions and delivery policy allow codes through. All paired, reachable devices with SMS enabled participate (up to 16 devices), and message IDs are scoped to the phone so codes from two phones do not collide.

The adapter subscribes to KDE Connect's direct SMS events on the local session bus. On connection it requests the latest message from each thread—KDE Connect requires this request to start forwarding SMS events—and immediately ignores outgoing, expired, malformed, or non-code messages. This snapshot can include old messages; Oma2FA does not request entire conversations or attachments. It keeps only the derived code record and deduplication hashes. KDE Connect itself may retain message history independently of Oma2FA. Restarting Oma2FA checks the latest cached message from each thread; this is not a guarantee of recovering every message received while Oma2FA was closed.

This covers SMS exposed by KDE Connect, not RCS or codes inside other apps. It avoids Android 15's OTP notification redaction. Android 17 can delay access to some OTP SMS for apps without a qualifying exemption. Pairing alone does not establish that KDE Connect has that exemption on your phone. Oma2FA does not bypass those protections. If a test message never appears, first verify that it appears in KDE Connect's own SMS window and check the app's SMS/Phone permissions and background restrictions.

The wire integration is verified against upstream KDE Connect source and a private D-Bus simulation with the real busctl. A physical Android phone has not yet been tested with this implementation.

Android transport boundaries

SMS is consumed only from conversationCreated and conversationUpdated from KDE Connect's current unique bus owner and the paired device allowlist. Minimal device lifecycle signals trigger rechecking pairing and SMS availability. A monitor-ready handshake completes before requesting SMS events. Device discovery repeats every five seconds, and cache checks run every 30 seconds or when opening the picker. Subprocess replies and individual event frames are capped at 512 KiB before JSON parsing, with a three-second deadline for replies and incomplete frames. The snapshot is capped at 4,096 entries and message bodies at 16,384 characters; oversized or malformed events report a source error without logging their content. Oversized optional snapshots are skipped while live capture continues; a monitor restart does not repeatedly request the same snapshot from the phone. Helpers stop on disable and restart with capped backoff after daemon failures.

The implementation follows KDE Connect's conversation interface, message serialization, and Android SMS subscription behavior.

Authenticated phone webhook

The recommended setup is available in the picker: open the transport summary, choose Manage phone webhook…, then choose Set up securely with Tailscale. Oma2FA detects the computer's active Tailscale IPv4 address, creates a random bearer token, installs and starts the hardened user service, and then offers a field-by-field iPhone walkthrough with privacy-safe reference screenshots. Every value that must be typed or pasted is individually copyable, including the full Bearer <token> header value; iOS menu choices and magic variables are shown without Copy buttons. The token itself never enters QML. The manager can also enable or disable the listener and rotate its token. Rotation requires a second confirmation because the phone must be updated afterward.

The setup UI deliberately supports only the direct Tailscale path. Connect the computer and phone to the same Tailscale network before opening it. Advanced loopback/HTTPS-proxy and other WireGuard configurations remain manual so the UI cannot accidentally assert that an arbitrary network interface is encrypted.

The webhook inside the UI bridge is disabled unless OMA2FA_WEBHOOK_ENABLED=1 is set or the bridge is started with --webhook. The standalone oma2fa webhook command enables only the listener. Its defaults are 127.0.0.1:8765; a phone cannot reach that loopback address. The built-in server is HTTP and rejects non-loopback and wildcard binds unless you explicitly declare an exact VPN address. Never expose it directly over ordinary Ethernet, Wi-Fi, port forwarding, or the public internet: both the reusable bearer token and the message body would be visible to network observers.

There are two supported remote-access patterns:

  • Bind to the computer's exact Tailscale/WireGuard address and set OMA2FA_WEBHOOK_TRANSPORT=vpn. Encryption is then supplied by the VPN.
  • Keep Oma2FA on 127.0.0.1 and put an HTTPS reverse proxy on the same computer in front of it. The phone must use the proxy's https:// URL.

The vpn setting is an explicit security assertion, not VPN detection. Set it only when the bind address belongs exclusively to an active encrypted tunnel.

Configuration is file/environment-based so the secret never appears in process arguments or QML state. The setup UI manages these files:

  • $XDG_CONFIG_HOME/oma2fa/webhook.env (normally ~/.config/oma2fa/webhook.env) contains only the bind, port, transport, and absolute token-file path.
  • $XDG_CONFIG_HOME/oma2fa/webhook-token is the mode-0600 bearer-token file.
  • $XDG_CONFIG_HOME/systemd/user/oma2fa-webhook.service is the hardened user unit copied from the plugin.

The underlying variables are:

Variable Meaning
OMA2FA_WEBHOOK_ENABLED Set to 1 to enable the listener in bridge mode.
OMA2FA_WEBHOOK_BIND Listen address; defaults to 127.0.0.1.
OMA2FA_WEBHOOK_PORT Listen port; defaults to 8765.
OMA2FA_WEBHOOK_TRANSPORT Must be vpn for a non-loopback bind; omit for loopback/TLS-proxy mode.
OMA2FA_WEBHOOK_TOKEN_FILE Preferred path to a mode-0600 bearer-token file.
OMA2FA_WEBHOOK_TOKEN Direct token fallback; avoid persistent environment files containing it.

Generate a token without printing it:

install -d -m 0700 ~/.config/oma2fa
(umask 077; openssl rand -hex 32 > ~/.config/oma2fa/webhook-token)

For an interactive foreground listener on the loopback default:

OMA2FA_WEBHOOK_TOKEN_FILE="$HOME/.config/oma2fa/webhook-token" \
  ./bin/oma2fa webhook

The only accepted endpoint is POST /v1/ingest, with Authorization: Bearer <token> and Content-Type: application/json:

{
  "sender": "Example",
  "body": "Your verification code is 123456",
  "source": "ios-shortcuts",
  "timestamp": "2026-08-21T12:00:00Z",
  "message_id": "phone-generated-stable-id"
}

sender and body are required; source, timestamp, and message_id are optional. Omit timestamp unless the automation supplies the message's actual receive time. Requests are capped at 16 KiB, unauthenticated requests and other methods/paths are rejected, and request bodies are not logged. Records appear as webhook/<source> in the picker.

For advanced manual setup, the repository includes the same hardened user unit installed by the UI. It runs the standalone webhook process, not another UI bridge. First create ~/.config/oma2fa/webhook.env with an absolute token path and your chosen bind address:

OMA2FA_WEBHOOK_BIND=127.0.0.1
OMA2FA_WEBHOOK_PORT=8765
OMA2FA_WEBHOOK_TOKEN_FILE=/home/your-user/.config/oma2fa/webhook-token

Keep that file private (chmod 0600 ~/.config/oma2fa/webhook.env). In an iOS Shortcut or trusted Android automation, configure a JSON POST to either http://<vpn-address>:8765/v1/ingest over the VPN or the reverse proxy's https://<hostname>/v1/ingest URL. Add the bearer token as the Authorization header. Prefer extracting or pre-filtering on the phone when the automation system permits it.

For direct VPN access, use an exact address rather than 0.0.0.0, ::, or a hostname:

OMA2FA_WEBHOOK_BIND=100.64.0.10
OMA2FA_WEBHOOK_TRANSPORT=vpn
OMA2FA_WEBHOOK_PORT=8765
OMA2FA_WEBHOOK_TOKEN_FILE=/home/your-user/.config/oma2fa/webhook-token

Then install and enable the unit explicitly:

install -Dm0644 systemd/oma2fa-webhook.service \
  ~/.config/systemd/user/oma2fa-webhook.service
systemctl --user daemon-reload
systemctl --user enable --now oma2fa-webhook.service

Check it with systemctl --user status oma2fa-webhook.service. The UI bridge and standalone webhook share the owner-only runtime store; the picker refreshes that store whenever it opens. The standalone listener also publishes a minimal owner-only heartbeat so the picker can report it as an active transport. That heartbeat contains only a format version, timestamp, and random process-instance identifier; it contains no bind address, token, sender, message body, or code. Hover the connections button in the picker to preview each transport's derived health, or click it to keep the details open and enter the webhook manager. Arbitrary backend detail is never rendered in that disclosure. Token-copying happens entirely in the Python backend through an expiring sensitive clipboard offer; the token is never returned across the bridge.

Requirements

  • A current Omarchy installation with omarchy plugin commands.
  • Python 3.12 or newer.
  • jq, hyprctl, wl-copy, wtype, and coreutils timeout (normally supplied by Omarchy).
  • Tailscale on the computer and phone when using the guided phone-webhook setup. The computer's tailscale ip -4 command must report an active address.
  • BlueFerry v0.7.7 or newer and a paired iPhone for automatic local SMS ingestion. The backend package must provide both /usr/bin/blueferry-quickshell-bridge and the blueferry.client Python module. Alternatively, use a trusted phone automation with the authenticated webhook.

The Python core uses the standard library. You do not need a virtual environment for the copied plugin.

Install

Review the source first: Omarchy plugins execute unsandboxed inside the long-running shell process.

Marketplace/Git installation

Install and enable the public repository with Omarchy's native lifecycle:

omarchy plugin add https://github.com/jondkinney/oma2fa.git --enable

This creates a Git-managed checkout at ~/.config/omarchy/plugins/io.github.jondkinney.oma2fa and enables the bar widget. It does not modify Hyprland keybindings; click the bar icon or use the manual toggle command below. Omarchy manages updates and removal for this path.

Development checkout with optional hotkey

From a reviewed development checkout, run:

./scripts/install.sh

For non-interactive use:

./scripts/install.sh --yes

The installer:

  1. stages a copy under ~/.config/omarchy/plugins/;
  2. refuses symlinks and validates the staged plugin with omarchy plugin validate;
  3. atomically installs it as io.github.jondkinney.oma2fa, enables it with the official Omarchy command, and places its widget in the right bar section; and
  4. checks the live keybinding list before adding a clearly marked SUPER+ALT+V block to ~/.config/hypr/bindings.lua.

Existing bindings are never overridden. If SUPER+ALT+V is occupied, the plugin is still installed and the installer prints the manual open command. Every binding edit is backed up, reloaded, and checked with hyprctl configerrors. Symlink-managed binding files are deliberately not edited; use --no-bind and add the shown binding to the symlink target yourself.

The install is a real copy, not a symlink, because Omarchy's plugin validator rejects symlinks. Re-run the installer after changing a development checkout; it preserves the previous managed copy in a hidden, timestamped backup. Reinstalling leaves an existing widget wherever you moved it. Upgrading from the original hotkey-only release performs a one-time migration from its old service entry to the new bar entry.

Use

Click the Oma2FA bar icon or press Super+Alt+V. The badge is only a count of available codes; the bar never displays a code or message content. Search is active when the picker opens, while the newest matching code remains the default Enter action. When a new code is accepted, Oma2FA also shows a generic desktop notification; it contains no code, sender, service, or message text. Click the notification to open the Oma2FA picker.

The empty picker stays compact and shows Ready for your next code when a connection is active. Without an active connection, Set up connections opens the connection choices. The window grows as codes arrive, up to a fixed maximum; code action hints appear only when there are matching codes. The connections button shows the active count, and connections keep their fixed list order when toggled.

  • Type to filter by service, source, or code.
  • Press Down to enter the results at the newest code, or Up to enter at the oldest; then use Up/Down, Page Up/Page Down, Home, or End to browse. Pressing Up from the newest result returns to the search input.
  • Press Enter to copy and request a paste into the window that was focused before the picker opened.
  • Press Shift+Enter to copy only.
  • Press Delete to remove the selected record.
  • Press Escape to close without touching the clipboard.

A successful copy consumes that record so it cannot be selected twice. The sensitive clipboard offer expires after about 60 seconds.

The paste path captures the active Hyprland window before opening the overlay. After selection, it closes the overlay and verifies that the same window is active before typing. If focus cannot be verified, it fails closed: the code remains on the clipboard, but Oma2FA does not type it.

You can always open the picker without a keybinding:

omarchy-shell shell toggle io.github.jondkinney.oma2fa '{}'

Update

Git-managed installations can be updated with:

omarchy plugin update io.github.jondkinney.oma2fa --yes

The Omarchy shell reloads the plugin code. If the standalone phone webhook is configured, restart that long-running process so it uses the updated Python code:

systemctl --user restart oma2fa-webhook.service

CLI and manual ingestion

The checkout-local launcher resolves its own directory, so it works from any current working directory:

./bin/oma2fa --help
./bin/oma2fa status
./bin/oma2fa list
printf '%s\n' 'Example verification code is 123456' | \
  ./bin/oma2fa ingest --sender Example --source manual --message-id docs-example-1

ingest deliberately accepts the body only on stdin, keeping a real OTP out of shell history and process arguments. It also accepts --timestamp as ISO time or epoch seconds. Global --runtime-dir PATH, when needed for an isolated test, must appear before the subcommand. The CLI's help remains the authoritative source for flags while the project is alpha.

The UI-facing bin/oma2fa-bridge is not intended for interactive use. Its normal channel contains derived records and allowlisted webhook status, not original BlueFerry message bodies or the webhook bearer token. Token-copy requests are completed inside the backend and return only success or failure.

Security and privacy model

  • Processing happens under your Linux user account and does not call a cloud classification service.
  • Original message bodies are processed in memory and discarded. The runtime store contains derived records only.
  • Records live below $XDG_RUNTIME_DIR/oma2fa (or a per-user runtime fallback), with mode-0700 directory and mode-0600 files. Codes expire after ten minutes by default.
  • A code enters the clipboard only after explicit selection. Oma2FA uses the Wayland sensitive-data hint and limits clipboard lifetime where supported.
  • Automatic paste is conditional on matching the window captured before the picker opened. Copy-only remains available when that check is unavailable.
  • Codes and message bodies must never be written to application logs, notifications, command-line arguments, or analytics. New-code notifications are intentionally generic.

These controls reduce exposure; they cannot make the desktop clipboard a secret enclave. Other processes running as your user may be able to observe clipboard contents or inspect process memory. BlueFerry/MAP also grants the computer broader message access before Oma2FA performs its filtering.

Troubleshooting

The Tailscale setup button is disabled

Confirm the Linux client is installed, authenticated, and online:

tailscale status
tailscale ip -4

The second command must print a 100.x.y.z address. Also confirm the phone is online in the same tailnet. Tailscale's device-connectivity guide covers ACL and reachability problems.

The picker says the webhook is disabled or not responding

Open Manage phone webhook… and use Enable webhook, then inspect the unit if it does not become ready:

systemctl --user status oma2fa-webhook.service
journalctl --user -u oma2fa-webhook.service -n 100 --no-pager

The service runs as the current user. Its configuration is normally at ~/.config/oma2fa/webhook.env; its token is in the separate owner-only ~/.config/oma2fa/webhook-token file. Do not paste either file into an issue.

The Shortcut reports unauthorized

Use Header 1 · value → Copy in the walkthrough again and ensure the header is exactly Authorization: Bearer <token>, including the space after Bearer. If the token was rotated, every phone automation using the old value must be updated.

The service is ready but no code appears

  • Verify the Shortcut ran and that its Message trigger filters match.
  • Confirm body receives the incoming message rather than a literal label.
  • Use a clear test phrase with a 4–8 character candidate, such as Your verification code is 123456.
  • Check that the phone's Tailscale connection is active when the message arrives.
  • Remember that duplicates are intentionally ignored and codes expire after ten minutes.

BlueFerry is disconnected or degraded

Pair the iPhone in BlueFerry first and verify BlueFerry can receive current messages. Oma2FA requires both /usr/bin/blueferry-quickshell-bridge and the blueferry.client Python module. A degraded state means the receive-event path is unavailable, even if conversation history can still be read.

The bar widget or notification does not appear

omarchy-shell shell rescanPlugins
omarchy plugin list
omarchy restart shell

The bar badge and desktop toast are intentionally generic; open the picker to see the derived record. If the plugin is enabled but absent from the bar, use Omarchy's bar settings to place the Oma2FA widget in a visible section.

Development and tests

Run the core tests and validate the Omarchy manifest from the repository root:

python -m unittest discover -s tests -v
omarchy plugin validate .
bash -n bin/oma2fa bin/oma2fa-bridge scripts/install.sh scripts/uninstall.sh \
  scripts/test-install.sh scripts/test-marketplace-install.sh \
  scripts/test-bar-widget.sh \
  scripts/test-qml-bar-widget.sh scripts/test-qml-picker-status.sh \
  scripts/test-qml-picker-shortcuts.sh
./scripts/test-bar-widget.sh
./scripts/test-qml-bar-widget.sh
./scripts/test-qml-picker-status.sh
./scripts/test-qml-picker-shortcuts.sh
./scripts/test-install.sh
./scripts/test-marketplace-install.sh

Optional static checks configured by pyproject.toml:

ruff check .
mypy oma2fa

To use a different interpreter without installing the package, set OMA2FA_PYTHON to one executable path:

OMA2FA_PYTHON=/path/to/python ./bin/oma2fa --help

Uninstall

If you installed the optional webhook unit, stop and remove it first:

systemctl --user disable --now oma2fa-webhook.service
rm ~/.config/systemd/user/oma2fa-webhook.service
systemctl --user daemon-reload

The private environment and token files are intentionally preserved so an accidental plugin removal does not silently destroy credentials. After the service is stopped, remove them too if you want a complete cleanup:

rm ~/.config/oma2fa/webhook.env ~/.config/oma2fa/webhook-token
rmdir ~/.config/oma2fa 2>/dev/null || true

For a marketplace/Git installation, use Omarchy's native removal command:

omarchy plugin remove io.github.jondkinney.oma2fa --yes

For a copy installed by scripts/install.sh, use its matching uninstaller:

./scripts/uninstall.sh

Use --yes for non-interactive confirmation. --keep-plugin or --keep-binding can preserve one part. The uninstaller removes only the exact binding marker block and a plugin directory carrying Oma2FA's installer ownership marker. It refuses an unmarked directory at the same path. Omarchy's official removal command deletes a Git-managed checkout. For a non-Git copy, the custom uninstaller preserves the plugin as a hidden backup, and the Hyprland binding file receives its own timestamped backup.

License

MIT