Skip to content

feat(dictation): warn when macOS Secure Input blocks paste - #267

Merged
sambitcreate merged 9 commits into
mainfrom
feature/dictation-secure-input
Sep 30, 2026
Merged

sambitcreate merged 9 commits into
mainfrom
feature/dictation-secure-input

Conversation

@sambitcreate

@sambitcreate sambitcreate commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

Dictation previously reported “Pasted” when macOS Secure Event Input blocked synthetic keystrokes. It now checks the documented Carbon IsSecureEventInputEnabled() API through an explicitly bound JXA function. When Secure Input is active, the transcript remains on the clipboard and the pill explains the block with a ⌘V hint.

If the probe fails, dictation keeps the transcript available for manual paste. After sending a paste keystroke, the atomic AppleScript checks that the focused field’s accessibility value changed and contains the transcript before reporting delivery and restoring the previous clipboard. Unconfirmed delivery retains the transcript with a copied result.

The warning uses semantic styling and accessible text, stays visible for four seconds, and preserves the existing Accessibility permission precedence. This is desktop-only dictation IPC; there is no Aiden Remote protocol change.

Validation

  • Focused paste and pill tests cover active Secure Input, probe failure, output parsing, delivery confirmation, and warning rendering.
  • A live macOS test enables and disables a process-owned Secure Input claim and verifies both detector transitions; the AppleScript compiles successfully.
  • The pill test is registered in the CI test inventory.

Coordination

The Parakeet activation-mode PR #279 also changes the dictation coordinator. Reconcile those overlapping edits when merging. There remains a small race if Secure Input changes between the probe and the paste transaction; unconfirmed delivery preserves the clipboard transcript.

CI flake record

Secure Event Input (password fields, Secure Keyboard Entry) silently drops
the synthetic Cmd+V, so dictation reported "Pasted" while nothing arrived.

