Skip to content

Privacy pause

This page mirrors implementation notes maintained in CLAUDE.md. Update both when changing this subsystem.

Privacy pause is Cue's first-class "Cue is not watching" state. It covers the manual hotkey, automatic password-field detection, configurable app/window/URL filters, and system sleep/wake. When on, the streaming recorder is killed, recently captured frames are purged, and a red border overlay confirms the state visually.

State model

Pause is keyed by a frozenset[str] of reasons. Pause is on iff the set is non-empty.

stateDiagram-v2
    [*] --> Watching
    Watching --> Paused : any reason adds → set non-empty
    Paused --> Paused : reason added / removed but still ≥ 1
    Paused --> Watching : last reason removed
    Paused --> [*] : process exit (atexit)

The five reason values:

Reason Source signal
manual Hotkey or tray menu toggle.
secure_input macOS Carbon IsSecureEventInputEnabled latch + AX kAXSecureTextField subrole on the focused element. Windows UIA IsPasswordPropertyId on the focused element.
blocked_app Frontmost app name on the configured block list, or its window title matches a configured regex. Title regex is skipped when the foreground app is in store.TERMINAL_APPS (Terminal / iTerm2 / Warp / 터미널 / ターミナル / 终端 / 終端機 / Console / 콘솔 / WindowsTerminal / Cmd / PowerShell / ConEmu / Cmder) so login-shell banners (Last login: …, bash --login) and SSH host names like login-1 don't false-trigger.
browser_url Frontmost browser's active tab URL matches a configured regex (only when the browser is in the authorized state).
system_transition NSWorkspaceWillSleep / DidWake (mac) or WM_POWERBROADCAST (Windows) — a 1.5 s pulse that covers the lock-screen / unlock window before regular detection cycles run.

reason_label() returns generic, UI-safe strings — "browser content", "blocked app", "system sleep/wake transition" — never specific app names or domains.

Two-component split

graph LR
    Hooks[NSWorkspace / SetWinEventHook<br/>AXObserver / AX poll<br/>fast 200ms + slow 1s] -- "set() wake" --> Monitor[PrivacyMonitor]
    Monitor -- "submit(reasons)" --> Controller[PauseController]
    Controller -- "drain queue<br/>coalesce to latest" --> Worker[Worker thread]
    Worker --> Recorder[recorder.stop_for_pause<br/>recorder.resume_after_pause]
    Worker --> Overlay[overlay.show / hide]
  • PrivacyMonitor runs the polling loops (200 ms fast / 1 s slow) and listens to OS event hooks. Hook callbacks only call set() on a wake event — never set_reasons or COM/UIA calls — so they never block the OS event delivery thread.
  • PauseController owns the recorder + overlay side-effects. The worker thread drains a coalescing Queue and only ever calls recorder.stop_for_pause / resume_after_pause / overlay.show / hide from one place. Drops on queue.Full via put_nowait so even a callback storm doesn't block the monitor.

PrivacyMonitor exposes a small signal surface:

  • request_rescan() — immediate poll wake.
  • request_rescan_debounced(min_interval_s=0.1) — coalesce a burst (used by the event-driven title/focus listeners).
  • pulse_reason(reason, duration_s) — transient reason that expires inside _compute_reasons. Used for the system_transition 1.5 s blackout.

Pause entry sequence

recorder.stop_for_pause(purge_lookback_s) does:

  1. Set _pause_requested so the recorder watchdog stops respawning ocap.
  2. SIGKILL the ocap subprocess immediately (no graceful EOS — pynput keyboard tap held by ocap can otherwise block macOS auth dialogs for the SIGINT grace period). Wait up to 1 s, then return.
  3. Run a purge cascade with inclusive >= filename-timestamp cutoff:
  4. Current session's MCAP + .log files — deleted wholesale.
  5. pruner.evict_newer_than(cutoff_ns) — keyframes inside the lookback.
  6. digest.evict_entries_newer_than(cutoff_ns) — digest rows ≥ cutoff removed from SQL; digest.md rewritten from the newest surviving row.
  7. Show the red-border overlay on every screen.

Resume

recorder.resume_after_pause() clears _pause_requested and spawns a fresh ocap session. There's a ~1 s gap of blank space before recording catches up.

Browser authorization tristate

<config root>/browser_auth.json:

  • authorized — user approved the macOS Automation prompt; URL auto-pause active.
  • denied — user explicitly opted out. With fail_closed_on_denied_browsers: True (default), foregrounding this browser triggers pause.
  • unknown — user hasn't decided. Permissive regardless of the fail-closed knob. This is the critical fix preventing the "Skip-all → all browsers bricked" footgun.

Manual pause persistence

Does NOT persist across restarts by default (persist_manual_pause: False). Manual is ephemeral ("pause now"); auto-reasons re-apply on next launch from live signals.

Thread model + lock ordering

  • PauseController owns a _lock. enter_pause / exit_pause are the only sites that call recorder/overlay.
  • Hook callbacks (NSWorkspace, SetWinEventHook) only set() a wake-event — never call COM/UIA or lock.
  • PauseController.submit() uses put_nowait() and drops on queue.Full. Monitor re-submits on next poll; loss is harmless.
  • Worker drains the queue and coalesces to the latest command before transitioning.
  • _pause_lock (recorder) is outermost; _digest_lock is innermost. No module-outside-digest call while holding _digest_lock.

atexit order

LIFO: recorder.stop registered first, privacy.stop registered second. On shutdown:

  1. privacy.stop fires first — hides overlay, unhooks listeners, joins worker.
  2. recorder.stop tears down ocap.

Known gaps

  • Windows console password prompts inside cmd.exe / ssh with no separate dialog window: no UIA surface, no system-wide latch. Wrapping process names (runas, etc.) are blocked apps so the elevation flow itself triggers pause, but a password typed into a plain cmd window without a separate prompt window is missed. Use manual pause.
  • Windows legacy Win32 #32770 + ES_PASSWORD dialogs (older VPN clients, SAP GUI, runas wrappers) are detected by walking the foreground HWND's child windows for a visible Edit whose style includes ES_PASSWORD. UIA's IsPasswordPropertyId is preferred when it works.
  • Windows UAC consent prompts run on the secure desktop; user-mode code can't enumerate that desktop, so the overlay can't render there. Pause still fires as soon as focus returns to the regular desktop.
  • macOS loginwindow is in the blocked list; it becomes the foreground "app" during lock-screen / fast-user-switch transitions. Pause is the intended behaviour there.
  • Purge horizon: bytes captured more than purge_lookback_s (default 5 s) before detection survive. Widen the knob or trigger manual pause sooner.
  • Audio / clipboard: Cue does not capture either today. Any future feature that adds them MUST consult privacy.is_paused() at the write site.

See also