<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: QUICOPY</title>
    <description>The latest articles on DEV Community by QUICOPY (@quicopy).</description>
    <link>https://dev.to/quicopy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3886173%2Fcecd83d7-aff8-4844-9e98-68ea115f1a29.png</url>
      <title>DEV Community: QUICOPY</title>
      <link>https://dev.to/quicopy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quicopy"/>
    <language>en</language>
    <item>
      <title>5 Ways to Type Repetitive Text Faster on Mac (2026 Guide)</title>
      <dc:creator>QUICOPY</dc:creator>
      <pubDate>Wed, 06 May 2026 10:37:09 +0000</pubDate>
      <link>https://dev.to/quicopy/5-ways-to-type-repetitive-text-faster-on-mac-2026-guide-34cg</link>
      <guid>https://dev.to/quicopy/5-ways-to-type-repetitive-text-faster-on-mac-2026-guide-34cg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Five paradigms, real setup steps, honest tradeoffs. From free built-in macOS features to dedicated apps. Pick the one that matches how your brain works, not whichever has the loudest reviews.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The hidden time tax of repetitive typing
&lt;/h2&gt;

&lt;p&gt;Most knowledge workers retype the same handful of phrases dozens of times a day. Email signatures. Standard replies. Code snippets. AI prompts that always start the same way. Your full address when filling forms. The phrase "Thanks, let me know if you have any questions" lives somewhere between muscle memory and pure resentment.&lt;/p&gt;

&lt;p&gt;If you type even ten common phrases ten times each per day, that's 100 chances to save 30 seconds. Forty minutes a day. &lt;strong&gt;Three and a half hours a week&lt;/strong&gt;. The math compounds quickly.&lt;/p&gt;

&lt;p&gt;This guide is the working tour of every method I've actually used on macOS. There are five distinct paradigms — not five products, but five different mental models for how to &lt;em&gt;not&lt;/em&gt; retype things. Most articles confuse these and recommend a tool without explaining the model. If you pick the wrong paradigm, no amount of feature polish saves you.&lt;/p&gt;

&lt;p&gt;Let's run through them in order of friction (lowest first), with the actual steps to set each one up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Way 1: macOS built-in Text Replacement
&lt;/h2&gt;

&lt;p&gt;The thing already on your Mac that you probably forgot exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; 5-10 short phrases · &lt;strong&gt;Cost:&lt;/strong&gt; Free (built-in) · &lt;strong&gt;Setup time:&lt;/strong&gt; 30 seconds&lt;/p&gt;

&lt;h3&gt;
  
  
  What it is
&lt;/h3&gt;

