Skip to content

Desktop Lite (macOS)

Desktop Lite is a macOS menu bar companion for the ov agent daemon. It shows whether the agent is running and unlocked, unlocks or locks it from the menu bar, and displays the daemon’s recent activity. A second window, the Vault Workbench, browses and manages apps, environments and secrets — by name and metadata, with no secret value in view.

It has no Dock icon by design. Look for it in the menu bar.


  • Session state. The tray icon, its tooltip and the panel show whether the daemon is unlocked, locked or not running — polled live from the daemon.
  • Unlock and lock. Unlocking opens a native macOS password dialog and starts ov agent start --foreground on a pseudo-terminal, delivering the password the way a terminal would. The app holds no KEK, no DEK and no socket key, and performs no vault cryptography — ov does. Lock Now in the tray menu stops the daemon (ov agent stop), which discards its keyring; the panel then shows it as not running.
  • Recent activity. The daemon’s activity feed (ov agent activity) rendered in the panel.
  • Vault Workbench. Open it from the tray menu. Browse apps → environments → secrets, see an environment-by-environment matrix of which secrets exist where, create apps and environments, and create, update, roll back and delete secrets with version history. Names, versions and timestamps only.

Secret values travel in one direction. When you create or update a secret, the value goes from the form field over stdin into ov — not on the command line, not into a log — and it does not come back: no Workbench read path returns a value, because the Rust types that carry rows have no field to carry one, pinned by a test that fails to compile the moment one of them grows a value-bearing field.


  • macOS 13 or newer. The build is universal — Apple Silicon and Intel.
  • ov 0.19.0 or newer, from a signed CLI release (installation). At startup the app runs ov --check-version-bounds against its floor and stops at an upgrade-required screen with an older binary.
  • Where the app looks for ov. Desktop Lite resolves ov from one absolute path — /usr/local/bin/ov by default, which is where the curl | sh installer puts it when it can write there. It does not search $PATH. If your ov lives elsewhere (Homebrew on Apple Silicon installs to /opt/homebrew/bin/ov; the installer falls back to ~/.local/bin/ov), point the app at it and relaunch:
Terminal window
D="$HOME/Library/Application Support/com.opaquevault.desktop-lite"
mkdir -p "$D"
(umask 077; printf '{"ov_path": "/opt/homebrew/bin/ov", "machine_key_path": null}\n' > "$D/settings.json")

If the path is not absolute, contains .., or the file does not parse, the app ignores it and falls back to /usr/local/bin/ov.

Both install paths deliver a signed ov: the curl | sh installer and the Homebrew tap (brew install opaquevault-brew/tap/ov — note the tap; a bare brew install ov is an unrelated terminal pager, which the app refuses with a signature error) install the same Developer ID-signed binary from the release, which is what the app’s signature check expects (see Security posture). To check a binary the way the app will:

Terminal window
codesign -dv --verbose=2 /opt/homebrew/bin/ov 2>&1 | grep -E '^(Identifier|TeamIdentifier)='

Expected: Identifier=com.opaquevault.ov and TeamIdentifier=UQZQ86Z9L9.

Why the floor is a security floor. Desktop Lite opens no sockets of its own. Anything that reaches the API leaves through the ov it spawns — so its transport posture (TLS 1.3, the X25519MLKEM768 hybrid key exchange, the rest of the security model) is whatever that ov’s posture is, and its version pin is that posture. When a CLI release raises the floor for a security reason, the desktop floor follows. 0.19.0 is the first release whose Workbench subcommands accept the typed-target confirmation the daemon enforces on destructive operations.


Three steps, in this order: download, verify, then unpack. Nothing runs until the signature has checked out.

Resolve the current stable tag, then fetch the app together with its checksums and signature from the versioned URLs:

