Autocomplete inside your own text view is a solved problem. You own the NSTextView, you know where the caret is, and you decide what Tab does. Autocomplete inside someone else's text field, in an app you did not write, is a different job. You don't own the text, the caret, or the Tab key. You borrow all three through macOS Accessibility, and each app lends them a little differently.

We build DraftKey, a Mac app that suggests the rest of your sentence as you type and adds it when you press Tab. This post is about the plumbing behind that kind of feature. For the general mechanics I'm citing Apple's public documentation. For what we actually ran into, I'm citing our own public release notes: 52 tagged releases on GitHub, from 0.1.1 on September 20 to 0.1.62 on November 11, 2026. When I'm describing the macOS API surface in general rather than DraftKey's code, I'll say so.

Step 0: you need permission before you can see anything

A Mac app can't read another app's text fields by default. It has to be a trusted accessibility client, and only the user can grant that. Apple's AXIsProcessTrustedWithOptions "returns whether the current process is a trusted accessibility client," and with the prompt option set, macOS shows the system alert. Apple's own support page tells users to turn the permission on in Privacy & Security settings.

The generic check looks like this (public API, not our source):

import ApplicationServices let key = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String let trusted = AXIsProcessTrustedWithOptions([key: true] as CFDictionary) 

The engineering lesson is that onboarding is part of the feature. A writing assistant without the permission just looks broken. Our 0.1.3 notes list Accessibility permission and a compatible text field among the requirements for writing assistance. Later releases added a dismissible explanation when setup requirements or an unsupported field get in the way (0.1.15) and clearer permission instructions in guided setup (0.1.59). Most of that work is copy and UI, not API calls, and it matters just as much.

Step 1: find the field the user is actually typing in

The usual starting point is the system-wide accessibility element and its kAXFocusedUIElementAttribute. From the focused element you read the value, the selected text, and the selection range with AXUIElementCopyAttributeValue.

let systemWide = AXUIElementCreateSystemWide() var focused: CFTypeRef? AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute as CFString, &focused) 

Apple's docs say kAXSelectedTextRangeAttribute and kAXSelectedTextAttribute are "required for all accessibility objects that represent editable text elements." In practice, "required" describes the contract, not what each app actually does. Three items from our 0.1.22 notes show the gap:

  • Supporting short text fields that expose their text value but omit the optional range-reading or character-count APIs.
  • Falling back to macOS system focus when an application doesn't report its focused editor, and verifying the active application before and after the read.
  • Requesting the editor accessibility tree in Chromium and Electron applications, while respecting the user's per-app exclusions.

The second point is worth copying. Reading another process is not atomic. Focus can move between your first call and your last, so you check that you are still talking to the same app once the read finishes.

The third point is a known quirk of Chromium-based apps. Electron's accessibility docs explain that Electron apps turn on accessibility features automatically when assistive technology like VoiceOver is present, and that third-party software can switch them on through an AXManualAccessibility attribute. If your tool isn't recognized as assistive technology, the text you want may simply not be in the tree yet.

Step 2: know exactly where the caret is

A suggestion that appears in a separate window is easy. A suggestion drawn inline, right after the caret, needs the caret's position in screen coordinates. The relevant parameterized attribute is kAXBoundsForRangeParameterizedAttribute, which Apple describes as "the bounding rectangle a sighted user would see on the display screen, in pixels," for a given text range.

This is where "compatible text fields" stops being a marketing hedge and becomes a technical boundary. If an editor doesn't answer that question correctly, you can't draw text at the caret. Our 0.1.15 notes put it in one line: manual writing menus can anchor to the text field when the editor doesn't provide caret bounds, but autocomplete still requires precise caret placement. A menu can sit near a field. Inline text has to sit at the caret.

Even when an app answers, the answer can be wrong. In 0.1.50 we found that message boxes in Arc, Chrome, the Claude app and ChatGPT reported the cursor at the start of the message, so DraftKey never looked at what had just been typed. Gmail needed exact text-fragment coordinates for overlays (0.1.18). And since 0.1.10, inline suggestions sit at the cursor with a small Tab hint and are kept within the editor and screen bounds, a detail that is easy to forget until a field sits near the edge of a display.