Probe the CoreGraphics session for kCGSSessionSecureInputPID via JXA before
pasting. When active, leave the transcript on the clipboard, skip the
keystroke, and show a warning in the pill ("Secure Input blocked paste —
press Cmd+V") that stays up longer than a normal result. Probe failures fall
through to the existing paste path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The warning can still miss blocked pastes, and the detector relies on an undocumented session key.

Reviewed changes This review covers the macOS Secure Input probe, dictation result propagation, pill notice, and related tests and documentation.

  • Secure Input detection Adds a JXA/CoreGraphics probe before the existing atomic paste and continues with the old paste path when probing fails.
  • Copied-result notice Adds secure-input to the desktop dictation payload and displays a longer-lived accessible warning with a manual paste instruction.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Luna | 𝕏

Comment thread main/services/dictation-paste.ts Outdated
Comment thread main/services/dictation-paste.ts
@very-hermes-bot

very-hermes-bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Hermes Review Bot

Confidence: 4

Engine: agy/gemini-3.8-flash-high
Review mode: full
Head: adf36bf9d9d9862ade48a1cadd3d2cdc8cdd6af7
Generated: 2026-09-30T21:41:21+00:00
Reviews: 1

Summary

Introduces detection of macOS Secure Event Input via Carbon's documented IsSecureEventInputEnabled() in JXA prior to synthetic keystroke delivery, leaving dictation transcripts on the clipboard with an explanatory warning when active. The pill UI now renders a dedicated warning notice (PillCopiedNotice) with accessibility announcements, semantic styling, and an extended four-second visibility window. The atomic paste AppleScript was also modified to inspect AXValue after dispatching ⌘V; however, the loop control flow in ATOMIC_PASTE_SCRIPT terminates on the very first check at 200 ms, rendering the 8-second polling limit dead and triggering false-negative failures on apps that take longer than 200 ms to insert text. Maintainers should double-check the polling and timeout logic in ATOMIC_PASTE_SCRIPT so slow target applications do not prematurely report unconfirmed paste delivery.

Confidence Score: 4/5

4/5. Traced the Carbon JXA bridge, coordinator state machine, IPC payload propagation, and pill rendering end-to-end against platform contracts and test suites; docked one point because live AppleScript interaction against diverse macOS GUI targets requires an interactive Darwin desktop session.

📁 Important Files Changed
  • main/services/dictation-paste.ts: Adds JXA detectMacSecureInput probe using IsSecureEventInputEnabled, branches in pasteTranscript on active Secure Input or probe errors, and updates ATOMIC_PASTE_SCRIPT to check AXValue.
  • main/services/dictation-coordinator.ts: Passes reason across IPC and extends pill dismissal timeout to WARNING_HIDE_DELAY_MS (4 s) when reason === "secure-input".
  • main/services/dictation.ts: Injects live Carbon detector dependency into livePasteDeps().
  • renderer/pill/pill-copied-notice.tsx: New component rendering the warning icon, copy guidance, and screen-reader accessibility context for copied outcomes.
  • renderer/pill/pill-app.tsx: Integrates PillCopiedNotice into the copied pill phase.
  • main/services/dictation-paste.test.ts: Unit tests for detector output mapping, precedence, and Darwin probe execution; adds string-grep assertions against ATOMIC_PASTE_SCRIPT.
  • main/services/dictation-coordinator.test.ts & renderer/pill/pill-copied-notice.test.tsx: Tests for linger delay, IPC propagation, and notice rendering.
  • package.json & scripts/ci-test-registry.json: Registers pill-copied-notice.test.tsx in test:voice and CI test inventory.

Findings

  • [P1 · logic] main/services/dictation-paste.ts:85 — ATOMIC_PASTE_SCRIPT terminates at 200 ms, making the 8-second polling loop and trailing return unreachable → commented inline
  • [P2 · logic] main/services/dictation-paste.test.ts:45 — Source-grep test assertions lock defective control flow and violate repository test rules → commented inline

Sequence Diagram

sequenceDiagram
    autonumber
    participant Pill as PillApp
    participant Coord as DictationCoordinator
    participant Paste as pasteTranscript
    participant Mac as macOS (Carbon / System Events)

    Pill->>Coord: handleDictationResult(text, opId)
    Coord->>Pill: broadcast("delivering")
    Coord->>Paste: paste(text)
    Paste->>Mac: detectMacSecureInput() (JXA Carbon probe)

    alt Secure Input Active
        Mac-->>Paste: "secure"
        Paste-->>Coord: { outcome: "copied", reason: "secure-input" }
        Coord->>Pill: broadcast("copied", reason: "secure-input")
        Note over Coord: scheduleHide(4000ms)
    else Secure Input Clear
        Mac-->>Paste: "clear"
        Paste->>Mac: runAtomicMacPaste(text) (AppleScript ⌘V)
        Mac-->>Paste: "pasted" / "copied"
        Paste-->>Coord: { outcome, reason, message }
        Coord->>Pill: broadcast(outcome)
        Note over Coord: scheduleHide(1200ms)
    end
Loading
[
  {
    "file": "main/services/dictation-paste.ts",
    "start_line": 85,
    "end_line": 93,
    "severity": "P1",
    "comment_type": "logic",
    "confidence": 0.92,
    "title": "ATOMIC_PASTE_SCRIPT terminates at 200 ms, making the 8-second polling loop and trailing return unreachable",
    "mechanism": "Every branch inside the quietWindow conditional exits via return, aborting the polling loop at 200 ms and prematurely returning copied when a target application takes longer than 200 ms to process ⌘V and update its AXValue.",
    "repair": "Continue the repeat loop when deliveredValue equals originalValue until a polling timeout expires, rather than immediately returning copied on the first 200 ms check.",
    "id": "17e446fe83c7d2b3"
  },
  {
    "file": "main/services/dictation-paste.test.ts",
    "start_line": 45,
    "end_line": 52,
    "severity": "P2",
    "comment_type": "logic",
    "confidence": 0.88,
    "title": "Source-grep test assertions lock defective control flow and violate repository test rules",
    "mechanism": "Growing assert.match assertions against ATOMIC_PASTE_SCRIPT violates the AGENTS.md rule against source-grepping tests and locks the defective control flow where deliveredValue is originalValue immediately exits.",
    "repair": "Remove the string-matching assertions against ATOMIC_PASTE_SCRIPT and rely on behavioral tests for clipboard preservation and delivery outcome.",
    "id": "e10b9ce10c3ec3e5"
  }
]

Last reviewed commit: adf36bf9d9d9
Reviews (1) · Comment /hermes review to trigger a new review · /hermes review full for full re-review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The probe's symbol choice is now verified correct — Apple's HIToolbox documents IsSecureEventInputEnabled() and its session-wide semantics — but no public JXA example calls it without ObjC.bindFunction. As committed the probe may throw on every macOS machine, and the new fail-closed handling would then silently degrade all dictation pastes to "copied". Must address before merging.

Reviewed changes Incremental review of the 2c68afe9 fix commit against the prior Pullfrog review of 28110282; both prior threads are resolved and retired.

  • Probe swap Replaced the undocumented kCGSSessionSecureInputPID CoreGraphics probe with the documented Carbon IsSecureEventInputEnabled() JXA one-liner.
  • Fail-closed probe failures A thrown or unrecognized probe result now leaves the transcript on the clipboard and skips the paste attempt instead of proceeding.
  • Delivery verification ATOMIC_PASTE_SCRIPT re-reads AXValue after the quiet window, returns copied when insertion can't be confirmed, and restores the previous clipboard only on evidence of delivery.
  • Pill notice extraction The copied branch moved into a PillCopiedNotice component with a warning-tone secure-input variant, plus refreshed coordinator, paste, and pill tests and CI-registry lane updates.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GLM Flash | 𝕏

Comment thread main/services/dictation-paste.ts
Comment thread docs/plans/dictation-secure-input-plan.md Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the new commits respond cleanly to the prior review; one stale plan-index row and an outdated PR description remain.

Reviewed changes Incremental review of the a779a6cb and 943827f6 commits against the prior Pullfrog review of 2c68afe9.

  • Probe cycle test Added a darwin-only test that runs a process-owned Secure Input enable/disable cycle through the plain $.IsSecureEventInputEnabled() JXA call, asserting the probe follows both transitions and restores the prior state — this is the permanent tripwire for the probe-binding concern raised earlier.
  • Plan doc correction Rewrote the Detection section, which previously claimed the Carbon API was not JXA-bridged and described the replaced CoreGraphics probe as the shipped mechanism.
  • Memory file Added the .memory/dictation-secure-input.md that the PR description referenced but that was missing from the earlier checkout.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GLM Flash | 𝕏

Comment thread docs/plans/README.md Outdated

@very-hermes-bot very-hermes-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline review — 2 finding(s) anchored to the diff. See the pinned summary comment for the overview.

Comment thread main/services/dictation-paste.ts Outdated
Comment thread main/services/dictation-paste.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Incremental review of the 4ed97014 commit against the prior Pullfrog review of 2c68afe9; both prior threads are now resolved and retired, and no earlier open thread is affected.

  • Probe binding Updated SECURE_INPUT_PROBE_SCRIPT to bind IsSecureEventInputEnabled with ["bool", []] before calling it, closing the BridgeSupport concern with the publicly evidenced recipe; the darwin enable/disable cycle and live-probe tests already cover this path.
  • Doc reconciliation Rewrote .memory/dictation-secure-input.md, the .papercuts correction, and the docs/plans/README.md index row for the Carbon detector and fail-closed fallback.

Pullfrog  | View workflow run | Using GLM Flash | 𝕏

@pullfrog

pullfrog Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Pullfrog stalled

The agent stopped emitting events for 120s and was killed by the activity-timeout watchdog. The model produced no output at all before the stall — the request was sent but nothing came back. This is usually transient; re-running often succeeds.

Recent agent stderr
timestamp=2026-09-27T17:41:20.824Z level=ERROR run=58372bfd message="stream error" providerID=opencode-go modelID=glm-5.3-flash session.id=ses_f1c0b07ceffen7LIPzBJapL39z small=false agent=build mode=primary error.error="AI_APICallError: Go usage limit exceeded"

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using GLM Flash | 𝕏

@very-hermes-bot very-hermes-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline review — 2 finding(s) anchored to the diff. See the pinned summary comment for the overview.

Comment on lines +85 to +93
if originalValue is missing value then return "copied"
try
tell application "System Events"
set deliveredValue to value of attribute "AXValue" of targetElement as text
end tell
if deliveredValue is originalValue or deliveredValue does not contain transcriptText then return "copied"
on error
return "copied"
end try

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 · logic] Atomic paste verification aborts after a single 200 ms check instead of polling
Confidence: 0.95

Inside ATOMIC_PASTE_SCRIPT, every code branch at quietWindow (0.2s) returns immediately, causing the repeat loop to terminate at 200ms and reporting unconfirmed delivery with lost clipboard restoration whenever an app takes longer than 200ms to update its accessibility value.

Repair: Decouple clipboard restoration quietWindow from the confirmation timeout, continuing the repeat loop when deliveredValue is still originalValue until a polling timeout expires.

Comment on lines +45 to +52
assert.match(
ATOMIC_PASTE_SCRIPT,
/deliveredValue is originalValue or deliveredValue does not contain transcriptText then return "copied"/,
);
assert.ok(
ATOMIC_PASTE_SCRIPT.indexOf("deliveredValue is originalValue") <
ATOMIC_PASTE_SCRIPT.indexOf("set the clipboard to previousClipboard"),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 · logic] Change-detector test asserts verbatim buggy AppleScript implementation
Confidence: 0.85

The test uses assert.match to lock the exact string of the premature abort in ATOMIC_PASTE_SCRIPT rather than testing behavior, violating AGENTS.md test guidelines and failing if the race condition is fixed.

Repair: Remove the source-matching assertions and replace them with behavioral tests verifying paste outcomes and clipboard preservation.

@pullfrog

pullfrog Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Pullfrog stalled

The agent stopped emitting events for 125s and was killed by the activity-timeout watchdog. The model produced no output at all before the stall — the request was sent but nothing came back. This is usually transient; re-running often succeeds.

Recent agent stderr
timestamp=2026-09-27T17:52:00.128Z level=ERROR run=19d0a660 message="stream error" providerID=opencode-go modelID=glm-5.3-flash session.id=ses_f1c0145bdffeLrw1AmdeOiPOaz small=false agent=build mode=primary error.error="AI_APICallError: Go usage limit exceeded"

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using GLM Flash | 𝕏

sambitcreate and others added 2 commits September 29, 2026 21:39
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/plans/README.md
#	scripts/ci-test-registry.json

@very-hermes-bot very-hermes-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline review — 1 finding(s) anchored to the diff. See the pinned summary comment for the overview.

@@ -68,6 +82,15 @@ export const ATOMIC_PASTE_SCRIPT = `on run argv
return "pasted"
end try
if elapsed is greater than or equal to quietWindow then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 · logic] Atomic paste confirmation prematurely aborts polling at 200 ms and leaves dead loop code
Confidence: 0.92

Every branch inside if elapsed is greater than or equal to quietWindow then returns immediately, exiting on iteration 4 at 0.20 s and making repeat while elapsed is less than 8 and trailing return "pasted" unreachable. If the target application takes longer than 200 ms to reflect the synthetic paste in AXValue, deliveredValue is originalValue returns copied prematurely even though text is inserted, skipping clipboard restoration.

Repair: Do not return copied immediately when deliveredValue is originalValue at quietWindow; only return pasted once deliveredValue is not originalValue and deliveredValue contains transcriptText, allowing the loop to continue polling until timeout before returning copied.

@very-hermes-bot very-hermes-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline review — 2 finding(s) anchored to the diff. See the pinned summary comment for the overview.

Comment on lines +85 to +93
if originalValue is missing value then return "copied"
try
tell application "System Events"
set deliveredValue to value of attribute "AXValue" of targetElement as text
end tell
if deliveredValue is originalValue or deliveredValue does not contain transcriptText then return "copied"
on error
return "copied"
end try

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1 · logic] ATOMIC_PASTE_SCRIPT terminates at 200 ms, making the 8-second polling loop and trailing return unreachable
Confidence: 0.92

Every branch inside the quietWindow conditional exits via return, aborting the polling loop at 200 ms and prematurely returning copied when a target application takes longer than 200 ms to process ⌘V and update its AXValue.

Repair: Continue the repeat loop when deliveredValue equals originalValue until a polling timeout expires, rather than immediately returning copied on the first 200 ms check.

Comment on lines +45 to +52
assert.match(
ATOMIC_PASTE_SCRIPT,
/deliveredValue is originalValue or deliveredValue does not contain transcriptText then return "copied"/,
);
assert.ok(
ATOMIC_PASTE_SCRIPT.indexOf("deliveredValue is originalValue") <
ATOMIC_PASTE_SCRIPT.indexOf("set the clipboard to previousClipboard"),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 · logic] Source-grep test assertions lock defective control flow and violate repository test rules
Confidence: 0.88

Growing assert.match assertions against ATOMIC_PASTE_SCRIPT violates the AGENTS.md rule against source-grepping tests and locks the defective control flow where deliveredValue is originalValue immediately exits.

Repair: Remove the string-matching assertions against ATOMIC_PASTE_SCRIPT and rely on behavioral tests for clipboard preservation and delivery outcome.

@sambitcreate
sambitcreate merged commit 0013e77 into main Sep 30, 2026
24 checks passed
@sambitcreate
sambitcreate deleted the feature/dictation-secure-input branch September 30, 2026 22:46
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.

2 participants