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]
PrivacyMonitorruns the polling loops (200 ms fast / 1 s slow) and listens to OS event hooks. Hook callbacks only callset()on a wake event — neverset_reasonsor COM/UIA calls — so they never block the OS event delivery thread.PauseControllerowns the recorder + overlay side-effects. The worker thread drains a coalescingQueueand only ever callsrecorder.stop_for_pause/resume_after_pause/overlay.show/hidefrom one place. Drops onqueue.Fullviaput_nowaitso 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 thesystem_transition1.5 s blackout.
Pause entry sequence¶
recorder.stop_for_pause(purge_lookback_s) does:
- Set
_pause_requestedso the recorder watchdog stops respawning ocap. - 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.
- Run a purge cascade with inclusive
>=filename-timestamp cutoff: - Current session's MCAP +
.logfiles — deleted wholesale. pruner.evict_newer_than(cutoff_ns)— keyframes inside the lookback.digest.evict_entries_newer_than(cutoff_ns)— digest rows ≥ cutoff removed from SQL;digest.mdrewritten from the newest surviving row.- 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. Withfail_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¶
PauseControllerowns a_lock.enter_pause/exit_pauseare the only sites that call recorder/overlay.- Hook callbacks (
NSWorkspace,SetWinEventHook) onlyset()a wake-event — never call COM/UIA or lock. PauseController.submit()usesput_nowait()and drops onqueue.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_lockis innermost. No module-outside-digest call while holding_digest_lock.
atexit order¶
LIFO: recorder.stop registered first, privacy.stop registered
second. On shutdown:
privacy.stopfires first — hides overlay, unhooks listeners, joins worker.recorder.stoptears 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 plaincmdwindow without a separate prompt window is missed. Use manual pause. - Windows legacy Win32
#32770+ES_PASSWORDdialogs (older VPN clients, SAP GUI, runas wrappers) are detected by walking the foreground HWND's child windows for a visibleEditwhose style includesES_PASSWORD. UIA'sIsPasswordPropertyIdis 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
loginwindowis 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¶
cue.privacy— module API reference.- Streaming recorder — what gets purged on pause.
- Privacy controls — user-facing companion page.