Step 3: secure fields are off limits, and that is correct

Password fields are the one place where "it doesn't work here" is the right answer. AppKit's NSSecureTextField is "a text field that hides the typed text," and Apple's TN2150 describes EnableSecureEventInput, which "protects keyboard entry so that keyboard events cannot be intercepted by a keyboard intercept process." A writing tool shouldn't try to get around either. DraftKey excludes secure fields, and per-app controls let users choose where it is active at all.

Step 4: owning Tab without breaking Tab

Tab already means something in most apps: indent, next field, accept a completion from the app's own engine. A cross-app autocomplete can only take Tab when a suggestion is actually showing, and has to hand it back untouched the rest of the time. The general mechanism for watching global keys is an event tap (CGEvent.tapCreate), and one detail matters a lot: the system can disable a tap that responds too slowly, which shows up as tapDisabledByTimeout.

Our early release notes read like a list of every way this can go wrong:

  • 0.1.3: held shortcuts run once per press, and accepting a preview no longer lets repeated Tab or Space presses leak into the editor. Keyboard connections are checked again after sleep and app switching, with a Reconnect action when needed.
  • 0.1.15: the shortcut returns to macOS immediately, before reading the editor, to avoid keyboard-tap timeouts.
  • 0.1.18: global keyboard delivery moved off the UI thread.

The pattern is the same each time. Decide fast, return the event, and do the slow Accessibility reads afterward.

Partial acceptance is its own small design problem. Since 0.1.45, Option + Right Arrow takes just the next word of a suggestion, with Option + Shift + Right Arrow as the alternative if the user has set Option + Right Arrow to accept everything. Keep typing and the suggestion goes away.

Step 5: rewrites write back only after review

Autocomplete adds text at the caret. A rewrite replaces text the user already wrote, so we treat it as a separate, explicit action. In DraftKey you tap Command + Option to open Writing tools, ask for a change, edit the preview if you want, and choose Insert. Nothing is written back until you do.

The write-back is the risky part, because the world has moved since you captured the text. AXUIElementSetAttributeValue documents error codes, including one for an element that doesn't support the attribute you're setting. Our notes describe the guardrails we ended up with:

  • 0.1.41: Insert returns to the captured editor, verifies the original text, and applies one edit. Supported Chromium editors get paste delivery, including paragraph breaks, with the clipboard restored afterward. A changed original is never silently overwritten.
  • 0.1.44: DraftKey waits for the original app, returns focus to the original field, checks that the text hasn't changed, and then applies one edit. It never writes into a different field.
  • 0.1.54: a failed insertion keeps your draft available to retry.

If you build anything that writes into another app, "same app, same field, same text, one edit" is a good rule to start from.

What I would tell anyone building on Accessibility

  1. Treat every attribute as optional, including the ones Apple marks as required.
  2. Verify before and after. Same app, same element, same text, or don't write.
  3. Diagnose without collecting text. Our 0.1.22 and 0.1.26 notes describe recording which stage of an editor read failed, without storing document text. You can find out why a field doesn't work without seeing what someone wrote in it.
  4. Say "compatible text fields" and mean it. Support depends on how each editor exposes its text. Some custom editors won't work. Saying so upfront sets the right expectation.
  5. Respect the event tap's time budget. Slow work belongs after the keystroke goes back to macOS, not inside the callback.

Where DraftKey fits

If you want to try the result: DraftKey needs an Apple Silicon Mac on macOS 14 or later, and its AI features need at least 8 GB of memory and a separate writing-model download of about 1.12 GB, with at least 2.2 GB of free space. Every Mac gets 500 free words of writing help. After that it's $4.99 once, for up to three Macs you own. It works in compatible text fields, secure fields are excluded, and some custom editors aren't supported. You can read more about it here.

If you've built on AXUIElement yourself, I'd like to compare notes: which app's text fields gave you the most trouble, and how did you detect it without logging what people typed?