curtain/.github/wiki/Installation.md
Aric Camarata 8c19e960d2 Detection root-cause fix + audit batch: netstat path, UDP activator, settings coherence, refactor, docs
Detection: netstat lives at /usr/sbin/netstat, not /usr/bin — the hardcoded wrong
path silently killed the ESTABLISHED-TCP activator (root cause of the failed live
test). Fixed and live-verified. Added peered-UDP activator (5900-5902) for
High-Performance sessions, per-signal transition logging, unconditional error
logging for dead probe helpers, and probe v2 with full CGSession dictionary
diffing. 7 new parser tests (32 total).

Fixes from a full audit + adversarial review: idle source setting honored
(default now Remote session activity), cover scope reduced to a coherent
two-mode model with legacy migration (per-display toggle was inverted in
onlyMarked and dead in all), curtain test no longer schedules a teardown over a
live session, specific-display password box placement gets a real picker,
refuse-to-arm enforced, activation notification posts a real banner, menu
password gate bypassed when the event tap is dead, shared single-decoder aerial
player with stale-task guard and async playability check, password buffer zeroed
on successful unlock and Esc, XPC interruption/invalidation handlers, modern
Accessibility settings URL, launchPath modernized, codesign failures now abort
release.sh, monotonic CFBundleVersion, install.sh temp cleanup, dead
armDisarmHotkey setting removed.

Refactor: Curtain.swift and PreferencesWindow.swift split into focused files
(largest now 479 lines). Wiki, README, and contributing docs updated to match.
Build clean at 0 warnings, 32/32 tests pass.
2026-06-09 20:36:30 -04:00

4 KiB

Installation

Prerequisites

  • macOS 13 (Ventura) or later. Apple Silicon recommended.
  • Screen Sharing enabled: System Settings → General → Sharing → Screen Sharing.

Install

  1. Download Curtain-1.0.0.dmg from the GitHub Releases page.
  2. Open the DMG.
  3. Drag Curtain.app into the Applications folder.
  4. Launch Curtain from /Applications.

On first launch an onboarding flow walks you through setup: Welcome → grant Accessibility → optional disconnect helper → optional password → finish. When it completes, the curtains icon appears in the menu bar.

First launch: Gatekeeper

Curtain is currently ad-hoc signed, not yet notarized. On a clean download macOS Gatekeeper blocks the first launch and reports the app is damaged or from an unidentified developer. This is expected for now.

Clear the quarantine flag once, then open the app normally:

xattr -dr com.apple.quarantine /Applications/Curtain.app

Then double-click Curtain.app. Right-clicking and choosing Open is no longer enough on recent macOS, so use the command above. Once a notarized build ships, this step goes away and Curtain opens straight from the DMG.

Grant Accessibility

Curtain needs Accessibility permission to block the desk keyboard and mouse. Without it, Curtain refuses to show the cover at all and posts a notification instead. This prevents putting up a screen that cannot be unlocked. The emergency hotkey Control + Option + Command + U always works regardless.

The onboarding flow deep-links you straight to the right pane. You can also open it yourself:

  1. Open System Settings → Privacy & Security → Accessibility.
  2. Find Curtain in the list and turn it on.
  3. Relaunch Curtain so the new permission takes effect.

If Curtain does not appear in the Accessibility list, launch it once from /Applications, then check again.

After every rebuild of a local ad-hoc build, re-grant Accessibility. Rebuilding produces a new code signature and macOS does not carry over the old grant automatically.

Open at login

Curtain manages login startup itself with SMAppService. Turn on Open at login in the settings window. macOS tracks this under System Settings → General → Login Items, where you can also toggle it off. There is no LaunchAgent and no plist to manage by hand.

Set a password

Open the settings window (click the menu-bar icon) and type a password in the Security section. This is what someone at the desk types to get past the curtain.

If you never set a password, the default is curtain. The password is stored as a salted PBKDF2-HMAC-SHA256 hash in UserDefaults. The plaintext is never saved.

Disconnect helper (optional)

The optional "disconnect the remote session" feature is off by default. When you enable it (in settings or during onboarding), Curtain registers a privileged helper through SMAppService and asks for one approval in System Settings. There is no sudoers rule.

Under the current ad-hoc build, this helper may fail to register. The privileged-helper path needs a notarized or Developer ID signed build to install cleanly. Until then, leave the feature off or expect the registration to be rejected.

If any external monitor is DisplayLink, open Settings → Displays and mark it as DisplayLink. This tells Curtain to use a capturable cover mode for that display.

Displays are identified by a stable UUID, so the marking survives reboots and reconnects. Detection works with both classic and high-performance Screen Sharing.

See How It Works for why this matters.

Confirm Curtain is running

pgrep -fl Curtain

You can also open Activity Monitor and search for Curtain.

Uninstall

Quit Curtain, then drag Curtain.app from /Applications to the Trash.

If you had an older script-based install on this machine, Scripts/uninstall.sh in the repo cleans up any legacy LaunchAgent, helper binary, or sudoers rule left behind.