Skip to content

Secure-field awareness: don't persist a password-field dictation in plaintext (#433) - #455

Open
Nabzx wants to merge 5 commits into
mainfrom
secure-field-awareness-433
Open

Nabzx wants to merge 5 commits into
mainfrom
secure-field-awareness-433

Conversation

@Nabzx

@Nabzx Nabzx commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Closes #433. Dictating (or pasting) into a password field no longer gets logged, copied, or recorded to history in plaintext.

The gap

focusContext() in the accessibility helper only ever checked role == "AXTextField" || role == "AXComboBox" for isOneLineField - nothing anywhere checked for AXSecureTextField, macOS's own role for a password field. Every delivered dictation's transcript was unconditionally logged to the console and written to recording-history.json (persisted for up to historyRetentionDays, default 30 days), regardless of what kind of field it landed in. Push-to-talk is a global hotkey - it fires the same way everywhere, so habit or muscle memory triggering it while a password field happens to be focused was a real, if narrow, path to a spoken secret ending up as a plaintext log entry on disk.

What's built

  • accessibility-helper: focusContext() now also reports isSecure, true only for a role-confirmed AXSecureTextField - never guessed true when the element doesn't resolve, same caution isOneLineField's own default already uses.
  • dictationCoordinator.js: isSecure threaded onto every result that carries text - delivered, held (all the salvage paths too), and the paste command's delivered/held paths.
  • main.js: when isSecure is true, the dictation still delivers exactly as normal - this is purely about what gets persisted afterward. Not logged, not copied (even with "Copy transcript to clipboard" on), not written to history. The Held-result overlay is deliberately untouched - it's ephemeral and never written anywhere, and suppressing it would mean silently discarding a password the user could otherwise recover and paste manually.

Judgement call to flag

Extended to the paste command too, not just ordinary dictation - pasting into a password field is the same leak (clipboard content landing in plaintext history) even though the words weren't spoken. Not literally what the issue asked for, but the same one-line check on the same focusContext, same protective intent. Easy to revert if you'd rather keep this strictly to dictation.

What I could not verify

Compile-verified only. swift build --package-path native/accessibility-helper -c release succeeds. This sandbox can't meaningfully drive a real AX role read against a live password field - unlike transcription-helper (which gets killed outright touching CoreML/ANE), accessibility-helper can technically run here, but reading a real AXSecureTextField role needs a live GUI session with the Accessibility grant and an actual focused password field, neither of which exist in this environment. docs/testing/secure-field-433.md has the manual checklist - please run it before trusting this, especially the regression check (an ordinary text field should be completely unaffected).

Tests

accessibilityHelper.test.js (+2: a role-confirmed secure field parses isSecure: true; a reply that omits the field entirely defaults to false), dictationCoordinator.test.js (+5, plus the pre-existing full-result and full-diagnostics-array assertions updated for the new field - same class of update as #401/#259's). Full suite (424) + typecheck + build green.

🤖 Generated with Claude Code

…433)

focusContext() only ever checked role == AXTextField/AXComboBox for
isOneLineField - nothing anywhere checked for AXSecureTextField, macOS's
own role for a password field. Adds isSecure to the tuple and the
context reply, following the same caution isOneLineField already uses:
an element that never resolves can't be confirmed secure either way,
so it defaults to false rather than guessing true.

Compile-verified (swift build --package-path native/accessibility-helper
-c release). Never executed - this sandbox can't meaningfully drive a
real AX role read against a live password field. See
docs/testing/secure-field-433.md.

Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
Only true when the helper positively said so - a helper built before
this field existed (reply.isSecure undefined) parses to false, the
same unchanged behaviour a stale binary already gets for isOneLineField
and axReady.

Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
#433)

Every return site that carries text - the main delivered/held path,
both app-switch and context-read-failure salvage paths, and the paste
command's delivered/held paths - now carries isSecure alongside it, so
main.js can gate what gets persisted without re-deriving anything.

Extended the fix to the paste command too, not just ordinary
dictation: pasting into a password field is the same leak (clipboard
content landing in plaintext history) even though the words weren't
spoken. Not literally what the issue asked for, but the same one-line
check applied to the same underlying focusContext, and the same
protective intent.

A context read that fails outright can't confirm secure either way -
defaults to false, same call the Swift side makes for an unresolved
element.

Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
Delivery itself is unchanged - this only touches what gets persisted
after. The Held-result overlay still shows held text either way (the
user's own recovery path if delivery didn't land), since that's
ephemeral and never written anywhere; only the clipboard copy and the
recording-history write are skipped.

Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No secure-field awareness - password dictation is logged in plaintext

1 participant