&lt;p&gt;macOS has a system-wide text replacement feature that converts a typed shortcut (like &lt;code&gt;omw&lt;/code&gt;) into a longer phrase (&lt;code&gt;On my way!&lt;/code&gt;). It works in any text field, syncs to iPhone and iPad via iCloud, and costs zero dollars.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;System Settings&lt;/strong&gt; → &lt;strong&gt;Keyboard&lt;/strong&gt; → click &lt;strong&gt;Text Replacements...&lt;/strong&gt; at the bottom&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;+&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Replace&lt;/strong&gt; column type your trigger (e.g. &lt;code&gt;;sig&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;With&lt;/strong&gt; column type the expansion (e.g. your full email signature)&lt;/li&gt;
&lt;li&gt;Press Return. It's live everywhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A real example
&lt;/h3&gt;

&lt;p&gt;I use it for two things: my mailing address (trigger &lt;code&gt;;addr&lt;/code&gt;) and the phrase "Thanks for the quick reply" (trigger &lt;code&gt;;tqr&lt;/code&gt;). Both expand instantly in Mail, Safari forms, and Messages. They sync to my iPhone, which is the only reason I bother filling forms on mobile at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free, already installed&lt;/li&gt;
&lt;li&gt;Syncs across Mac, iPhone, iPad via iCloud&lt;/li&gt;
&lt;li&gt;Zero permissions to grant&lt;/li&gt;
&lt;li&gt;Cannot be more lightweight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editor is awkward — adding 10+ entries is painful&lt;/li&gt;
&lt;li&gt;No formatting (bold, links, line breaks work but inconsistently)&lt;/li&gt;
&lt;li&gt;Doesn't fire in some apps (notably some Electron apps and code editors)&lt;/li&gt;
&lt;li&gt;No groups, no organization, no search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Try this first. If it covers your needs, stop reading. Most people only think they need a paid tool because they never opened the System Settings panel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Way 2: A dedicated text expansion app
&lt;/h2&gt;

&lt;p&gt;Same paradigm as Way 1, but with a real editor and serious scaling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; 30-500+ snippets, possibly with variables · &lt;strong&gt;Cost:&lt;/strong&gt; Free to ~$40/yr · &lt;strong&gt;Setup time:&lt;/strong&gt; 5 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  What it is
&lt;/h3&gt;

&lt;p&gt;A category of apps that take the abbreviation-trigger model from Way 1 and adds proper management: groups, search, fillable variables, scripted snippets, multi-line formatting, sync, and a real UI for editing. The leaders are &lt;a href="https://textexpander.com" rel="noopener noreferrer"&gt;TextExpander&lt;/a&gt; ($40/yr), &lt;a href="https://espanso.org" rel="noopener noreferrer"&gt;Espanso&lt;/a&gt; (free, open-source), and &lt;a href="https://trankynam.com/atext/" rel="noopener noreferrer"&gt;aText&lt;/a&gt; (~$5 one-time).&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup (Espanso, since it's free)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install via Homebrew: &lt;code&gt;brew install espanso&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;espanso start --unmanaged&lt;/code&gt; to confirm it's working&lt;/li&gt;
&lt;li&gt;Edit &lt;code&gt;~/Library/Application Support/espanso/match/base.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add a snippet:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:sig"&lt;/span&gt;
       &lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Best,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Name"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Save. Reload with &lt;code&gt;espanso restart&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A real example
&lt;/h3&gt;

&lt;p&gt;A friend who runs customer support uses TextExpander for ~80 templated responses, organized into groups by topic. Trigger &lt;code&gt;;refund&lt;/code&gt; opens a fillable form asking for the customer's name and order number, then expands into a full apology + refund confirmation. This is the kind of workflow Way 1 cannot do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scales to hundreds of snippets cleanly&lt;/li&gt;
&lt;li&gt;Variables / fillable forms (templated emails)&lt;/li&gt;
&lt;li&gt;Cross-platform (TextExpander) or cross-OS (Espanso)&lt;/li&gt;
&lt;li&gt;Team libraries with shared groups (TextExpander)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real cost: TextExpander is ~$40/year forever&lt;/li&gt;
&lt;li&gt;Espanso requires editing yaml files (engineers love this; nobody else does)&lt;/li&gt;
&lt;li&gt;You still have to type a trigger — not "true" one-keypress&lt;/li&gt;
&lt;li&gt;False triggers happen with poorly-chosen abbreviations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: The right call if you have 30+ snippets you actually use, especially if you need fillable forms or team sharing. If you're shopping in this category, see my &lt;a href="https://www.quicopy.com/blog/textexpander-alternative-mac" rel="noopener noreferrer"&gt;honest TextExpander comparison&lt;/a&gt; for the deeper breakdown.&lt;/p&gt;




&lt;h2&gt;
  
  
  Way 3: A keyboard shortcut tool (direct hotkey)
&lt;/h2&gt;

&lt;p&gt;A different paradigm entirely. Skip the abbreviation; bind text directly to a key combo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; 5-30 frequent phrases you reach for daily · &lt;strong&gt;Cost:&lt;/strong&gt; Free trial → $9.99 lifetime (QUICOPY) · &lt;strong&gt;Setup time:&lt;/strong&gt; 2 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  What it is
&lt;/h3&gt;

&lt;p&gt;Instead of typing &lt;code&gt;;sig&lt;/code&gt; and waiting for it to expand, you press &lt;code&gt;⌃⌥1&lt;/code&gt; directly and the text appears. No abbreviation to remember; no false triggers; no typing latency. The tradeoff: you only have so many comfortable modifier combinations, so this paradigm caps somewhere around 30 snippets before your fingers run out of memorable bindings.&lt;/p&gt;

&lt;p&gt;This is what &lt;a href="https://apps.apple.com/app/quicopy/id6761418490" rel="noopener noreferrer"&gt;QUICOPY&lt;/a&gt; does. It's the app I built. There are a handful of other tools that bolt this on top of an abbreviation-first design, but QUICOPY is one of the few where direct hotkey binding is the design center.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup (QUICOPY)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Download from the &lt;a href="https://apps.apple.com/app/quicopy/id6761418490" rel="noopener noreferrer"&gt;Mac App Store&lt;/a&gt; (free trial, $9.99 lifetime)&lt;/li&gt;
&lt;li&gt;Click the menu bar icon → &lt;strong&gt;Add Mapping&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Type your text (an email signature, a code snippet, an AI prompt — anything)&lt;/li&gt;
&lt;li&gt;Click the keyboard field, press your shortcut (e.g. &lt;code&gt;⌃⌥1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Save. Press the shortcut anywhere on macOS to paste the text.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A real example
&lt;/h3&gt;

&lt;p&gt;I use seven shortcuts every day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;⌃⌥1&lt;/code&gt; — "Explain step by step:" (AI prompt scaffold)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;⌃⌥2&lt;/code&gt; — "Translate to English:" (AI prompt scaffold)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;⌃⌥3&lt;/code&gt; — My email signature&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;⌃⌥4&lt;/code&gt; — My GitHub address&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;⌃⌥5&lt;/code&gt;, &lt;code&gt;⌃⌥6&lt;/code&gt;, &lt;code&gt;⌃⌥7&lt;/code&gt; — Three semi-templated email replies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;QUICOPY ships with seven AI prompt templates pre-bound to &lt;code&gt;⌘⇧1&lt;/code&gt; through &lt;code&gt;⌘⇧7&lt;/code&gt;, which was the original use case it was built for. The custom mapping layer came after — once I had the hotkey infrastructure for AI prompts, binding personal text to additional shortcuts was a free byproduct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;True one-keypress output&lt;/li&gt;
&lt;li&gt;No abbreviation to remember; no false triggers&lt;/li&gt;
&lt;li&gt;Muscle memory binds physical keys to outputs&lt;/li&gt;
&lt;li&gt;Works in every Mac app (including Zed, VS Code, Slack)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Caps at ~30 snippets before modifier fatigue&lt;/li&gt;
&lt;li&gt;Mac only (no iOS / Windows version)&lt;/li&gt;
&lt;li&gt;No fillable forms, no variables&lt;/li&gt;
&lt;li&gt;Requires Accessibility permission for non-AppKit apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: The fastest paradigm for the texts you reach for daily. Wrong paradigm if your library is hundreds of snippets, you need fillable variables, or you work outside macOS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Way 4: A launcher snippet feature
&lt;/h2&gt;

&lt;p&gt;If you already have a launcher, you might already have this and not know it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Hundreds of snippets, search-driven recall · &lt;strong&gt;Cost:&lt;/strong&gt; Free (Raycast Free) to $8/mo (Raycast Pro) · &lt;strong&gt;Setup time:&lt;/strong&gt; 1 minute&lt;/p&gt;

&lt;h3&gt;
  
  
  What it is
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://raycast.com" rel="noopener noreferrer"&gt;Raycast&lt;/a&gt; and &lt;a href="https://www.alfredapp.com" rel="noopener noreferrer"&gt;Alfred&lt;/a&gt; both have built-in snippet features. The flow: open the launcher (&lt;code&gt;⌘Space&lt;/code&gt; or your custom hotkey) → type a few letters of the snippet's name → hit Enter → the text gets pasted at your cursor position. It's three keystrokes minimum, but it scales infinitely because you search by name, not by exact-match trigger.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup (Raycast)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install Raycast (free)&lt;/li&gt;
&lt;li&gt;Open Raycast (&lt;code&gt;⌘Space&lt;/code&gt; if you replaced Spotlight, otherwise the default &lt;code&gt;⌥Space&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;Create Snippet&lt;/code&gt; → Enter&lt;/li&gt;
&lt;li&gt;Name the snippet, paste the text, optionally set an inline keyword&lt;/li&gt;
&lt;li&gt;Save. To use later: open Raycast → type the snippet name → Enter to paste.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A real example
&lt;/h3&gt;

&lt;p&gt;I keep maybe 40 snippets in Raycast: code patterns I rarely need (a SwiftUI &lt;code&gt;NavigationStack&lt;/code&gt; scaffold, a Vercel build config block), boilerplate emails for one-off scenarios, structured AI prompts I use weekly but not daily. None of them are frequent enough to deserve a Way 3 hotkey, but they're real enough that I don't want to rewrite them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scales to hundreds of snippets cleanly&lt;/li&gt;
&lt;li&gt;Fuzzy search by name beats exact-match abbreviations&lt;/li&gt;
&lt;li&gt;Free if you already use Raycast or Alfred&lt;/li&gt;
&lt;li&gt;Visual confirmation before pasting (good for high-stakes texts)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three keystrokes minimum — slower than Way 1, 2, or 3 for daily-use texts&lt;/li&gt;
&lt;li&gt;Cognitive overhead: you have to remember the name&lt;/li&gt;
&lt;li&gt;Lock-in to launcher app (snippets don't follow you if you switch)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Excellent for the long tail. Bad for high-frequency repetition. Most knowledge workers benefit from combining this with Way 3 (hotkeys for daily texts, launcher search for the rest).&lt;/p&gt;




&lt;h2&gt;
  
  
  Way 5: A clipboard manager with templates
&lt;/h2&gt;

&lt;p&gt;The clipboard-centric paradigm. Pin text to your clipboard history; recall it visually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Visual thinkers, occasional repeated text · &lt;strong&gt;Cost:&lt;/strong&gt; Free (Maccy) to ~$15 one-time (Paste) · &lt;strong&gt;Setup time:&lt;/strong&gt; 2 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  What it is
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/p0deje/Maccy" rel="noopener noreferrer"&gt;Maccy&lt;/a&gt; (free, open-source), &lt;a href="https://pasteapp.io" rel="noopener noreferrer"&gt;Paste&lt;/a&gt; ($15 one-time or subscription), and &lt;a href="https://copyclip.app" rel="noopener noreferrer"&gt;CopyClip 2&lt;/a&gt; (free) all do the same fundamental thing: track what you copy, let you re-paste old items via a panel UI. The "templates" feature in some of these lets you &lt;em&gt;pin&lt;/em&gt; specific texts so they never roll off the history. You access them by hotkey → arrow keys → Enter, or by typing a search query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup (Maccy)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install via Homebrew: &lt;code&gt;brew install --cask maccy&lt;/code&gt; (or download from website)&lt;/li&gt;
&lt;li&gt;Grant Accessibility permission when prompted&lt;/li&gt;
&lt;li&gt;Default hotkey: &lt;code&gt;⌘⇧C&lt;/code&gt; opens the history panel&lt;/li&gt;
&lt;li&gt;To pin a snippet: copy it once, then in the panel right-click → &lt;strong&gt;Pin&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;To paste: &lt;code&gt;⌘⇧C&lt;/code&gt; → arrow to the pinned item → Enter&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A real example
&lt;/h3&gt;

&lt;p&gt;This paradigm shines for content that doesn't fit cleanly into "frequent enough for a hotkey" or "memorable enough for a name." Think: a long URL you keep referencing this week, a code error message you're tracking, a multiline JSON payload for testing. Pin it, recall it visually, throw it away when the project ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✅ Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doubles as a clipboard manager (huge value beyond text expansion)&lt;/li&gt;
&lt;li&gt;Visual recall — see your snippets, don't memorize them&lt;/li&gt;
&lt;li&gt;Maccy and CopyClip 2 are completely free&lt;/li&gt;
&lt;li&gt;Natural fit for transient text that has a project lifespan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slowest paradigm for high-frequency texts (panel + arrow keys)&lt;/li&gt;
&lt;li&gt;Pin management gets messy past 20 items&lt;/li&gt;
&lt;li&gt;Not designed for true permanent snippets — feels like a workaround&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Worth installing as a clipboard manager regardless. The text-expansion use case is a side-effect, not the design center. Use it for transient project-bound texts, not your permanent snippet library.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision matrix: which one matches your situation?
&lt;/h2&gt;

&lt;p&gt;Skim the rows. Pick the first one that describes you. If multiple match, the higher row wins (lower friction = better default).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Best paradigm&lt;/th&gt;
&lt;th&gt;Specific tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You have 5-10 phrases and don't want to install anything&lt;/td&gt;
&lt;td&gt;Way 1&lt;/td&gt;
&lt;td&gt;macOS Text Replacement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You have 30-200 snippets and need them organized in groups&lt;/td&gt;
&lt;td&gt;Way 2&lt;/td&gt;
&lt;td&gt;TextExpander or Espanso&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your snippets need variables (fill in name, date, amount)&lt;/td&gt;
&lt;td&gt;Way 2&lt;/td&gt;
&lt;td&gt;TextExpander (paid) or Espanso (free, yaml)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You have a small library but use the same 5-10 every hour&lt;/td&gt;
&lt;td&gt;Way 3&lt;/td&gt;
&lt;td&gt;QUICOPY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You already use Raycast or Alfred daily&lt;/td&gt;
&lt;td&gt;Way 4&lt;/td&gt;
&lt;td&gt;Raycast Snippets or Alfred Snippets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want a clipboard manager and snippets are a bonus&lt;/td&gt;
&lt;td&gt;Way 5&lt;/td&gt;
&lt;td&gt;Maccy (free) or Paste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You work across Mac + iOS + Windows&lt;/td&gt;
&lt;td&gt;Way 2&lt;/td&gt;
&lt;td&gt;TextExpander (only one with full cross-platform sync)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want everything to be free and you're comfortable with config files&lt;/td&gt;
&lt;td&gt;Way 2&lt;/td&gt;
&lt;td&gt;Espanso (free, OSS, cross-platform)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most people will benefit from &lt;strong&gt;combining two of these&lt;/strong&gt;: Way 1 for casual phrases (free, syncs to phone), plus either Way 3 (one-keypress for daily texts) or Way 4 (search-based for the long tail). The combinations that feel natural:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Light user&lt;/strong&gt;: Way 1 alone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily writer with templates&lt;/strong&gt;: Way 1 + Way 4 (Raycast)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy user with team&lt;/strong&gt;: Way 2 (TextExpander)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed-obsessed daily user&lt;/strong&gt;: Way 3 (QUICOPY) + Way 1 for cross-device sync&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Beyond the 5: AI-assisted writing
&lt;/h2&gt;

&lt;p&gt;One category I deliberately skipped: AI tools that generate text on demand instead of expanding pre-saved snippets. Things like &lt;a href="https://raycast.com/blog/raycast-ai" rel="noopener noreferrer"&gt;Raycast AI&lt;/a&gt;, &lt;a href="https://www.macgpt.com" rel="noopener noreferrer"&gt;MacGPT&lt;/a&gt;, or any number of menu-bar ChatGPT clients. These solve a different problem — generating new text that varies — versus pasting saved text that doesn't.&lt;/p&gt;

&lt;p&gt;Both have a place. The mistake is using AI tools for high-frequency static text (slow, expensive, non-deterministic) or using snippet tools for one-off generation (impossible). Pick the right shelf, then the right product on that shelf.&lt;/p&gt;

&lt;p&gt;For what it's worth, QUICOPY ships with seven pre-bound AI prompt templates that scaffold ChatGPT/Claude conversations — "Explain step by step:", "Translate to English:", "Summarize in 3 bullets:" — which is the bridge between these two categories. Type the scaffold with one keypress, then continue the conversation with whatever you actually want to ask. If you do this all day, the keystroke savings stack up.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about… the thing nobody asked about
&lt;/h2&gt;

&lt;p&gt;Two questions I get often that don't fit the structure above:&lt;/p&gt;

&lt;h3&gt;
  
  
  "Can I just use my keyboard's built-in macros?"
&lt;/h3&gt;

&lt;p&gt;Some keyboards (Logitech MX Keys, Keychron Q-series) have macro recording built in. They work, but the macros are stored on the keyboard, which means: you lose them if the keyboard breaks, you can't sync them, and editing is awful. Use them for things tied to that specific keyboard's job (like a streamer's stream-deck-style trigger) — not for general text expansion.&lt;/p&gt;

&lt;h3&gt;
  
  
  "What about emoji shortcuts?"
&lt;/h3&gt;

&lt;p&gt;macOS already has &lt;code&gt;⌃⌘Space&lt;/code&gt; for the emoji picker. If you want a specific emoji to map to a specific trigger (e.g. &lt;code&gt;:shrug:&lt;/code&gt; → &lt;code&gt;¯\_(ツ)_/¯&lt;/code&gt;), Way 1 (macOS Text Replacement) handles that perfectly. No need for a special tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Picking one: the 5-minute test
&lt;/h2&gt;

&lt;p&gt;If you've read this far and still aren't sure, do this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open System Settings → Keyboard → Text Replacements (Way 1)&lt;/li&gt;
&lt;li&gt;Add 3 of your most-typed phrases with simple triggers&lt;/li&gt;
&lt;li&gt;Use them for one full day&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you find yourself frustrated by the editor or hitting the limits, you've already validated that you need to graduate to Way 2, 3, or 4. You'll know which one based on what frustrated you: too many snippets to manage (→ Way 2), wanting one-keypress speed (→ Way 3), or struggling to remember triggers (→ Way 4).&lt;/p&gt;

&lt;p&gt;If Way 1 covers your needs, you've saved yourself $40/year and a lot of decision-making.&lt;/p&gt;




&lt;h2&gt;
  
  
  About QUICOPY
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://www.quicopy.com" rel="noopener noreferrer"&gt;QUICOPY&lt;/a&gt; — a macOS menu bar app for Way 3, the direct keyboard shortcut paradigm. It's on the &lt;a href="https://apps.apple.com/app/quicopy/id6761418490" rel="noopener noreferrer"&gt;Mac App Store&lt;/a&gt; for $9.99 lifetime or $1.99/month with a 7-day free trial.&lt;/p&gt;

&lt;p&gt;If your situation calls for a different paradigm than Way 3, this article is genuinely the right answer over a sales pitch. I'd rather you pick the right tool for your situation than the one I happen to make.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.quicopy.com/blog/type-repetitive-text-faster-mac" rel="noopener noreferrer"&gt;quicopy.com&lt;/a&gt;. Related posts: &lt;a href="https://www.quicopy.com/blog/textexpander-alternative-mac" rel="noopener noreferrer"&gt;TextExpander Alternatives for Mac in 2026&lt;/a&gt; · &lt;a href="https://www.quicopy.com/blog/macos-shortcut-dispatch-zed" rel="noopener noreferrer"&gt;The macOS Global Shortcut That Won't Fire in Zed&lt;/a&gt; · &lt;a href="https://www.quicopy.com/blog/macos-sandbox-keyboard-shortcuts" rel="noopener noreferrer"&gt;macOS Sandbox and Keyboard Shortcuts&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>productivity</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>TextExpander Alternatives for Mac in 2026: An Honest Comparison</title>
      <dc:creator>QUICOPY</dc:creator>
      <pubDate>Sat, 02 May 2026 14:06:26 +0000</pubDate>
      <link>https://dev.to/quicopy/textexpander-alternatives-for-mac-in-2026-an-honest-comparison-3cn8</link>
      <guid>https://dev.to/quicopy/textexpander-alternatives-for-mac-in-2026-an-honest-comparison-3cn8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Five tools, real tradeoffs, no feature checklists. Includes the case for staying on TextExpander, plus notes on migrating off if you decide to leave.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you have a few dozen frequently-used phrases and you're tired of paying TextExpander's subscription, you can probably leave. &lt;a href="https://apps.apple.com/app/quicopy/id6761418490" rel="noopener noreferrer"&gt;QUICOPY&lt;/a&gt; ($9.99 lifetime) and &lt;a href="https://espanso.org" rel="noopener noreferrer"&gt;Espanso&lt;/a&gt; (free) cover most personal use cases.&lt;/p&gt;

&lt;p&gt;If you have hundreds of snippets, share them with a team, depend on fillable forms, or work across Mac + iOS + Windows, &lt;strong&gt;TextExpander still has no real peer&lt;/strong&gt;. The pricing exists because the product earns it.&lt;/p&gt;

&lt;p&gt;This post is a working tour of the five tools I'd actually recommend in 2026. The point is to surface tradeoffs honestly, including the ones that make my own app the wrong choice for some readers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why people search for TextExpander alternatives
&lt;/h2&gt;

&lt;p&gt;The search query doesn't usually mean "TextExpander is bad." It means one of three more specific things, and the right alternative depends on which one is yours:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Subscription fatigue.&lt;/strong&gt; TextExpander is around &lt;code&gt;$3.33/month&lt;/code&gt; billed yearly for the individual plan as of 2026-05. Multiply by N years of use and the math eventually gets uncomfortable, especially for people who only use a dozen snippets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow mismatch.&lt;/strong&gt; The abbreviation paradigm (type &lt;code&gt;;sig&lt;/code&gt;, get your signature) is brilliant for some people and friction for others. If you've tried it for six months and you still mistype the trigger or forget which abbreviation you set, the tool isn't wrong, but the paradigm isn't yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature gap in a specific direction.&lt;/strong&gt; You want native AI prompt templates, or you want shortcuts that work without typing first, or you want something that runs on Linux too. TextExpander is the most polished tool in its lane, but it's not the only lane.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your reason is &lt;strong&gt;(1) subscription fatigue and a small library&lt;/strong&gt;, the cheap-and-cheerful alternatives below are real. If it's &lt;strong&gt;(2) workflow mismatch&lt;/strong&gt;, you might be looking for a different paradigm entirely, not a TextExpander clone. If it's &lt;strong&gt;(3) a specific feature gap&lt;/strong&gt;, the right answer is whichever tool fills that gap, even if it's not as polished overall.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fundamental UX divide nobody mentions
&lt;/h2&gt;

&lt;p&gt;Every text-expansion tool falls into one of three camps. Most "alternative" articles compare features. The bigger question is which camp you naturally live in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Camp A: Abbreviation triggers
&lt;/h3&gt;

&lt;p&gt;You type a short string (&lt;code&gt;;sig&lt;/code&gt;, &lt;code&gt;ddate&lt;/code&gt;, &lt;code&gt;:eml&lt;/code&gt;) and the tool replaces it with longer text in place. This is what TextExpander, Espanso, Rocket Typist, and aText all do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: Scales to thousands of snippets without filling your shortcut space. Works in any text field by definition. Mnemonic triggers ride on language memory you already have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: You have to remember the abbreviations. After 50+ snippets, you forget which trigger maps to which text and end up using maybe 10 of them. False triggers happen (try setting &lt;code&gt;;ad&lt;/code&gt; as a trigger and watch chaos). And typing a trigger string is still typing — for one-keypress reflexes, it's slower than a hotkey.&lt;/p&gt;

&lt;h3&gt;
  
  
  Camp B: Direct shortcut binding
&lt;/h3&gt;

&lt;p&gt;You bind a global hotkey (&lt;code&gt;⌃⌥1&lt;/code&gt;, &lt;code&gt;⌃⌥2&lt;/code&gt;) to a piece of text. Press the key in any app and the text appears at the cursor. macOS's built-in &lt;em&gt;System Settings → Keyboard → Text Replacements&lt;/em&gt; tries this and gives up; tools like QUICOPY and a few others run further with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: True one-keypress output. No abbreviation to remember. No false triggers. Muscle memory binds physical key positions to outputs, which is faster than any string trigger once you've learned 5-10 of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: Doesn't scale past 20-50 snippets before you run out of comfortable modifier combinations. Your fingers, not your brain, are the bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  Camp C: Launcher pickers
&lt;/h3&gt;

&lt;p&gt;You open a launcher (Raycast, Alfred), search for the snippet, hit Enter. The text gets pasted at your previous cursor position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: Scales infinitely. Searchable by name, not just by exact-match trigger. Free if you already use Raycast or Alfred.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: Three keystrokes minimum (open launcher, type query, Enter). For text you use multiple times per hour, this loses to both A and B.&lt;/p&gt;

&lt;p&gt;This taxonomy matters because &lt;strong&gt;most "alternative" recommendations ignore it&lt;/strong&gt;. You can't replace a Camp A workflow with a Camp B tool and expect happiness. If you've been using TextExpander successfully, you're a Camp A user. The question is: do you want to &lt;em&gt;stay&lt;/em&gt; in Camp A on different software, or are you ready to try a different camp?&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature matrix
&lt;/h2&gt;

&lt;p&gt;The honest version. Every cell is something I'd be willing to defend in a comments section.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;TextExpander&lt;/th&gt;
&lt;th&gt;QUICOPY&lt;/th&gt;
&lt;th&gt;Espanso&lt;/th&gt;
&lt;th&gt;Rocket Typist&lt;/th&gt;
&lt;th&gt;aText&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Camp&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing (USD, 2026-05)&lt;/td&gt;
&lt;td&gt;~$40/yr&lt;/td&gt;
&lt;td&gt;$9.99 once / $1.99 mo&lt;/td&gt;
&lt;td&gt;Free / OSS&lt;/td&gt;
&lt;td&gt;~$20 once or Setapp&lt;/td&gt;
&lt;td&gt;~$5 MAS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platforms&lt;/td&gt;
&lt;td&gt;Mac, iOS, Windows, Chrome&lt;/td&gt;
&lt;td&gt;Mac only&lt;/td&gt;
&lt;td&gt;Mac, Windows, Linux&lt;/td&gt;
&lt;td&gt;Mac only&lt;/td&gt;
&lt;td&gt;Mac only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync across devices&lt;/td&gt;
&lt;td&gt;Built-in cloud&lt;/td&gt;
&lt;td&gt;iCloud (optional)&lt;/td&gt;
&lt;td&gt;DIY (file sync)&lt;/td&gt;
&lt;td&gt;iCloud&lt;/td&gt;
&lt;td&gt;iCloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team / shared groups&lt;/td&gt;
&lt;td&gt;✅ first-class&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ via git/yaml&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fillable forms&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ basic&lt;/td&gt;
&lt;td&gt;⚠️ basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS / scripted snippets&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ shell/yaml&lt;/td&gt;
&lt;td&gt;⚠️ AppleScript&lt;/td&gt;
&lt;td&gt;⚠️ AppleScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-keypress output&lt;/td&gt;
&lt;td&gt;⚠️ via OS hotkeys&lt;/td&gt;
&lt;td&gt;✅ native&lt;/td&gt;
&lt;td&gt;⚠️ via OS hotkeys&lt;/td&gt;
&lt;td&gt;⚠️ via OS hotkeys&lt;/td&gt;
&lt;td&gt;⚠️ via OS hotkeys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI prompt templates&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ 7 built-in&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App size&lt;/td&gt;
&lt;td&gt;~80 MB&lt;/td&gt;
&lt;td&gt;2.8 MB&lt;/td&gt;
&lt;td&gt;~10 MB&lt;/td&gt;
&lt;td&gt;~15 MB&lt;/td&gt;
&lt;td&gt;~6 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Store sandbox&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;⚠️ via Setapp&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few cells deserve commentary, because tables flatten nuance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"⚠️ via OS hotkeys"&lt;/strong&gt; means: yes, you can technically bind a snippet to a hotkey, but it goes through the system's general hotkey APIs, not a first-class one-keypress flow. In practice it works for a handful but isn't the design center.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Espanso's "team via git/yaml"&lt;/strong&gt; means: you can absolutely share snippets across a team, but you do it by committing yaml files to a git repo and asking everyone to clone it. Engineers love this. Nobody else does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TextExpander price&lt;/strong&gt; is the headline individual rate. Team and Enterprise plans are different.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where TextExpander still wins
&lt;/h2&gt;

&lt;p&gt;Skipping over the strengths of the incumbent makes a comparison post easy to dismiss. Here's where TextExpander earns its money, and where I'd tell you not to leave it:&lt;/p&gt;

&lt;h3&gt;
  
  
  Team libraries with permission scoping
&lt;/h3&gt;

&lt;p&gt;If your snippet library is "the canonical wording our team uses," and you have writers, support reps, or sales people who all need to stay aligned, no other tool on this list comes close. Espanso's git-based approach works for engineers; it does not work for a 50-person customer support team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fillable forms, well done
&lt;/h3&gt;

&lt;p&gt;TextExpander's snippet-with-variables flow (open a popup, fill in three fields, get formatted output) is genuinely well-designed. If you write semi-templated emails all day where each one needs a different name, date, and reference number, this is irreplaceable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-platform reality
&lt;/h3&gt;

&lt;p&gt;If your work day spans Mac, iOS, and Windows, TextExpander's sync across all three is the path of least resistance. Espanso runs everywhere too, but you'll be hand-syncing yaml files. A Mac-only tool like QUICOPY is a non-starter for this user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hundreds of snippets with searchable groups
&lt;/h3&gt;

&lt;p&gt;If you have 500 snippets across 12 categories and you actually use most of them, the abbreviation-trigger paradigm is the only one that scales. Camp B (one-keypress hotkeys) caps out somewhere around 30-50 before your fingers run out of memorable combinations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where QUICOPY genuinely doesn't fit
&lt;/h2&gt;

&lt;p&gt;I built QUICOPY, so it would be easy to write this section short. Resisting:&lt;/p&gt;

&lt;h3&gt;
  
  
  You have a large library
&lt;/h3&gt;

&lt;p&gt;If you're already at 100+ snippets in TextExpander and you use most of them, don't migrate to QUICOPY. The keyboard-shortcut paradigm runs out of physical key combinations before you do. You'll either stop using most of your snippets, or you'll start memorizing shortcut sequences, at which point you've reinvented abbreviations badly.&lt;/p&gt;

&lt;h3&gt;
  
  
  You need to share snippets with anyone
&lt;/h3&gt;

&lt;p&gt;QUICOPY has no team features. Two engineers, two writers, or two support reps can't share a snippet library. If that's your use case, you want TextExpander or you want to learn yaml and use Espanso.&lt;/p&gt;

&lt;h3&gt;
  
  
  You work outside macOS
&lt;/h3&gt;

&lt;p&gt;Mac only. There's no iOS app, no iPad app, no Windows version, no Linux build. If your work day touches anything beyond a Mac, this isn't the tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  You need fillable forms
&lt;/h3&gt;

&lt;p&gt;QUICOPY outputs static text. There's no popup-then-fill flow, no variable substitution at paste time. If you write semi-templated content where each instance needs different values, you'll feel the gap immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision tree
&lt;/h2&gt;

&lt;p&gt;Read the conditions top-to-bottom. The first one that matches is your answer. If multiple match, the higher one wins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─ Need to share snippets with a team
│  with permission scoping?
│  → TextExpander
│
├─ Work spans Mac + iOS + Windows?
│  → TextExpander
│
├─ Need fillable forms (variables at paste time)?
│  → TextExpander or Espanso
│
├─ Have 200+ snippets and use most of them?
│  → TextExpander or Espanso
│
├─ Linux user / cross-platform open source?
│  → Espanso
│
├─ Want one-keypress output for ~20 frequent texts?
│  → QUICOPY
│
├─ Already use Raycast or Alfred heavily?
│  → Their built-in snippet feature
│
├─ Mac only, &amp;lt;30 snippets, want cheapest paid option?
│  → aText
│
└─ Just want something free that works fine?
   → Espanso
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The biggest mistake I see in "alternative" articles is treating this as a single-axis ranking. There is no #1. Different rows of this tree go to different products and that's not me hedging — that's the actual answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing reality check
&lt;/h2&gt;

&lt;p&gt;Subscription fatigue is the most common reason people search for an alternative, so the math deserves a section.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Year 1&lt;/th&gt;
&lt;th&gt;Year 5&lt;/th&gt;
&lt;th&gt;Year 10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TextExpander&lt;/td&gt;
&lt;td&gt;~$40&lt;/td&gt;
&lt;td&gt;~$200&lt;/td&gt;
&lt;td&gt;~$400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QUICOPY (lifetime)&lt;/td&gt;
&lt;td&gt;$9.99&lt;/td&gt;
&lt;td&gt;$9.99&lt;/td&gt;
&lt;td&gt;$9.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QUICOPY (monthly)&lt;/td&gt;
&lt;td&gt;~$24&lt;/td&gt;
&lt;td&gt;~$120&lt;/td&gt;
&lt;td&gt;~$240&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Espanso&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rocket Typist&lt;/td&gt;
&lt;td&gt;~$20&lt;/td&gt;
&lt;td&gt;~$20&lt;/td&gt;
&lt;td&gt;~$20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aText&lt;/td&gt;
&lt;td&gt;~$5&lt;/td&gt;
&lt;td&gt;~$5&lt;/td&gt;
&lt;td&gt;~$5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two honest framings of this table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pro-subscription view&lt;/strong&gt;: $40/year buys ongoing development, cloud sync infrastructure, customer support, team features, and a working iOS app. If you're getting that value, the "lifetime $9.99" tools are not playing the same game. You're comparing a managed service to a one-time download.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro-lifetime view&lt;/strong&gt;: A snippet manager is a finished product, not an evolving service. After 10 years, your snippets are still snippets. If the tool you have today does what you need, paying $400 over a decade for the same capability feels excessive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both views are correct for different users. The wrong move is to optimize for the upfront sticker without thinking about whether you actually need the things subscriptions buy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration: how to leave TextExpander
&lt;/h2&gt;

&lt;p&gt;If you've decided to leave, here's the practical path. TextExpander exports its library; the question is what to do with the export.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Export from TextExpander
&lt;/h3&gt;

&lt;p&gt;TextExpander → Snippet menu → &lt;strong&gt;Export Snippets&lt;/strong&gt;. You'll get a &lt;code&gt;.textexpander&lt;/code&gt; bundle, which is really a folder of plist files. You can also "Export as JSON" from the same menu in recent versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Pick the destination based on volume
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Under 30 snippets you actually use frequently&lt;/strong&gt;: just type them into QUICOPY by hand. Sounds dumb; it's actually faster than building a converter, and it forces you to drop the dead snippets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;30-200 snippets&lt;/strong&gt;: import into Espanso. Espanso's yaml format is straightforward and there are community converters (search "textexpander to espanso").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;200+ snippets&lt;/strong&gt;: stay on TextExpander. The migration cost will exceed any savings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Don't migrate everything
&lt;/h3&gt;

&lt;p&gt;The hidden value of migration is that it's a forcing function for cleanup. Before importing, look at TextExpander's usage statistics (Settings → Statistics) and see which snippets you've actually expanded in the last 90 days. Most users discover that 70% of their library is dead. Don't migrate the dead ones.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Migration is a one-way door&lt;/strong&gt; if you cancel TextExpander before exporting. Export first, cancel after. The downloadable export remains usable indefinitely, but you can't get it back from a canceled account.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  One more honest thing
&lt;/h2&gt;

&lt;p&gt;There's a category of tool I deliberately didn't include above: the "AI assistant that lives in your menu bar and rewrites text on demand" category. Those are different products with overlapping use cases. If your real need is "ChatGPT but with one keystroke," you're looking at a different shelf in the store. QUICOPY's seven built-in AI prompt templates are a small step in that direction; tools like Spark, Bartender's AI integrations, and dedicated AI launchers go much further. They're not text expanders, and treating them as such mismatches the comparison.&lt;/p&gt;

&lt;p&gt;Pick the right shelf first. Then pick the right product on that shelf.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually use
&lt;/h2&gt;

&lt;p&gt;For full disclosure: I use QUICOPY for about 12 frequently-needed phrases (email replies, AI prompt scaffolds, two shell incantations I refuse to memorize). For the long tail of code-snippet-style text I rarely need, I use Raycast's snippet picker. I do not use TextExpander, but I have a friend who runs a 4-person customer support team on TextExpander's shared groups and I have never tried to talk her out of it.&lt;/p&gt;

&lt;p&gt;That's not advice. That's just the stack one developer settled into.&lt;/p&gt;




&lt;h2&gt;
  
  
  About QUICOPY
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://www.quicopy.com" rel="noopener noreferrer"&gt;QUICOPY&lt;/a&gt; — a macOS menu bar app that turns global shortcuts into instant text output, with 7 built-in AI prompt templates. It's on the &lt;a href="https://apps.apple.com/app/quicopy/id6761418490" rel="noopener noreferrer"&gt;Mac App Store&lt;/a&gt; for $9.99 lifetime or $1.99/month with a 7-day free trial.&lt;/p&gt;

&lt;p&gt;Have a use case I missed? Disagree with where I drew a line in the comparison? I read every email at &lt;code&gt;support@quicopy.com&lt;/code&gt;. Especially interested in hearing from heavy TextExpander users who tried to leave and came back, since that's a story this kind of post tends to underweight.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.quicopy.com/blog/textexpander-alternative-mac" rel="noopener noreferrer"&gt;quicopy.com&lt;/a&gt;. Related posts: &lt;a href="https://www.quicopy.com/blog/macos-shortcut-dispatch-zed" rel="noopener noreferrer"&gt;The macOS Global Shortcut That Won't Fire in Zed&lt;/a&gt; · &lt;a href="https://www.quicopy.com/blog/macos-sandbox-keyboard-shortcuts" rel="noopener noreferrer"&gt;macOS Sandbox and Keyboard Shortcuts&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>productivity</category>
      <category>tutorial</category>
      <category>comparison</category>
    </item>
    <item>
      <title>The macOS Global Shortcut That Won't Fire in Zed: When a 23-Year-Old Carbon API Meets a Self-Drawn Terminal</title>
      <dc:creator>QUICOPY</dc:creator>
      <pubDate>Tue, 21 Apr 2026 03:38:43 +0000</pubDate>
      <link>https://dev.to/quicopy/the-macos-global-shortcut-that-wont-fire-in-zed-when-a-23-year-old-carbon-api-meets-a-self-drawn-24o9</link>
      <guid>https://dev.to/quicopy/the-macos-global-shortcut-that-wont-fire-in-zed-when-a-23-year-old-carbon-api-meets-a-self-drawn-24o9</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;A user reported that &lt;a href="https://apps.apple.com/app/quicopy/id6761418490" rel="noopener noreferrer"&gt;QUICOPY&lt;/a&gt;'s global shortcut &lt;code&gt;⌘1&lt;/code&gt; fires in iTerm2, Ghostty, Warp, and even in Zed's editor — but &lt;strong&gt;silently does nothing inside Zed's built-in terminal&lt;/strong&gt;. Changing the key combo doesn't help. Restarting doesn't help.&lt;/p&gt;

&lt;p&gt;The cause is not a key conflict. It's an &lt;strong&gt;event dispatch architecture mismatch&lt;/strong&gt;: Carbon's &lt;code&gt;RegisterEventHotKey&lt;/code&gt; (the API almost every indie macOS app uses for global hotkeys) only fires when the frontmost app &lt;em&gt;doesn't&lt;/em&gt; consume the event. Zed's GPUI-based terminal consumes every &lt;code&gt;keyDown:&lt;/code&gt; it sees. Carbon never gets a chance.&lt;/p&gt;

&lt;p&gt;I originally planned a three-tier fallback (&lt;code&gt;CGEventTap&lt;/code&gt; → &lt;code&gt;NSEvent&lt;/code&gt; → &lt;code&gt;Carbon&lt;/code&gt;) to cover every regime. What I actually shipped for the Mac App Store is &lt;strong&gt;two tiers&lt;/strong&gt; (&lt;code&gt;NSEvent&lt;/code&gt; + &lt;code&gt;Carbon&lt;/code&gt;) plus a separate paste-strategy rewrite I didn't see coming. This post is the design log for both.&lt;/p&gt;

&lt;p&gt;If you ship a macOS app with global shortcuts in 2026 and your users live in Zed, VS Code, or any Electron/Tauri/Flutter app that self-draws its text UI — you're going to hit this. Here's the full autopsy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Correction from my last post
&lt;/h2&gt;

&lt;p&gt;In my &lt;a href="https://www.quicopy.com/blog/macos-sandbox-keyboard-shortcuts.html" rel="noopener noreferrer"&gt;previous post&lt;/a&gt; I wrote that QUICOPY uses &lt;code&gt;CGEvent.tapCreate&lt;/code&gt; to capture global shortcuts. That was a lie — or more precisely, what I had originally prototyped but never shipped. The released build uses &lt;strong&gt;Carbon &lt;code&gt;RegisterEventHotKey&lt;/code&gt;&lt;/strong&gt;, because CGEventTap requires an Accessibility permission prompt I wanted to avoid, and Carbon works out of the box in a sandboxed app with zero entitlements.&lt;/p&gt;

&lt;p&gt;That decision is exactly what broke in Zed. This post is the correction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bug Report
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"I pressed ⌘1 in Zed's terminal. Nothing happens. Menu bar icon doesn't flash. No text inserted."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Compatibility matrix after reproducing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host app&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;⌘1&lt;/code&gt; fires?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;iTerm2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghostty&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warp&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zed — &lt;strong&gt;editor&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zed — &lt;strong&gt;built-in terminal&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code — editor&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code — &lt;strong&gt;built-in terminal&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome / Safari&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The failure is not about Zed the application. It's specifically about the &lt;em&gt;terminal view inside Zed&lt;/em&gt;. That's a strange enough shape to rule out the usual suspects immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrong Hypothesis #1: "Zed stole the key"
&lt;/h2&gt;

&lt;p&gt;First instinct: Zed has its own &lt;code&gt;⌘1&lt;/code&gt; binding (switch to tab 1, or switch pane), and it's grabbing the key before the OS routes it to anything else.&lt;/p&gt;

&lt;p&gt;I unbound Zed's &lt;code&gt;⌘1&lt;/code&gt; and tried again. Still broken.&lt;br&gt;
I switched QUICOPY's shortcut to &lt;code&gt;⌘⌃F9&lt;/code&gt; — a combination no sane app would ever claim. Still broken.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you change the key and the bug stays, you're not looking at a key conflict. You're looking at &lt;strong&gt;something upstream of key identity&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Wrong Hypothesis #2: "My handler isn't registered"
&lt;/h2&gt;

&lt;p&gt;Second instinct: maybe Carbon failed to install the hotkey when Zed became frontmost. Some shell-out race condition.&lt;/p&gt;

&lt;p&gt;Logs from &lt;code&gt;HotkeyManager&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HotkeyManager] Started with 7 hot keys registered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All seven mappings register cleanly. But the Carbon callback — the function that Carbon is supposed to invoke when my hotkey matches — is &lt;strong&gt;never called&lt;/strong&gt; when I press &lt;code&gt;⌘1&lt;/code&gt; inside Zed's terminal.&lt;/p&gt;

&lt;p&gt;Which means the problem isn't in my code. It's in the pipe &lt;em&gt;before&lt;/em&gt; my code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Culprit: How Carbon Actually Dispatches
&lt;/h2&gt;

&lt;p&gt;Carbon's event model is documented, but the documentation buries the critical detail. Here's the dispatch order for a &lt;code&gt;keyDown&lt;/code&gt; event on modern macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────────────┐
│ Hardware → WindowServer → NSEvent     │
└──────────────────┬────────────────────┘
                   ▼
         ┌──────────────────┐
         │ Frontmost App    │  NSApplication.sendEvent:
         │ (AppKit)         │  → NSWindow.sendEvent:
         └────────┬─────────┘  → NSResponder chain
                  │
          ┌───────┴────────┐
          │                │
    event consumed   event not consumed
    (returns YES)    (returns NO / unhandled)
          │                │
          ▼                ▼
      STOP            ┌────────────────────┐
                      │ Carbon Event Mgr   │
                      │ RegisterEventHotKey│ ← your callback fires here
                      └────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Carbon's hotkey dispatch runs &lt;strong&gt;after&lt;/strong&gt; the frontmost app has had a chance to handle the event, and &lt;strong&gt;only&lt;/strong&gt; if the app reports "unhandled." This design comes from Carbon's original purpose: application-level shortcut registration for the &lt;em&gt;frontmost&lt;/em&gt; app, extended over time to become a pseudo-global mechanism by registering at the process-group level.&lt;/p&gt;

&lt;p&gt;For a traditional AppKit app, the responder chain dutifully returns "unhandled" for any key that doesn't match a menu item or an active text field. Carbon fires. All is well.&lt;/p&gt;

&lt;p&gt;For an app whose text area is an &lt;code&gt;NSTextView&lt;/code&gt; — iTerm2, Terminal.app, TextEdit — same story. &lt;code&gt;NSTextView&lt;/code&gt; gracefully forwards keys it doesn't recognize.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But Zed's editor and terminal are not &lt;code&gt;NSTextView&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Zed's Terminal Specifically: GPUI + Unconditional Consumption
&lt;/h2&gt;

&lt;p&gt;Zed is built on &lt;a href="https://www.gpui.rs/" rel="noopener noreferrer"&gt;GPUI&lt;/a&gt;, a Rust UI framework that renders its entire interface with Metal shaders. There are no &lt;code&gt;NSTextView&lt;/code&gt; instances inside Zed's main content area — just a single &lt;code&gt;NSView&lt;/code&gt; that captures raw input and dispatches it to GPUI's own keyboard handling.&lt;/p&gt;

&lt;p&gt;Zed's terminal view uses a class called &lt;code&gt;TerminalInputHandler&lt;/code&gt; (you can see traces of this in the open-source repo and in related bug reports: &lt;a href="https://github.com/zed-industries/zed/issues/15517" rel="noopener noreferrer"&gt;Zed #15517&lt;/a&gt;, &lt;a href="https://github.com/zed-industries/zed/issues/16187" rel="noopener noreferrer"&gt;Zed #16187&lt;/a&gt;). For every &lt;code&gt;keyDown:&lt;/code&gt; event, &lt;code&gt;TerminalInputHandler&lt;/code&gt; does one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the key matches a registered Zed binding → run the Zed action, &lt;strong&gt;return handled&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;If nothing matches → forward the keystroke to the shell process (PTY write), &lt;strong&gt;return handled&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note the important word in both branches: &lt;strong&gt;handled&lt;/strong&gt;. Zed's terminal never returns "unhandled" — because from its perspective, there's no such thing as an unhandled keystroke. Either it's a command or it goes to the shell.&lt;/p&gt;

&lt;p&gt;This is perfectly correct behavior for a terminal. A terminal's whole job is to eat keystrokes. But it means Carbon upstream sees "app consumed the event" and refuses to fire any registered hotkey.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Changing the shortcut combo changes nothing. The event was consumed before Carbon was even consulted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not a Zed bug. VS Code's integrated terminal, which uses xterm.js inside an Electron &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, has the same behavior. Any modern self-drawn UI framework — Flutter macOS, Tauri with custom keyboard handling, JetBrains IDEs in certain modes — can reproduce this. Carbon's assumption that "the frontmost app will pass unknown keys through" has quietly rotted as native text views have been replaced by self-drawn ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Planned: A Three-Tier Auto Fallback
&lt;/h2&gt;

&lt;p&gt;Three APIs can observe keyboard events system-wide. The tradeoffs are what make this interesting:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Intercept point&lt;/th&gt;
&lt;th&gt;Fires even when app consumes&lt;/th&gt;
&lt;th&gt;Sandbox-friendly&lt;/th&gt;
&lt;th&gt;Permission&lt;/th&gt;
&lt;th&gt;Can swallow event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;RegisterEventHotKey&lt;/code&gt; (Carbon)&lt;/td&gt;
&lt;td&gt;After frontmost app&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CGEvent.tapCreate&lt;/code&gt; (&lt;code&gt;.cgSessionEventTap&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Before frontmost app&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;❌ in MAS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NSEvent.addGlobalMonitorForEvents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Outside frontmost app&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two columns that matter: &lt;strong&gt;Fires even when app consumes&lt;/strong&gt; and &lt;strong&gt;Can swallow event&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Carbon loses the first. That's our bug.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CGEventTap&lt;/code&gt; wins both — but Apple does not allow it in Mac App Store sandboxed apps. (Officially, &lt;code&gt;com.apple.security.app-sandbox = true&lt;/code&gt; + event tap creation returns nil. Unofficially, confirmed across a half-decade of developer forum posts.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NSEvent&lt;/code&gt; global monitor wins the first column and runs fine under sandbox, but &lt;strong&gt;cannot swallow events&lt;/strong&gt;. So if you bind &lt;code&gt;⌘1&lt;/code&gt; and the user is in Chrome, they'll both switch to the first tab &lt;em&gt;and&lt;/em&gt; fire your shortcut. That's a bad experience for a text expansion tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No single backend wins. My original plan was to run all three behind a single protocol and pick at startup based on runtime conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="kt"&gt;HotkeyMonitoring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AnyObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isRecordingSuspended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;onHotkeyTriggered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;KeyMapping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;KeyMapping&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;   &lt;span class="c1"&gt;// return false → caller falls through&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;updateMappings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;KeyMapping&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three implementations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;HotkeyManager&lt;/code&gt; — the existing Carbon code, untouched&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EventTapHotkeyManager&lt;/code&gt; — new, for non-sandbox + AX-authorized&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NSEventHotkeyManager&lt;/code&gt; — new, for sandbox-safe fallback&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Selection logic at launch and whenever Accessibility permission flips:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;chooseBackend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;any&lt;/span&gt; &lt;span class="kt"&gt;HotkeyMonitoring&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;sandboxed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SandboxInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isSandboxed&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;axGranted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;permissionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isAccessibilityGranted&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;sandboxed&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;axGranted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;EventTapHotkeyManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;// best: Zed-compatible + swallows&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;axGranted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;NSEventHotkeyManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;// sandbox fallback: Zed-compatible&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;HotkeyManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                     &lt;span class="c1"&gt;// zero-permission baseline&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And subscribe to the permission flip so the user upgrades automatically once they grant Accessibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;permissionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;$isAccessibilityGranted&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeDuplicates&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropFirst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hotSwapBackend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cancellables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To detect the sandbox at runtime (rather than &lt;code&gt;#if SANDBOXED&lt;/code&gt;), I planned to read the app's own entitlements via &lt;code&gt;Security.framework&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;SandboxInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;isSandboxed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SecTaskCreateFromSelf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SecTaskCopyValueForEntitlement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"com.apple.security.app-sandbox"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="kc"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also sketched out the CGEventTap edge cases I'd have to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.tapDisabledByTimeout&lt;/code&gt; / &lt;code&gt;.tapDisabledByUserInput&lt;/code&gt; → re-enable, circuit-break after 3 consecutive failures, fall back to NSEvent&lt;/li&gt;
&lt;li&gt;Fresh AX grant where &lt;code&gt;tapCreate&lt;/code&gt; returns &lt;code&gt;nil&lt;/code&gt; → treat as soft failure, fall back, show a one-time toast asking for restart&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@MainActor&lt;/code&gt; isolation for the C callback — planned to be nasty&lt;/li&gt;
&lt;li&gt;A generation token to reject stale callbacks after a backend swap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was the plan. It's a satisfying bit of architecture on paper. &lt;strong&gt;It's not what I shipped.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Shipped: Two Tiers and a Paste Rewrite
&lt;/h2&gt;

&lt;p&gt;QUICOPY ships &lt;strong&gt;only on the Mac App Store&lt;/strong&gt;. The sandbox makes &lt;code&gt;CGEventTap&lt;/code&gt; permanently unavailable for this target. Rather than maintain a tier of code that's statically unreachable in my only distribution channel, I collapsed the architecture to what actually runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="kt"&gt;HotkeyMonitoring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AnyObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isRecordingSuspended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;onHotkeyTriggered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;KeyMapping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;KeyMapping&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;updateMappings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;KeyMapping&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two implementations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;NSEventHotkeyManager&lt;/code&gt; — when Accessibility is granted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HotkeyManager&lt;/code&gt; — Carbon, when it isn't&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Selection logic collapses to a single &lt;code&gt;if&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;chooseAndStartMonitor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;dispatchPrecondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;suspended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activeMonitor&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isRecordingSuspended&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;activeMonitor&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;authed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;permissionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isAccessibilityGranted&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;any&lt;/span&gt; &lt;span class="kt"&gt;HotkeyMonitoring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authed&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kt"&gt;NSEventHotkeyManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;HotkeyManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;installCallbacks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isRecordingSuspended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;suspended&lt;/span&gt;
    &lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mappingStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;activeMonitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;monitor&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;start() -&amp;gt; Bool&lt;/code&gt;. No factory array. No &lt;code&gt;assertionFailure&lt;/code&gt; hard-floor. No generation tokens. The code reads exactly as the design is: two states, one switch.&lt;/p&gt;

&lt;p&gt;I still subscribe to the permission flip so the user upgrades automatically once they grant Accessibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;permissionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;$isAccessibilityGranted&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeDuplicates&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropFirst&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chooseAndStartMonitor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cancellables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;End state: grant AX → &lt;code&gt;NSEventHotkeyManager&lt;/code&gt; starts → Zed's terminal receives &lt;code&gt;⌘1&lt;/code&gt; events via the global monitor → QUICOPY fires its mapping. Revoke AX → &lt;code&gt;chooseAndStartMonitor&lt;/code&gt; re-runs → Carbon comes back as the fallback → Zed's terminal stops responding (expected).&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;trade-off&lt;/strong&gt; you're making by shipping only NSEvent + Carbon on sandboxed macOS: when the user is in Chrome and hits &lt;code&gt;⌘1&lt;/code&gt;, Chrome still switches to its first tab &lt;em&gt;and&lt;/em&gt; QUICOPY inserts its text. No backend can swallow the event in the sandbox. In practice users learn to avoid &lt;code&gt;⌘1..9&lt;/code&gt; for bindings that collide with browser tab shortcuts, and the in-app permission screen suggests &lt;code&gt;⌘⌃1..9&lt;/code&gt; or &lt;code&gt;⌘⌥1..9&lt;/code&gt; combos instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Surprise: Carbon Is Not the Only Thing That Broke in Zed
&lt;/h2&gt;

&lt;p&gt;Getting the event to fire was half the fight. Getting text to actually appear in Zed's terminal was the other half — and this part isn't in any original design doc, because it only surfaced during the regression pass.&lt;/p&gt;

&lt;p&gt;QUICOPY's text-output strategy on the MAS side has always been: write to the pasteboard → trigger &lt;code&gt;Edit → Paste&lt;/code&gt; via AppleScript &lt;code&gt;click menu item "Paste" of menu 1 of menu bar item "Edit"&lt;/code&gt;. Menu click goes through AppKit's first-responder chain, doesn't depend on physical modifier keys, and is immune to IME state. It works beautifully in every &lt;code&gt;NSTextView&lt;/code&gt;-based app.&lt;/p&gt;

&lt;p&gt;In Zed's terminal, something very strange happens instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;⌘1&lt;/code&gt; fires (good — NSEvent monitor is working)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;onHotkeyTriggered&lt;/code&gt; callback runs (good)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TextOutputManager.output&lt;/code&gt; writes "hello world" to the pasteboard and runs the AppleScript&lt;/li&gt;
&lt;li&gt;The AppleScript &lt;strong&gt;returns without error&lt;/strong&gt; — my code logs &lt;code&gt;paste path=menu_en&lt;/code&gt;, marks it as autoPasted, and celebrates&lt;/li&gt;
&lt;li&gt;Nothing appears in the terminal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No error. No failure. The AppleScript says it clicked the Paste menu item, the menu item exists in Zed's &lt;code&gt;Edit&lt;/code&gt; menu, and the click is reported as successful. &lt;strong&gt;But the paste doesn't actually happen.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The autoPasted Illusion
&lt;/h3&gt;

&lt;p&gt;This is a deeply subtle bug. The old &lt;code&gt;TextOutputManager&lt;/code&gt; judged "paste succeeded" purely by whether the AppleScript threw an exception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeAndReturnError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clipboardOnly&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;autoPasted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But AppleScript's &lt;code&gt;click menu item&lt;/code&gt; on a disabled-looking-but-technically-present menu item in a non-AppKit host returns &lt;strong&gt;no error&lt;/strong&gt; and performs &lt;strong&gt;no visible action&lt;/strong&gt;. Zed's &lt;code&gt;Edit → Paste&lt;/code&gt; menu item is routed to whichever buffer GPUI considers focused — and when that focused buffer is the built-in terminal, the menu-click route apparently doesn't reach the terminal's input handler.&lt;/p&gt;

&lt;p&gt;So the fix: stop trusting "AppleScript didn't throw" as a proxy for "paste happened." Ask AppleScript to check the menu item's &lt;code&gt;enabled&lt;/code&gt; state explicitly, and fall back to synthesizing the keystroke directly when the menu route looks unreliable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;frontProcess&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;editMenu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Paste"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;editMenu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Paste"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;editMenu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nv"&gt;click&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Paste"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;editMenu&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nb"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"menu_en"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;editMenuZh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"编辑"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"粘贴"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;editMenuZh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"粘贴"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;editMenuZh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nv"&gt;click&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;menu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"粘贴"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;editMenuZh&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nb"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"menu_zh"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;keystroke&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;using&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;down&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;-- fallback&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nb"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keystroke"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two useful wins from this structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The three-way return value is a live diagnostic signal.&lt;/strong&gt; In production logs I now see &lt;code&gt;paste path=menu_en&lt;/code&gt; for iTerm2, &lt;code&gt;paste path=menu_zh&lt;/code&gt; for localized apps, and &lt;code&gt;paste path=keystroke&lt;/code&gt; for Zed, VS Code, and anything else self-drawn. When users report "paste doesn't work," the first question I ask is what path they're on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;keystroke is a real fallback, not a silent substitute.&lt;/strong&gt; Menu click still wins by default for every normal AppKit app (IME-safe, modifier-key-safe), and keystroke kicks in only when the menu route is genuinely missing or disabled.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Self-Paste Trap (a.k.a. error 1002)
&lt;/h3&gt;

&lt;p&gt;While I was testing the paste rewrite, I hit another class of bug: what happens when the user presses a bound shortcut while QUICOPY's own popover is in the foreground?&lt;/p&gt;

&lt;p&gt;Old behavior (Carbon): Carbon fires the callback, the callback writes to the pasteboard, AppleScript tries to keystroke &lt;code&gt;⌘V&lt;/code&gt; into the frontmost process — which &lt;em&gt;is QUICOPY itself&lt;/em&gt;. macOS Accessibility policy then says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;"QUICOPY" is not allowed to send keystrokes.&lt;/code&gt; (Error 1002)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No app is allowed to synthesize keystrokes into itself through System Events. The paste silently fails. The user sees a stray "Copied! Press ⌘V to paste" toast they never asked for. It looks broken.&lt;/p&gt;

&lt;p&gt;This bug has been in the Carbon code &lt;strong&gt;since day one&lt;/strong&gt;. It's just that nobody ever deliberately sits in QUICOPY's own popover and hits a bound shortcut. I found it because the regression matrix includes that exact case.&lt;/p&gt;

&lt;p&gt;I fixed it in two places, because Carbon and NSEvent reach the self-paste code path through different doors:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NSEvent local monitor&lt;/strong&gt; intercepts key events that are dispatched inside QUICOPY. Instead of firing the mapping and letting it fail downstream, I just swallow the event at the monitor layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handleLocalKeyDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;NSEvent&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isRecordingSuspended&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;matchMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Popover is frontmost; don't trigger, don't beep, don't paste to self&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;AppDelegate.handleTriggered&lt;/code&gt;&lt;/strong&gt; is the universal junction the Carbon callback also goes through. I added a frontmost check there as a second layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handleTriggered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;KeyMapping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;frontBundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSWorkspace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frontmostApplication&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bundleIdentifier&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;ownBundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bundleIdentifier&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;popoverShown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;popover&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isShown&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;frontBundle&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ownBundle&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;popoverShown&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;   &lt;span class="c1"&gt;// self-paste would trigger error 1002, abort&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... proceed to paste ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;popoverShown&lt;/code&gt; check is load-bearing. &lt;code&gt;NSWorkspace.frontmostApplication&lt;/code&gt; has an interesting property: when a menu-bar popover is displayed, &lt;code&gt;frontmostApplication&lt;/code&gt; still returns &lt;strong&gt;whatever app was frontmost before the popover opened&lt;/strong&gt;, not QUICOPY itself. I discovered this the hard way when the first version of my defense only checked &lt;code&gt;frontBundle == ownBundle&lt;/code&gt; and still let the popover case through. Reading &lt;code&gt;popover.isShown&lt;/code&gt; is the reliable way to detect "QUICOPY's UI is currently on screen."&lt;/p&gt;

&lt;p&gt;Neither of those checks is elegant. But together they cover both Carbon (which doesn't distinguish foreground/background) and NSEvent (which does), and neither backend now produces the stray &lt;code&gt;error 1002 → clipboardOnly → toast&lt;/code&gt; cascade.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Shortcut Recorder Problem (And Why It Almost Bit Me)
&lt;/h2&gt;

&lt;p&gt;QUICOPY has a "record a shortcut" UI, implemented with &lt;code&gt;NSEvent.addLocalMonitorForEvents(matching: .keyDown)&lt;/code&gt; to capture whatever combo the user presses. Perfectly fine in isolation.&lt;/p&gt;

&lt;p&gt;But the new &lt;code&gt;NSEventHotkeyManager&lt;/code&gt; also uses &lt;code&gt;addLocalMonitorForEvents&lt;/code&gt; to handle self-triggered shortcuts when the popover is frontmost. Two local monitors, same app, racing to consume the same &lt;code&gt;keyDown&lt;/code&gt;. One of them wins, which one depends on registration order and call site — and Apple's docs don't guarantee ordering.&lt;/p&gt;

&lt;p&gt;The fix is a shared &lt;code&gt;isRecordingSuspended: Bool&lt;/code&gt; flag on the monitor. When the user opens the mapping edit screen and taps "record shortcut," &lt;code&gt;MappingEditView.onChange(of: isRecordingShortcut)&lt;/code&gt; flips the flag on via a closure provider. The hotkey monitor's local callback short-circuits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In NSEventHotkeyManager's local monitor:&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isRecordingSuspended&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// pass through untouched&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;matchMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;                              &lt;span class="c1"&gt;// popover self-paste — swallow, don't trigger&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;                                &lt;span class="c1"&gt;// unrelated key — forward&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order in which the two local monitors fire no longer matters. If the shortcut recorder fires first, it captures the key and returns &lt;code&gt;nil&lt;/code&gt;; my monitor never sees it. If mine fires first, the &lt;code&gt;isRecordingSuspended&lt;/code&gt; check passes the event straight through and the recorder captures it. Either way, the user's recorded shortcut ends up in the recorder, not triggering a mapping.&lt;/p&gt;

&lt;p&gt;The closure-provider pattern is worth flagging: I initially passed &lt;code&gt;HotkeyMonitoring&lt;/code&gt; instances directly into &lt;code&gt;MappingEditView&lt;/code&gt;. That worked until the backend swapped mid-edit (AX permission flipped while the edit screen was open), and the edit screen kept writing &lt;code&gt;isRecordingSuspended&lt;/code&gt; to the old, now-stopped monitor. Passing a closure that resolves &lt;code&gt;appDelegate.activeMonitor&lt;/code&gt; on every call instead of a captured instance reference made the whole chain swap-safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Narrowed the Design for a Mac App Store-only Release
&lt;/h2&gt;

&lt;p&gt;Three tiers on paper was intellectually satisfying. Two tiers in production is what QUICOPY actually runs. Here's why the simpler thing is the right thing for my distribution:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraint 1: MAS sandbox rejects CGEventTap.&lt;/strong&gt;&lt;br&gt;
Apple's Developer Forums have confirmed this across several macOS releases. There's no entitlement to request, no review workaround, no "just ask for Input Monitoring." &lt;code&gt;CGEvent.tapCreate&lt;/code&gt; under &lt;code&gt;com.apple.security.app-sandbox = true&lt;/code&gt; returns &lt;code&gt;nil&lt;/code&gt;. If I shipped the three-tier code, the CGEventTap branch would be unreachable in every App Store build I'd ever produce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraint 2: I'm not shipping a Developer ID notarized build.&lt;/strong&gt;&lt;br&gt;
Originally I'd kept the Developer ID target as an option for users who wanted the "best" experience — event-swallowing included. Maintaining two distribution channels for a solo-developer indie app, with different permission flows, different crash-report pipelines, different update mechanisms, and a marketing story that has to explain which version to download, turned out to be more work than the feature was worth. I scoped down to MAS-only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence: The entire CGEventTap branch is orphan code.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;EventTapHotkeyManager&lt;/code&gt;, &lt;code&gt;SandboxInfo&lt;/code&gt; runtime detection, the circuit-breaker, the generation token — all of it exists to serve a tier that will never run. Carrying it adds maintenance cost (tests, comments that reference non-existent types, code reviewers asking "why is there a fallback that can't trigger?") without buying anything.&lt;/p&gt;

&lt;p&gt;Shipping the two-tier version in full means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HotkeyMonitoring.start&lt;/code&gt; returns &lt;code&gt;Void&lt;/code&gt;, not &lt;code&gt;Bool&lt;/code&gt;. It can't fail-and-fall-through because there's nothing to fall through to.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chooseAndStartMonitor&lt;/code&gt; is a single &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;else&lt;/code&gt;, not a factory array with a for loop.&lt;/li&gt;
&lt;li&gt;Readers of the code see exactly two backends and one branch. The structure matches the runtime reality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I ever do re-introduce a Developer ID channel, I'll add the CGEventTap tier back then — along with the test matrix, the release pipeline, and the marketing copy that all go with it. Reintroducing a protocol member in Swift is a 10-minute mechanical change; scoping the whole effort correctly is the harder work. Keeping placeholder scaffolding around today doesn't reduce that cost materially.&lt;/p&gt;

&lt;p&gt;The word I'd avoid here is "killed." Nothing was killed. The scope was narrowed to the distribution I actually ship, and the code now reflects that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Boring Problem of Entitlements
&lt;/h2&gt;

&lt;p&gt;People get scared of new entitlements because they're scared of App Review.&lt;/p&gt;

&lt;p&gt;For this migration, the entitlements file does not change at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;com.apple.security.app-sandbox&lt;/code&gt; — already set for MAS target&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;com.apple.security.automation.apple-events&lt;/code&gt; — already set for pasting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;NSAccessibilityUsageDescription&lt;/code&gt; is &lt;strong&gt;not required&lt;/strong&gt; for QUICOPY today. &lt;code&gt;AXIsProcessTrustedWithOptions(nil)&lt;/code&gt; performs a silent check that doesn't trigger the system permission prompt, and my UI routes the user to System Settings directly when they opt into the Zed-compatible mode. If your app calls any of the prompting variants of that API, you'll want to supply the usage string — but QUICOPY never does.&lt;/p&gt;

&lt;p&gt;Hardened Runtime is already on (&lt;code&gt;ENABLE_HARDENED_RUNTIME = YES&lt;/code&gt;) and requires no additional &lt;code&gt;com.apple.security.cs.*&lt;/code&gt; exceptions for &lt;code&gt;NSEvent&lt;/code&gt; monitors. I'd been paranoid about that for weeks before actually testing it. No exception needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Final Flow Looks Like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User launches QUICOPY
       │
       ▼
PermissionManager probes AX via AXIsProcessTrustedWithOptions(nil)
       │
       ▼
AppDelegate.chooseAndStartMonitor()
       │
       ├─ AX granted     → NSEventHotkeyManager starts
       └─ AX not granted → HotkeyManager starts (Carbon legacy)
       │
       ▼
User grants AX later → applicationDidBecomeActive → probeAccessibility() → $isAccessibilityGranted fires → chooseAndStartMonitor() re-runs → swap to NSEvent
       │
       ▼
User presses ⌘1 inside Zed's terminal
       │
       ▼ (with NSEvent backend)
Event captured via addGlobalMonitorForEvents BEFORE Zed's TerminalInputHandler sees it
       │
       ▼
KeyMapping matched → AppDelegate.handleTriggered
       │
       ▼
TextOutputManager writes text to pasteboard, then AppleScript:
  • Checks Edit→Paste enabled state
  • enabled → click menu item "Paste"
  • disabled / absent → fallback to keystroke ⌘V
       │
       ▼
Text appears at the cursor inside Zed's terminal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bottleneck in the chain is the clipboard → AppleScript round-trip. Hotkey capture itself is negligible on any backend.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Tell You If You're Building Something Similar
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't start with Carbon.&lt;/strong&gt; It's tempting because it's the zero-permission path. But in 2026, "zero permission" means "silently broken in a growing list of self-drawn apps." Budget an AX permission request into your onboarding from day one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test in Zed, VS Code, and at least one Electron/Tauri app.&lt;/strong&gt; &lt;code&gt;NSTextView&lt;/code&gt;-based apps will always work. Self-drawn UIs are the canary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick your distribution channel before you pick your hotkey API.&lt;/strong&gt; MAS-only means you can't use CGEventTap, full stop. Developer ID gives you that option but costs you a separate release pipeline. Decide first, architect second.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Subscribe to AX permission changes.&lt;/strong&gt; &lt;code&gt;applicationDidBecomeActive&lt;/code&gt; + &lt;code&gt;AXIsProcessTrustedWithOptions(nil)&lt;/code&gt; is the closest macOS gives you to a "permission flipped" signal. There's no proper notification API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't trust "AppleScript didn't throw" as "paste succeeded."&lt;/strong&gt; In self-drawn hosts, &lt;code&gt;click menu item&lt;/code&gt; can claim success on a menu that doesn't actually route to the focused view. Check &lt;code&gt;enabled&lt;/code&gt; explicitly and keep a &lt;code&gt;keystroke&lt;/code&gt; fallback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Two local monitors on the same &lt;code&gt;keyDown&lt;/code&gt; is a race.&lt;/strong&gt; Use a shared suspended flag, not priority ordering. The recorder UI and the hotkey monitor cannot both assume they go first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check &lt;code&gt;popover.isShown&lt;/code&gt; when defending against self-paste.&lt;/strong&gt; &lt;code&gt;NSWorkspace.frontmostApplication&lt;/code&gt; won't tell you that a menu-bar popover is currently visible; it still reports the app underneath.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Meta: Two Posts, Two Sides of the Same Wall
&lt;/h2&gt;

&lt;p&gt;If you read &lt;a href="https://www.quicopy.com/blog/macos-sandbox-keyboard-shortcuts.html" rel="noopener noreferrer"&gt;the previous post&lt;/a&gt;, you have the full story now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output side&lt;/strong&gt;: &lt;code&gt;CGEvent.post()&lt;/code&gt; is blocked by sandbox. Workaround: clipboard + AppleScript &lt;code&gt;System Events&lt;/code&gt;, with menu-enabled checks for self-drawn hosts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input side&lt;/strong&gt;: Carbon &lt;code&gt;RegisterEventHotKey&lt;/code&gt; is blocked by self-drawn frontmost apps. Workaround: upgrade to &lt;code&gt;NSEvent&lt;/code&gt; global monitor whenever Accessibility is granted; keep Carbon as the zero-permission fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both problems stem from the same underlying fact: macOS's global-shortcut APIs were designed when the frontmost app was always an AppKit app with an &lt;code&gt;NSTextView&lt;/code&gt; in it, and always happy to let the OS see events it didn't care about. That world is gone. Zed, VS Code, Warp's Rust UI, Flutter desktop apps, every Electron app with a canvas text surface — they all break the old assumptions, each in their own way.&lt;/p&gt;

&lt;p&gt;What you ship for this class of problem is not a single API choice. It's an &lt;em&gt;adaptive&lt;/em&gt; architecture that picks the right tool for the permission and distribution channel you're actually in.&lt;/p&gt;




&lt;h2&gt;
  
  
  About QUICOPY
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://www.quicopy.com" rel="noopener noreferrer"&gt;QUICOPY&lt;/a&gt; — a macOS menu bar app that turns global shortcuts into instant text output, with 7 built-in AI prompt templates. It's on the &lt;a href="https://apps.apple.com/app/quicopy/id6761418490" rel="noopener noreferrer"&gt;Mac App Store&lt;/a&gt; for $9.99 lifetime or $1.99/month with a 7-day free trial.&lt;/p&gt;

&lt;p&gt;If you want to see how all the pieces in this post ship in a real app, grab the binary.&lt;/p&gt;

&lt;p&gt;Questions, corrections, or better approaches? Especially interested in hearing from anyone building non-AppKit macOS apps who has opinions on whether Carbon should finally be retired, or whether GPUI / Flutter should be patched to forward unhandled keys upstream. I don't think either fix is coming. So here we are.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://www.quicopy.com" rel="noopener noreferrer"&gt;quicopy.com&lt;/a&gt;. Follow me for more macOS indie dev notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>zed</category>
      <category>indie</category>
    </item>
    <item>
      <title>Shipping Global Keyboard Shortcuts on macOS Sandbox: The Part Apple Doesn't Document</title>
      <dc:creator>QUICOPY</dc:creator>
      <pubDate>Sat, 18 Apr 2026 14:49:56 +0000</pubDate>
      <link>https://dev.to/quicopy/shipping-global-keyboard-shortcuts-on-macos-sandbox-the-part-apple-doesnt-document-57no</link>
      <guid>https://dev.to/quicopy/shipping-global-keyboard-shortcuts-on-macos-sandbox-the-part-apple-doesnt-document-57no</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I shipped a macOS menu bar app (&lt;a href="https://apps.apple.com/app/quicopy/id6761418490" rel="noopener noreferrer"&gt;QUICOPY&lt;/a&gt;) to the Mac App Store. Its core feature is &lt;strong&gt;global keyboard shortcuts that output text in any app&lt;/strong&gt;. The obvious implementation — &lt;code&gt;CGEvent.post()&lt;/code&gt; — is silently blocked by App Sandbox. Apple does not document this clearly. Here's what actually works, and why.&lt;/p&gt;

&lt;p&gt;If you're building anything that simulates keyboard input on macOS and targeting the App Store, this will save you a week.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I wanted a menu bar app where pressing &lt;code&gt;⌘⇧1&lt;/code&gt; in any application types a piece of pre-set text at the cursor. TextExpander-style. Simple, right?&lt;/p&gt;

&lt;p&gt;Two subproblems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture&lt;/strong&gt; a global keyboard shortcut (even when my app is not focused)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output&lt;/strong&gt; text into whatever app is focused&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both sound like 10-line solutions. Neither is, if you want to be on the Mac App Store.&lt;/p&gt;




&lt;h2&gt;
  
  
  Capturing Global Shortcuts: CGEvent Tap
&lt;/h2&gt;

&lt;p&gt;This part is mostly fine. Use &lt;code&gt;CGEvent.tapCreate&lt;/code&gt; with &lt;code&gt;.cgSessionEventTap&lt;/code&gt; and listen for &lt;code&gt;.keyDown&lt;/code&gt; events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;eventMask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="kt"&gt;CGEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyDown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tapCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cgSessionEventTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headInsertEventTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;eventsOfInterest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGEventMask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventMask&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="c1"&gt;// Inspect event.flags and keycode here&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;passUnretained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nv"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sandbox gotcha:&lt;/strong&gt; &lt;code&gt;CGEvent.tapCreate&lt;/code&gt; requires the user to grant your app &lt;strong&gt;Input Monitoring&lt;/strong&gt; (or in some cases Accessibility) permission. This is fine — you get a permission prompt, user approves, done.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;This part works in a sandboxed Mac App Store app.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; monitor events at the session level as long as the user grants the permission. The permission is called "Input Monitoring" in macOS 10.15+ and it specifically covers event taps.&lt;/p&gt;

&lt;p&gt;No entitlement file magic needed for input monitoring — the sandbox allows the system permission prompt to handle it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Outputting Text: The Approach That Everyone Tries First
&lt;/h2&gt;

&lt;p&gt;Naturally, you reach for &lt;code&gt;CGEvent.post()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Synthesize ⌘V&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;stateID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;combinedSessionState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cmdVDown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;keyboardEventSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;virtualKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;keyDown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cmdVDown&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maskCommand&lt;/span&gt;
&lt;span class="n"&gt;cmdVDown&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cgSessionEventTap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works flawlessly in a non-sandboxed development build. You celebrate.&lt;/p&gt;

&lt;p&gt;Then you enable App Sandbox (required for App Store), run it, and... &lt;strong&gt;nothing happens&lt;/strong&gt;. No error. No log. No permission prompt. The &lt;code&gt;.post()&lt;/code&gt; call just silently does nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Undocumented Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CGEvent.post()&lt;/code&gt; is completely blocked inside App Sandbox.&lt;/strong&gt; There is no entitlement that re-enables it. Apple will not provide one.&lt;/p&gt;

&lt;p&gt;I spent two days reading every developer forum thread, every entitlement documentation page, every StackOverflow answer. The closest Apple documentation admits it is this line in the App Sandbox Design Guide, which you have to squint to find:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sending synthetic events to other processes is disallowed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. One sentence. In a guide that most indie devs stop reading after the "Container Directory" section.&lt;/p&gt;

&lt;p&gt;The workarounds I saw suggested online, which &lt;strong&gt;do not work&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Adding &lt;code&gt;com.apple.security.temporary-exception.user-interaction&lt;/code&gt; — deprecated, no longer honored&lt;/li&gt;
&lt;li&gt;❌ Adding &lt;code&gt;com.apple.security.device.input&lt;/code&gt; — wrong entitlement, not related&lt;/li&gt;
&lt;li&gt;❌ Using &lt;code&gt;HIDPostAuxKey&lt;/code&gt; — same sandbox rule applies&lt;/li&gt;
&lt;li&gt;❌ Using &lt;code&gt;IOHIDPostEvent&lt;/code&gt; — same&lt;/li&gt;
&lt;li&gt;❌ Requesting Accessibility permission — has no effect on this&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Actually Works: AppleScript System Events
&lt;/h2&gt;

&lt;p&gt;The workaround is to pretend to be an automation client and ask &lt;code&gt;System Events&lt;/code&gt; (an Apple-signed helper) to type for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the text into the clipboard (&lt;code&gt;NSPasteboard&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Send an AppleScript "System Events → keystroke ⌘V" via &lt;code&gt;NSAppleScript&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Restore the previous clipboard contents
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;outputText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSPasteboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;general&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;savedItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pasteboardItems&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compactMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* snapshot */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearContents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;forType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
    tell application "&lt;/span&gt;&lt;span class="kt"&gt;System&lt;/span&gt; &lt;span class="kt"&gt;Events&lt;/span&gt;&lt;span class="s"&gt;"
        keystroke "&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="s"&gt;" using command down
    end tell
    """&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSDictionary&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="kt"&gt;NSAppleScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeAndReturnError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Restore clipboard after a short delay (let the paste complete)&lt;/span&gt;
    &lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asyncAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearContents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;// re-write savedItems&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Required entitlement:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.automation.apple-events&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Required Info.plist entry&lt;/strong&gt; (macOS 10.14+):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSAppleEventsUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;QUICOPY uses System Events to paste your shortcut text into the currently focused application.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first use, macOS shows the user a permission prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"QUICOPY wants permission to control System Events."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;User clicks Allow → done. Works for all subsequent invocations.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Latency Trade-off
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CGEvent.post()&lt;/code&gt; (if it worked) would be ~5 ms. The AppleScript round-trip is 40–80 ms on a modern M-series Mac, closer to 100 ms on Intel.&lt;/p&gt;

&lt;p&gt;For a text-expansion use case, this is invisible to the user — it feels instant. The whole flow (user releases shortcut → text appears) stays under 100 ms on Apple Silicon, which is the human perception threshold.&lt;/p&gt;

&lt;p&gt;If you need &lt;em&gt;true&lt;/em&gt; sub-10-ms latency (game input, accessibility apps), App Store distribution is probably not your path. Direct notarized distribution outside the store lifts the sandbox and lets you use &lt;code&gt;CGEvent.post()&lt;/code&gt; directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Clipboard Restoration Problem
&lt;/h2&gt;

&lt;p&gt;The paste trick mutates the user's clipboard. If you don't restore it, users will paste your shortcut text into their next real Cmd+V — very bad UX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naive fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSPasteboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;general&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// ... do paste ...&lt;/span&gt;
&lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asyncAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;NSPasteboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;general&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;forType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; clipboard can hold multiple representations — images, rich text, file promises. A &lt;code&gt;.string(forType:)&lt;/code&gt; snapshot throws all of that away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pasteboardItems&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compactMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;NSPasteboardItem&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSPasteboardItem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;forType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ... paste ...&lt;/span&gt;

&lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asyncAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearContents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeObjects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Iterate every &lt;code&gt;NSPasteboardItem.types&lt;/code&gt; and copy the raw &lt;code&gt;Data&lt;/code&gt; for each. Preserves everything including image clipboard, file references, and rich text.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Timing Problem
&lt;/h2&gt;

&lt;p&gt;If you restore the clipboard too fast, you restore &lt;em&gt;before&lt;/em&gt; &lt;code&gt;System Events&lt;/code&gt; has read it → the paste gets nothing.&lt;/p&gt;

&lt;p&gt;If you restore too slowly, there's a visible window where the user's original clipboard is gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empirical finding:&lt;/strong&gt; 100 ms delay is safe on Apple Silicon. 150 ms on Intel. &lt;code&gt;executeAndReturnError&lt;/code&gt; does not block until the paste is truly complete — it returns as soon as the AppleScript dispatch succeeds.&lt;/p&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; rely on &lt;code&gt;DispatchQueue.sync&lt;/code&gt; thinking it'll wait — it won't, the paste is asynchronous on System Events' side.&lt;/p&gt;




&lt;h2&gt;
  
  
  App Store Review Pitfalls
&lt;/h2&gt;

&lt;p&gt;When I submitted to the App Store, the reviewer flagged &lt;strong&gt;Accessibility&lt;/strong&gt; in my initial build. I had included Accessibility API calls as a fallback for when Automation permission wasn't granted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple's position&lt;/strong&gt;: If you can accomplish the task through a less-privileged mechanism (Automation), don't ask for the more-privileged one (Accessibility).&lt;/p&gt;

&lt;p&gt;I removed all Accessibility code and replaced it with a non-blocking permission-request flow that only asks for Automation. Resubmitted. Approved.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;Lesson:&lt;/strong&gt; Minimize entitlements. Each entitlement is a question the reviewer will ask "do you really need this?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What the Final Architecture Looks Like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User presses ⌘⇧1
         │
         ▼
CGEvent Tap (captures system-wide keydown)
         │
         ▼
Match against user's shortcut mapping
         │
         ▼
Load text snippet → NSPasteboard
         │
         ▼
NSAppleScript: "keystroke v using command down"
         │
         ▼
macOS System Events types Cmd+V into focused app
         │
         ▼
Restore previous clipboard (100ms delay)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total latency: ~80–100 ms on M-series.&lt;br&gt;
Total code: ~200 lines Swift.&lt;br&gt;
App binary size: 2.8 MB (no Electron).&lt;br&gt;
App Store: approved.&lt;/p&gt;




&lt;h2&gt;
  
  
  If You're Building Something Similar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the App Sandbox Design Guide twice.&lt;/strong&gt; The fine print matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't assume CGEvent.post works&lt;/strong&gt;, even if your dev build works. Test with sandbox enabled early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot clipboard as pasteboard items&lt;/strong&gt;, not strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a permission request UI&lt;/strong&gt; for &lt;code&gt;com.apple.security.automation.apple-events&lt;/code&gt;. Without it, the AppleScript fails silently the first time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimize entitlements.&lt;/strong&gt; Every one is a review friction point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider whether you really need App Store&lt;/strong&gt;. Direct notarized distribution gives you more freedom but less reach.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About QUICOPY
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://www.quicopy.com" rel="noopener noreferrer"&gt;QUICOPY&lt;/a&gt; — a menu bar app that does exactly what this post describes, plus 7 built-in AI prompt templates mapped to &lt;code&gt;⌘⇧1&lt;/code&gt; through &lt;code&gt;⌘⇧7&lt;/code&gt;. It's on the &lt;a href="https://apps.apple.com/app/quicopy/id6761418490" rel="noopener noreferrer"&gt;Mac App Store&lt;/a&gt; for $9.99 lifetime or $1.99/month.&lt;/p&gt;

&lt;p&gt;If you want to see the end result of all this sandbox wrestling, there it is.&lt;/p&gt;

&lt;p&gt;Questions or corrections? Happy to discuss in the comments — especially interested in hearing from anyone who's found a way to make &lt;code&gt;CGEvent.post()&lt;/code&gt; work under sandbox (I don't think it's possible, but would love to be wrong).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://www.quicopy.com" rel="noopener noreferrer"&gt;quicopy.com&lt;/a&gt;. Follow me for more macOS indie dev notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>appstore</category>
      <category>sandbox</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
