Skip to content

Commit d6862df

Browse files
feat: add experimental system-wide API key masking via Accessibility overlay
Add system-wide masking that detects API keys in any macOS application using the Accessibility API and covers them with floating NSPanel overlays. Core implementation: - SystemMaskingService: AXObserver monitors focused UI elements across all apps via kAXFocusedUIElementChangedNotification and kAXValueChangedNotification. 10ms initial / 30ms debounce + 500ms polling fallback. Immediate scan on app switch. - AXBoundsForRange: precise multi-line text coordinate calculation, supports keys spanning multiple visual lines - SystemOverlayController: click-through NSPanel overlays with ignoresMouseEvents, reuses panels via deterministic UUID per match - MaskOverlayView: solid background overlay with masked text display Integration: - Settings toggle: "System-wide masking (experimental)" in Security tab, default OFF. Only activates when both setting AND Demo Mode are ON. - Peek mode: hold ⌃⌥Space to temporarily hide all overlays - Overlay lifecycle: auto-clear on app switch, text change, Demo Mode OFF Performance (measured): - Pattern matching: 0.1-0.3ms per scan - Total scan + overlay: 2-6ms - NSHostingView reuse prevents view accumulation - Text cache prevents redundant overlay updates - Safe AX API usage: nil guards, isRunning check in callback Known limitations (marked experimental): - Only scans focused element (sidebar/non-focused areas not covered) - Copy/paste still exposes original key (visual overlay only) - Some apps may not fully support AX text attributes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 31577ea commit d6862df

9 files changed

Lines changed: 547 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ These are absolute rules — never violate them:
106106
- **Terminal masking sync block buffering** (experimental): Shielded Terminal buffers PTY output by detecting DEC 2026 sync block markers (`\x1b[?2026h`/`\x1b[?2026l`). Complete sync blocks are masked atomically. Non-sync data uses 30ms timeout buffer. This matches claude-chill's approach.
107107
- **Terminal masking ANSI-aware matching**: `maskTerminalOutput()` strips ANSI escape codes AND all whitespace (spaces, tabs, newlines) before regex matching. Ink word-wraps long keys with `\r\n` + indentation; stripping all whitespace allows regex to match keys across visual line breaks. Structural characters (ANSI + whitespace) within matched ranges are preserved in output via `extractStructural()`.
108108
- **Terminal masking node-pty loading**: Triple fallback: (1) `require('node-pty')`, (2) `require(vscode.env.appRoot + '/node_modules.asar.unpacked/node-pty')`, (3) `require(vscode.env.appRoot + '/node_modules/node-pty')`. Falls back to `child_process.spawn` line-mode terminal.
109+
- **System-wide masking** (experimental): Uses `AXObserver` + `kAXFocusedUIElementChangedNotification` / `kAXValueChangedNotification` to monitor focused text elements across all apps. `AXBoundsForRange` provides precise multi-line text coordinates. NSPanel overlay with `ignoresMouseEvents = true` for click-through. Settings toggle (`systemWideMasking` in UserDefaults), only active when both setting AND Demo Mode are ON.
110+
- **System-wide masking performance**: 10ms initial scan delay, 30ms debounce for changes, 500ms polling fallback. Actual processing 2-6ms. Immediate scan on app switch. Text cache (`lastScannedText`) prevents redundant overlay updates.
111+
- **System-wide masking coordinate conversion**: AX API uses top-left origin, AppKit uses bottom-left. Convert via `primaryScreenHeight - axY - height`. Multi-screen: use primary screen height as reference.
109112

110113
## Documentation
111114

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ See the [docs/](docs/) directory for detailed specifications:
169169
- [x] Linked Key Groups (sequential paste with ⌘V→Tab automation, Settings UI CRUD, pre-fetch Keychain)
170170
- [x] Clipboard Capture (⌃⌥⌘V hotkey, 22 built-in patterns, 3-tier confidence routing, whitespace-tolerant)
171171
- [x] 🧪 Terminal masking — Shielded Terminal (node-pty proxy, DEC 2026 sync block buffering, ANSI-aware masking, Claude Code compatible)
172+
- [x] 🧪 System-wide masking — Accessibility overlay (AXObserver + AXBoundsForRange, NSPanel click-through overlay, Settings toggle)
172173
- [ ] API Key rotation & deployment sync
173-
- [ ] System-wide masking (Accessibility API)
174174

175175
## Contributing
176176

