Every time a clipboard manager pastes an old item, it’s doing something most apps never do: typing on your behalf. It writes data to the pasteboard, then simulates pressing ⌘V — as if you pressed it yourself. The API that makes this possible is CGEvent, part of the Core Graphics framework.
CGEvent operates at the lowest level of macOS input handling. It can create synthetic keyboard and mouse events and inject them into the system’s event stream. Here’s how it works.
What is CGEvent?
CGEvent (Core Graphics Event) represents a low-level input event — a key press, key release, mouse click, mouse movement, or scroll wheel action. It’s part of the Quartz Event Services API, which sits between the hardware input layer (IOKit HID) and the higher-level AppKit event handling.
When you press a physical key, the keyboard generates a HID event, which the system converts into a CGEvent and routes to the frontmost application. CGEvent lets you create these events programmatically and inject them at the same point in the pipeline — making them indistinguishable from physical input.
To the receiving app, a CGEvent keystroke is identical to a physical key press. There’s no flag, no metadata, no way to tell the difference. That’s what makes it powerful — and why it requires elevated permissions.
Creating key events
A keystroke is actually two events: key down and key up. Here’s how to create a ⌘V (paste) keystroke:
let source = CGEventSource(stateID: .hidSystemState)
// Key down
let keyDown = CGEvent(
keyboardEventSource: source,
virtualKey: 0x09, // V key
keyDown: true
)
keyDown?.flags = .maskCommand
// Key up
let keyUp = CGEvent(
keyboardEventSource: source,
virtualKey: 0x09,
keyDown: false
)
keyUp?.flags = .maskCommand
The key components:
Virtual key codes are not ASCII values and not Unicode scalars. They’re hardware scan codes from the old Carbon days. The most commonly used ones: 0x09 (V), 0x08 (C), 0x06 (Z), 0x07 (X), 0x00 (A).
Posting events to the system
Creating the event doesn’t do anything — you need to post it to the system’s event stream:
keyDown?.post(tap: .cghidEventTap)
keyUp?.post(tap: .cghidEventTap)
The tap parameter determines where in the event pipeline the event is injected:
.cghidEventTap— earliest point, before any event taps or filters process it. This is the most reliable option..cgSessionEventTap— injected at the session level, after HID processing..cgAnnotatedSessionEventTap— latest point, with additional annotation data.
For keystroke simulation, .cghidEventTap is the standard choice. The event flows through the entire normal pipeline, just as if a physical key was pressed.
Delay between key down and key up
Post the key down event, then the key up event. In most cases, posting them back-to-back with no delay works fine. Some apps (particularly games and Electron apps) expect a brief gap — inserting a 10-20ms delay between the down and up events using usleep(10000) or DispatchQueue.main.asyncAfter can improve reliability.
Why clipboard managers use it
The clipboard manager paste workflow is:
- User selects a historical item from the manager’s UI
- The manager writes that item’s data to
NSPasteboard.general - The manager dismisses its panel
- The manager simulates ⌘V via CGEvent
- The frontmost app receives the paste command and reads from the pasteboard
Step 4 is the critical one. Without CGEvent, the user would have to manually press ⌘V after selecting an item — breaking the flow of a feature that’s supposed to be seamless.
The panel must be dismissed before posting the event. If the clipboard manager’s panel is still frontmost, the ⌘V goes to the panel itself instead of the target app. A brief delay (10-50ms) between dismissing the panel and posting the event ensures the target app has regained focus.
func pasteItem(_ item: ClipItem) {
// Write to pasteboard
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(item.content, forType: .string)
// Dismiss panel
panel.orderOut(nil)
// Small delay, then simulate paste
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
simulateCommandV()
}
}
The Accessibility requirement
CGEvent.post() silently fails without Accessibility permissions. No error is thrown, no exception is raised — the event simply doesn’t get delivered. This is one of the most common “it works in development but not in production” issues for Mac developers.
To check if your app has Accessibility access:
let trusted = AXIsProcessTrustedWithOptions(
[kAXTrustedCheckOptionPrompt.takeRetainedValue(): true] as CFDictionary
)
Passing kAXTrustedCheckOptionPrompt: true opens System Settings to the Accessibility pane if access hasn’t been granted, guiding the user to enable it.
Check for Accessibility permissions at app launch and show a clear onboarding screen explaining why it’s needed. Don’t wait until the user tries to paste — by then they’ll think the app is broken. QuietClip checks on first launch and explains exactly what the permission enables.
Common pitfalls
Forgetting key up. If you post a key down without a key up, the system thinks the key is still held. This can cause bizarre behavior in the target app — repeated paste, stuck modifier keys, or unresponsive input.
Wrong event source state. Using .privateState creates an event source that doesn’t interact with the physical keyboard state. This can cause issues if the user is holding modifier keys while the event is posted.
Not handling secure input. When a password field has focus, macOS enables secure input mode, which blocks CGEvent keystroke injection. Your clipboard manager should detect this (via IsSecureEventInputEnabled()) and either skip paste simulation or warn the user.
Testing in the debugger. Xcode itself has Accessibility access, which can mask permission issues during development. Always test your paste workflow from a standalone build outside of Xcode.
Seamless paste, powered by CGEvent.
QuietClip handles all the complexity of keystroke simulation so you just press Enter. Free to start, $8.99 once for Pro.