Skip to content

Harden clipboard fallback with native NSPasteboard snapshots #39

Description

@fly1d

Problem

When Accessibility does not expose AXSelectedText, the macOS fallback must use Command+C. The former AppleScript round trip kept only one preferred clipboard value and restored it unconditionally, which could lose rich representations or overwrite a newer clipboard generation.

Hardened direction

  • Snapshot every pasteboard item, declared type, type order, item order, and exact data bytes before sending Copy.
  • Fail before Copy if any declared representation cannot be materialized or the snapshot is unstable or exceeds the bounded in-memory limit.
  • Do not seed a sentinel. An unchanged changeCount never reads stale clipboard text.
  • Accept only the immediately following generation as a copy candidate. Reject generation jumps and any additional observed change.
  • Return candidate text only after the candidate stays stable and the original snapshot is restored successfully.
  • Restore with NSPasteboardContentsCurrentHostOnly because macOS does not expose the original host-only option.
  • Keep System Events only for the static Command+C event and retain explicit Accessibility and Automation permission guidance.
  • Run pasteboard object creation, access, restoration, and release on the macOS main thread; wait for the external System Events process off the UI thread.
  • Bound the System Events wait with a 15-second watchdog. Once Copy has been dispatched, any timeout, no-selection observation window, generation anomaly, native failure, or abandoned request quarantines copy fallback until app restart rather than admitting another transaction.

Public API limits

NSPasteboard does not expose writer identity or an atomic compare-and-swap operation. System Events and CoreGraphics also do not acknowledge that the target application's copy handler has completed. Therefore:

  • The first generation observed after Command+C cannot be proven to come from the target application rather than a simultaneous third-party write.
  • A write can land between the final generation check and prepareForNewContentsWithOptions; this check-to-write TOCTOU window cannot be eliminated with public APIs.
  • A target application may process an already-dispatched Copy after the observation window or after osascript is terminated. Quarantine prevents that late event from entering a newer Wordwise fallback transaction, but it may still change the current system clipboard.
  • The implementation can preserve every unexpected generation it observes before restoration starts and can isolate ambiguous post-Copy failures, but it cannot truthfully guarantee that every concurrent or delayed write is never overwritten.

An absolute guarantee requires disabling the copy fallback, leaving the copied value in place instead of restoring, or adding user confirmation before a fallback candidate can be translated.

Implementation acceptance

  • Plain text, RTF/HTML, image, file URL, empty, and multi-item snapshots round-trip byte-for-byte in native named-pasteboard tests.
  • No selection with an unchanged generation reports that no text was detected without reading old clipboard text, releases the in-memory snapshot, and quarantines copy fallback until restart.
  • Generation jumps, post-candidate changes, incomplete snapshots, native exceptions, and restore failures fail closed and never return candidate text for translation.
  • Timeout, wait failure, no-selection, generation anomaly, native failure, and abandoned-request paths cannot admit a newer fallback transaction in the same app process.
  • Error messages and logs contain no selected text or previous clipboard content.
  • Automated policy tests cover unchanged, exact-next, jump, wraparound, restore guard, empty snapshot, multi-item/type, observed newer-generation, and process watchdog paths.
  • Documentation discloses memory-only snapshot handling, current-host-only restoration, first-change attribution, delayed-event handling, quarantine, and the TOCTOU boundary.
  • PR and release workflows run Rust gates; PR CI builds the .app and checks bundled usage text; signed release CI verifies Developer ID authority, Team ID, and the Apple Events entitlement.

Release validation before closing

Keep this issue open as a public-release blocker until a signed and notarized candidate is manually tested for AX success, forced fallback with every representation class, Automation denied, no selection and quarantine, watchdog timeout and quarantine, observed concurrent update, permission revoke/regrant, first-run TCC text, effective entitlements, and actual cross-application shortcut behavior. Closing also requires an explicit product decision accepting the documented public-API residual risk or choosing a stricter fallback policy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrelease-blockerBlocks delivery of an installable public releaserisk:highPermissions, credentials, packaging, or release risk

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions