feat(dictation): warn when macOS Secure Input blocks paste - #267
Conversation
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>
There was a problem hiding this comment.
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-inputto the desktop dictation payload and displays a longer-lived accessible warning with a manual paste instruction.
GPT Luna | 𝕏
Hermes Review BotConfidence: 4 Engine: SummaryIntroduces detection of macOS Secure Event Input via Carbon's documented Confidence Score: 4/54/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
Findings
Sequence DiagramsequenceDiagram
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
[
{
"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"
}
]
|
There was a problem hiding this comment.
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
kCGSSessionSecureInputPIDCoreGraphics probe with the documented CarbonIsSecureEventInputEnabled()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_SCRIPTre-readsAXValueafter the quiet window, returnscopiedwhen insertion can't be confirmed, and restores the previous clipboard only on evidence of delivery. - Pill notice extraction The copied branch moved into a
PillCopiedNoticecomponent with a warning-tone secure-input variant, plus refreshed coordinator, paste, and pill tests and CI-registry lane updates.
GLM Flash | 𝕏
There was a problem hiding this comment.
ℹ️ 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.mdthat the PR description referenced but that was missing from the earlier checkout.
GLM Flash | 𝕏
very-hermes-bot
left a comment
There was a problem hiding this comment.
Inline review — 2 finding(s) anchored to the diff. See the pinned summary comment for the overview.
There was a problem hiding this comment.
✅ 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_SCRIPTto bindIsSecureEventInputEnabledwith["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.papercutscorrection, and thedocs/plans/README.mdindex row for the Carbon detector and fail-closed fallback.
GLM Flash | 𝕏
|
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
|
very-hermes-bot
left a comment
There was a problem hiding this comment.
Inline review — 2 finding(s) anchored to the diff. See the pinned summary comment for the overview.
| 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 |
There was a problem hiding this comment.
[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.
| 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"), | ||
| ); |
There was a problem hiding this comment.
[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 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
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # docs/plans/README.md # scripts/ci-test-registry.json
very-hermes-bot
left a comment
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
[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.
# Conflicts: # docs/plans/README.md
very-hermes-bot
left a comment
There was a problem hiding this comment.
Inline review — 2 finding(s) anchored to the diff. See the pinned summary comment for the overview.
| 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 |
There was a problem hiding this comment.
[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.
| 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"), | ||
| ); |
There was a problem hiding this comment.
[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.

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
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
npm installpostinstall failed:spawnSync node_modules/esbuild/bin/esbuild Unknown system error -88, a runner-level broken esbuild binary.