docs/01-product-spec/implementation-status.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
| ~~`⌃⌥⌘V` capture clipboard~~ | Spec §4.4 | ~~~~ | ✅ HotkeyManager → ClipboardEngine.detectKeysInClipboard() → 內建 22 種 pattern 偵測 → confidence 三階路由 → VaultManager 存儲 |
3636
| ~~Linked Key Groups (sequential paste)~~ | Spec §6.3 | ~~~~ |`LinkedGroup`/`GroupEntry`/`SequentialPasteEngine` 完成、Settings UI 群組管理(CRUD)、`request_paste_group` IPC handler |
3737
| ~~Terminal masking (Shielded Terminal)~~ | Spec §3.2 | ~~~~ | 🧪 實驗性。node-pty proxy + DEC 2026 sync block buffering + ANSI-aware masking。Known limitation: Rewind 確認頁部分洩漏 |
38+
| ~~System-wide masking (Accessibility overlay)~~ | Spec §3.3 | ~~~~ | 🧪 實驗性。AXObserver + AXBoundsForRange + NSPanel overlay。Settings 可開關,Demo Mode 附加功能。Known limitation: 僅掃描 focused element、copy/paste 不安全 |
3839
| Shortcut conflict detection | Spec §4.4 || |
3940
| Import / Export vault | Spec §9.1 || |
4041

docs/en/01-product-spec/implementation-status.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
| ~~`⌃⌥⌘V` capture clipboard~~ | Spec §4.4 | ~~Medium~~ | ✅ HotkeyManager → ClipboardEngine.detectKeysInClipboard() → 22 built-in patterns → 3-tier confidence routing → VaultManager store |
3636
| ~~Linked Key Groups (sequential paste)~~ | Spec §6.3 | ~~Medium~~ |`LinkedGroup`/`GroupEntry`/`SequentialPasteEngine` complete, Settings UI group management (CRUD), `request_paste_group` IPC handler |
3737
| ~~Terminal masking (Shielded Terminal)~~ | Spec §3.2 | ~~Medium~~ | 🧪 Experimental. node-pty proxy + DEC 2026 sync block buffering + ANSI-aware masking. Known limitation: Rewind confirmation page partial leak |
38+
| ~~System-wide masking (Accessibility overlay)~~ | Spec §3.3 | ~~Medium~~ | 🧪 Experimental. AXObserver + AXBoundsForRange + NSPanel overlay. Settings toggle, Demo Mode enhancement. Known limitation: focused element only, copy/paste unsafe |
3839
| Shortcut conflict detection | Spec §4.4 | Low | |
3940
| Import / Export vault | Spec §9.1 | Low | |
4041

packages/swift-core/DemoSafe/App/AppState.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ final class AppState: ObservableObject {
2121
let sequentialPasteEngine: SequentialPasteEngine
2222
let toolboxState: ToolboxState
2323
let toolboxController: FloatingToolboxController
24+
let systemOverlayController: SystemOverlayController
25+
let systemMaskingService: SystemMaskingService
2426

2527
private var cancellables = Set<AnyCancellable>()
2628

@@ -34,6 +36,8 @@ final class AppState: ObservableObject {
3436
self.hotkeyManager = HotkeyManager(maskingCoordinator: maskingCoordinator)
3537
self.toolboxState = ToolboxState(vaultManager: vaultManager)
3638
self.toolboxController = FloatingToolboxController()
39+
self.systemOverlayController = SystemOverlayController()
40+
self.systemMaskingService = SystemMaskingService(maskingCoordinator: maskingCoordinator, overlayController: systemOverlayController)
3741

3842
// Sync isDemoMode bidirectionally with MaskingCoordinator
3943
maskingCoordinator.$isDemoMode
@@ -85,6 +89,14 @@ final class AppState: ObservableObject {
8589
isDemoMode.toggle()
8690
maskingCoordinator.isDemoMode = isDemoMode
8791
maskingCoordinator.broadcastState()
92+
93+
// Sync system-wide masking with Demo Mode (only if enabled in settings)
94+
let systemMaskingEnabled = UserDefaults.standard.bool(forKey: "systemWideMasking")
95+
if isDemoMode && systemMaskingEnabled {
96+
systemMaskingService.start()
97+
} else {
98+
systemMaskingService.stop()
99+
}
88100
}
89101

90102
func switchContext(contextId: UUID) {
@@ -116,18 +128,20 @@ final class AppState: ObservableObject {
116128
// MARK: - Private — Hotkey Wiring
117129

118130
private func wireHotkeyCallbacks() {
119-
// Toolbox show (hold start)
131+
// Toolbox show (hold start) — also enables peek mode for system overlays
120132
hotkeyManager.onToolboxShow = { [weak self] in
121133
guard let self else { return }
122134
self.toolboxState.reset()
123135
self.toolboxState.isVisible = true
136+
self.systemMaskingService.setPeekMode(true)
124137
let mouseLocation = NSEvent.mouseLocation
125138
self.toolboxController.show(near: mouseLocation)
126139
}
127140

128-
// Toolbox release (hold end)
141+
// Toolbox release (hold end) — also disables peek mode
129142
hotkeyManager.onToolboxRelease = { [weak self] in
130143
guard let self else { return }
144+
self.systemMaskingService.setPeekMode(false)
131145
self.toolboxState.handleRelease { [weak self] keyId in
132146
self?.copyKey(keyId: keyId)
133147
}

0 commit comments

Comments
 (0)