There are a thousand clipboard managers on macOS. I know because I’ve tried a lot of them — bought and paid for at least a dozen. Some are bare-bones, some are packed with features I’ll never use, and a surprising number of them charge a subscription. A monthly fee. For a clipboard manager!
None of them quite did what I wanted. Too cluttered, too opinionated, too much, or not enough. The usual story: if you want something done the way you want it, you end up doing it yourself.
On top of that, it felt like a nice challenge. Complicated enough to keep me interested, simple enough — or so I thought — that I could knock one together over a weekend. A clipboard manager is one of those utilities that sounds trivial to describe. Copy something, it goes in a list. Click the list, it pastes. How hard could that be?
Harder than the weekend I’d budgeted, as it turns out. Not because the core concept is complex, but because the details — the pasteboard types, the paste simulation, the deduplication, the panel management, the “please don’t capture your own paste as a new history item” problem — add up to something that required more thought than I’d anticipated.
ClipMan is the result. About 3,700 lines of Swift across 18 source files, one external dependency, and a set of decisions I want to walk through.
macOS doesn’t offer a reliable notification when the clipboard changes.1 What it does offer is NSPasteboard.general.changeCount — an integer that increments every time something writes to the pasteboard. So ClipMan polls. Every half a second, it checks whether the change count has moved. If it has, something new was copied.
Half a second is imperceptible to a human but frequent enough that nothing slips through. It’s not elegant, but it’s reliable — and reliability beats elegance every time for something that runs all day in the background.
When a change is detected, the monitor captures what’s there. The macOS pasteboard isn’t just text — it’s a stack of typed data. A single copy operation might include plain text, rich text (RTF), an image, or file URLs. ClipMan checks for each:
.stringThis matters because when you paste something back later, you want it to come back exactly as it was — formatting, images, and all. Capturing only the plain text string would lose information the user expected to keep.
I went with SwiftData — Apple’s modern persistence framework that replaced Core Data. Each clipboard item is a model object with the text content, optional RTF data, optional image data, optional file URLs, the source application’s bundle identifier, a timestamp, and a pinned flag.
The history is configurable from 10 to 500 items. When the limit is reached, the oldest unpinned items are trimmed. Pinned items are exempt — they survive indefinitely regardless of the buffer size. This means you can pin a frequently-used snippet and it’ll stay in your history even as hundreds of newer items cycle through.
Deduplication is simple: before inserting a new item, the monitor compares the content string and file URLs against the most recent entry. If they match, it skips the insert. This prevents the common annoyance of copying the same thing twice and seeing it appear twice in your history.
This is where it gets interesting. When the user selects a history item, ClipMan needs to paste it into whatever application was active before the browser panel opened. That means:
Step 4 is the reason ClipMan requires Accessibility permission. Simulating keystrokes on macOS requires posting low-level CGEvent keyboard events, and that requires being a trusted accessibility client. There’s no way around it — you can write to the pasteboard from anywhere, but actually triggering the paste in another application requires the ability to inject keyboard input.
The keystroke simulation looks roughly like this:
let keyDown = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(kVK_ANSI_V), keyDown: true)
keyDown?.flags = .maskCommand
keyDown?.post(tap: .cghidEventTap)
There’s a 50ms delay before posting to ensure the target application has properly regained focus. Without it, the paste event occasionally arrives before the app is ready to receive it, and the keystroke goes nowhere.
Here’s a problem that’s obvious in hindsight but caught me off guard: when ClipMan writes an item back to the pasteboard for pasting, the change count increments. The monitor sees the change, captures the “new” content, and adds it to the history. You end up with the same item duplicated every time you paste from history.
The fix:
func ignoreNextChange() {
lastChangeCount = NSPasteboard.general.changeCount + 1
}
Before writing to the pasteboard, the paste engine tells the monitor to skip the next change count increment. Simple, but the kind of thing you only think of after watching your history fill up with duplicates.
ClipMan offers two paste modes: standard and “paste and match style.” The standard mode writes everything back — plain text, RTF data, images. The match style mode writes only the plain text string, stripping all formatting. This is useful when you’re pasting into a rich text editor and don’t want to bring along the font, size, and colour from the source.
It’s triggered with shiftreturn in the browser. return alone does a full-format paste.
The clipboard history UI is a floating panel — an NSPanel subclass that can become the key window (receive keyboard input) without becoming the main window (stealing focus from the underlying application permanently).
final class FloatingPanel: NSPanel {
override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { false }
}
This distinction matters. A standard NSWindow that becomes key also becomes main, which changes the menu bar and disrupts the user’s context. An NSPanel configured this way appears above everything, takes keyboard input for navigation, and disappears cleanly when dismissed — leaving the user exactly where they were.
The panel dismisses on escape, on return (after pasting), or when you click anywhere outside it. That last one uses a global event monitor:
NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { event in
let clickLocation = NSEvent.mouseLocation
if !panel.frame.contains(clickLocation) {
dismissBrowser()
}
}
It captures mouse clicks across the entire system — not just within the app — and checks whether they landed inside the panel. If they didn’t, the panel closes. The monitor is removed on dismiss to avoid leaking event handlers.
Navigation is keyboard-driven: left and right arrows move through history, with a position counter (“3 of 15”) showing where you are. Pinned items sort to the top.
The browser shows a preview of each item. Text items render in a monospaced font. Images display inline. File items show QuickLook thumbnails — the same previews Finder generates — at 72 pixels for multi-file copies or larger for single files.
This uses Apple’s QLThumbnailGenerator, which produces previews for any file type the system recognises. PDFs get a page thumbnail, images get a scaled preview, documents get their icon. It’s one of those system APIs that does a lot of work for free.
ClipMan doesn’t filter by source application. It doesn’t exclude password managers. It doesn’t have a search feature. It doesn’t sync to iCloud.
These are deliberate omissions, not oversights — but one of them kept me up at night.
I spent a considerable amount of time wrestling with the password question. Half of me wanted to exclude password copies at all costs. A clipboard manager that stores your banking password in plain text alongside your shopping list feels irresponsible. The other half of me kept arriving at the same conclusion: there’s no deterministic way to know if the thing being copied is a password. Password managers don’t consistently flag their pasteboard writes as confidential.2 And even if they did, plenty of people copy passwords from browsers, email, text files, or password-protected PDFs. There’s no signal to catch.
There’s also the practical reality: we’re all encouraged to use long, complex passwords. Copy-pasting them is more reliable than typing them. A clipboard manager that refuses to store the thing you just copied — because it might be sensitive — is a clipboard manager that gets in your way at exactly the wrong moment.
In the end, I decided not to stress about it. The choice is with the user. It’s not for me to put barriers in the way of how people use their own clipboard. Instead, I made sure it was easy to page through the history and see what’s there, and it’s a one-click operation to delete any clip you don’t want kept. It’s a compromise, I know. But I can live with it — and I think it’s the honest one.
Search would be useful for large histories, but the sequential browser with pinning covers my own usage well enough. Sync would require a server or iCloud entitlements and would violate our no-telemetry, no-network policy.
I’d rather ship something focused that does its job well than something sprawling that tries to cover every edge case.
One external dependency: KeyboardShortcuts by Sindre Sorhus, which handles global hotkey registration. Everything else is built-in frameworks — SwiftUI for the interface, SwiftData for persistence, AppKit for the panel and pasteboard, QuickLook for thumbnails, Carbon’s HIToolbox for key codes.
The app is a menu bar agent (LSUIElement in Info.plist), so it doesn’t appear in the Dock. It sits in the menu bar, watches the clipboard, and stays out of the way until you call it. That’s the whole point.
Clipboard managers look simple because the concept is simple. The implementation has edges — pasteboard types, paste simulation, self-referential capture, panel focus management, accessibility permissions. None of these are individually hard, but together they require a level of care that the “it just watches the clipboard” description doesn’t suggest.
The best decision was using NSPanel instead of a regular window. The worst decision was initially not handling the self-capture problem, which led to a confusing hour or two of watching my history fill with duplicates before I understood what was happening.
ClipMan is free, open source, and available now. Source on GitHub.
There are NSPasteboard change notifications in some contexts, but they’re not reliably delivered for all pasteboard types and all application states. Timer-based polling on the change count is the approach used by most clipboard managers on macOS, including the popular ones. ↩
Some apps use org.nspasteboard.ConcealedType to flag sensitive clipboard content, but adoption is inconsistent. 1Password supports it; many others don’t. Building a feature around an unreliable signal felt worse than not building it at all. ↩