Terminal window
TAG=$(curl -fsS https://get.opaquevault.com/desktop-latest) # e.g. desktop-v0.1.2
VERSION=${TAG#desktop-v}
curl -fsSLO "https://releases.opaquevault.com/${TAG}/OpaqueVault-Desktop-Lite_${VERSION}_universal.zip"
curl -fsSLO "https://releases.opaquevault.com/${TAG}/desktop_${VERSION}_checksums.txt"
curl -fsSLO "https://releases.opaquevault.com/${TAG}/desktop_${VERSION}_checksums.txt.minisig"

Desktop releases are signed with the same minisign key as the CLI — see Release signing for the trust model (brew install minisign if you don’t have it):

Terminal window
shasum -a 256 -c "desktop_${VERSION}_checksums.txt"
minisign -Vm "desktop_${VERSION}_checksums.txt" \
-P 'RWQLHCx3CKub+D3Wnc1zX/YBVr1fJD5SrK08d2xp4XoTQipbFET8V0fU'

Expected:

OpaqueVault-Desktop-Lite_0.1.2_universal.zip: OK
Signature and comment signature verified
Trusted comment: opaquevault desktop-v0.1.2

Read the trusted comment: the tag after opaquevault must equal the $TAG you resolved. A valid signature over a different tag means another release’s files are being served under this URL. A signature proves origin, not currency — https://get.opaquevault.com/desktop-latest is the source of truth for which tag is current.

The key in the command above (F89BAB08772C1C0B) must match /.well-known/ov-release.pub on this site and the copy embedded in the install script served by get.opaquevault.com, a separate origin:

Terminal window
curl -fsSL https://get.opaquevault.com | grep RELEASE_PUBKEY

If any of the three differ, stop and email [email protected].

Terminal window
ditto -x -k "OpaqueVault-Desktop-Lite_${VERSION}_universal.zip" .
mv "OpaqueVault Desktop Lite.app" /Applications/
codesign -dv --verbose=2 "/Applications/OpaqueVault Desktop Lite.app" 2>&1 | grep -E '^(Identifier|TeamIdentifier)='
spctl --assess --type execute -vv "/Applications/OpaqueVault Desktop Lite.app"
open "/Applications/OpaqueVault Desktop Lite.app"

Expected from codesign: Identifier=com.opaquevault.desktop-lite and TeamIdentifier=UQZQ86Z9L9. Expected from spctl: accepted, source=Notarized Developer ID, and an origin= line ending in (UQZQ86Z9L9). spctl proves the app is notarized; the identifier and Team ID lines are what pin it to OpaqueVault rather than to any notarized developer.

The app is Developer ID signed, notarized by Apple and stapled, so Gatekeeper opens it without a right-click override. If an older copy is already in /Applications, remove it first — mv will not replace a directory.


Desktop Lite adds no security invariant of its own; it inherits the CLI’s. What it adds is a short list of rules for how it reaches ov, each pinned by a test in the tree.

No $PATH lookup, ever. ov is resolved from the one absolute path in settings.json (see Requirements). $PATH resolution would let anything that can prepend a directory to the app’s environment substitute its own ov — the process that receives your master password — so the spawner refuses a relative path outright and never consults $PATH.

Signed ov only. Before spawning, the app verifies the binary against a code requirement pinned to OpaqueVault’s Apple Team ID and to the identifier com.opaquevault.ov. A locally built or ad-hoc-signed ov is refused. Support for unsigned binaries is compiled out of release builds, and the release workflow fails the build if it finds that support compiled in.

No keys, no vault crypto, no sockets of its own. The app holds no KEK, no DEK and no socket key, performs no vault cryptography, and its Rust core opens no sockets — anything that reaches the API leaves through the ov it spawns. The master password, the pseudo-terminal and the ov spawn path live in that Rust core; the panel’s web UI and its npm dependencies have no path to the master password. The one secret-bearing thing the web UI does handle is the value you type into the Workbench’s create or update form, on its way in — a compromised web UI could read that as you type it; it has nothing to read back out. The web view is confined by its content-security policy to the bundled assets; a navigation guard on top of that is tracked as a follow-up (OV-502).

Destructive operations need the typed target — and the daemon checks it. Deleting an app, environment or secret, or rolling a secret back, opens a native macOS confirmation modal (not a web view) that asks you to type the exact app slug, environment slug or secret name; for a rollback the CLI composes name@version from the name you typed and the version you chose, and that composed target is what the daemon checks. The typed target travels to the daemon with the request, and the daemon refuses the operation when it is absent or does not match byte-for-byte — so a compromised web view inside the app has no way to confirm on your behalf. This is mistake-proofing and defense-in-depth on top of the vault’s key-scoping floor, not the authorization boundary itself (ADR-024 in the architecture decision record).

Polling does not extend the session. The status and activity polls behind the panel are keepalive-safe on the daemon side — pinned there, not in the app; the app’s polling cannot keep a session unlocked past its expiry.


  • OV-464 — the Workbench window floats above other applications, and its green button does not enter native macOS fullscreen. Cosmetic, present since desktop-v0.1.0; key-status transfer and Workbench behaviour are unaffected.

Desktop Lite ships from its own desktop-v* tags, independent of the CLI’s v* line: a desktop release says nothing about the CLI version and vice versa. The one coupling is the ov floor above (0.19.0 for desktop-v0.1.2). get.opaquevault.com with no path remains the CLI installer; the app lives at /desktop, and /desktop-latest prints the current stable tag.

There is no in-app update check — an update notice would need the app to make a network request, which is exactly what its posture rules out. To see whether you are current, compare the stable tag against the version you have installed:

Terminal window
curl -fsS https://get.opaquevault.com/desktop-latest
defaults read "/Applications/OpaqueVault Desktop Lite.app/Contents/Info.plist" CFBundleShortVersionString