Skip to content

Instantly share code, notes, and snippets.

@ajpc500
Last active February 21, 2026 19:01
Show Gist options
  • Select an option

  • Save ajpc500/13566fd48647885845cd2c91746ed852 to your computer and use it in GitHub Desktop.

Select an option

Save ajpc500/13566fd48647885845cd2c91746ed852 to your computer and use it in GitHub Desktop.
macOS Pasteboard Monitor
// ClipboardMonitor.m
//
// Simple pasteboard monitor that logs clipboard and drag pasteboard activity
// with source and destination attribution.
//
// Compile:
// clang -fobjc-arc -framework Cocoa -framework ApplicationServices -framework Security \
// -o ClipboardMonitor ClipboardMonitor.m
//
// Run (requires Accessibility permission):
// ./ClipboardMonitor
#import <Cocoa/Cocoa.h>
#import <ApplicationServices/ApplicationServices.h>
// ─────────────────────────────────────────────────────────────────────────────
#pragma mark - Configuration
// ─────────────────────────────────────────────────────────────────────────────
static NSSet<NSString *> *gTerminalBundleIDs;
static NSSet<NSString *> *gBrowserBundleIDs;
static void InitConfig(void) {
gTerminalBundleIDs = [NSSet setWithArray:@[
@"com.apple.Terminal",
@"com.googlecode.iterm2",
@"com.mitchellh.ghostty",
@"net.kovidgoyal.kitty",
@"dev.warp.Warp-Stable",
@"com.github.wez.wezterm",
@"co.zeit.hyper",
]];
gBrowserBundleIDs = [NSSet setWithArray:@[
@"com.apple.Safari",
@"com.google.Chrome",
@"org.mozilla.firefox",
@"com.microsoft.edgemac",
@"com.brave.Browser",
@"company.thebrowser.Browser",
@"com.operasoftware.Opera",
@"com.vivaldi.Vivaldi",
@"app.zen-browser.zen",
]];
}
// ─────────────────────────────────────────────────────────────────────────────
#pragma mark - Signing ID lookup
// ─────────────────────────────────────────────────────────────────────────────
static NSMutableDictionary<NSString *, NSString *> *gSigningIDCache;
static NSString *SigningID(NSRunningApplication *app) {
if (!app || !app.bundleURL) return @"<unknown>";
NSString *cacheKey = app.bundleURL.absoluteString;
if (!gSigningIDCache) gSigningIDCache = [NSMutableDictionary new];
NSString *cached = gSigningIDCache[cacheKey];
if (cached) return cached;
SecStaticCodeRef staticCode = NULL;
OSStatus status = SecStaticCodeCreateWithPath(
(__bridge CFURLRef)app.bundleURL, kSecCSDefaultFlags, &staticCode);
if (status != errSecSuccess || !staticCode) return @"<unsigned>";
CFDictionaryRef info = NULL;
status = SecCodeCopySigningInformation(staticCode, kSecCSSigningInformation, &info);
CFRelease(staticCode);
if (status != errSecSuccess || !info) return @"<unsigned>";
NSDictionary *sigInfo = (__bridge_transfer NSDictionary *)info;
NSString *identifier = sigInfo[(__bridge NSString *)kSecCodeInfoIdentifier];
NSString *result = identifier ?: @"<unsigned>";
gSigningIDCache[cacheKey] = result;
return result;
}
// ─────────────────────────────────────────────────────────────────────────────
#pragma mark - App categorisation helpers
// ─────────────────────────────────────────────────────────────────────────────
static NSString *AppCategory(NSString *bundleID) {
if (!bundleID) return @"unknown";
if ([gTerminalBundleIDs containsObject:bundleID]) return @"terminal";
if ([gBrowserBundleIDs containsObject:bundleID]) return @"browser";
return @"other";
}
static NSString *AppLabel(NSRunningApplication *app) {
if (!app) return @"<unknown>";
return [NSString stringWithFormat:@"%@ (%@)",
app.localizedName ?: @"?",
app.bundleIdentifier ?: @"?"];
}
// ─────────────────────────────────────────────────────────────────────────────
#pragma mark - Shared preview helper
// ─────────────────────────────────────────────────────────────────────────────
static const NSUInteger kPreviewMaxLen = 120;
static NSString *Preview(NSString *text) {
if (!text || text.length == 0) return @"<empty>";
NSUInteger len = MIN(kPreviewMaxLen, text.length);
NSString *preview = [text substringToIndex:len];
preview = [preview stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
preview = [preview stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
if (text.length > len) preview = [preview stringByAppendingString:@"…"];
return preview;
}
// ─────────────────────────────────────────────────────────────────────────────
#pragma mark - Clipboard source tracking
// ─────────────────────────────────────────────────────────────────────────────
static NSString *gCopySourceBundleID = nil;
static NSString *gCopySourceAppName = nil;
static NSString *gCopySourceSigningID = nil;
static NSString *gCopySourceCategory = nil;
static void RecordCopySource(void) {
NSRunningApplication *front = NSWorkspace.sharedWorkspace.frontmostApplication;
gCopySourceBundleID = front.bundleIdentifier ?: @"<unknown>";
gCopySourceAppName = front.localizedName ?: @"<unknown>";
gCopySourceSigningID = SigningID(front);
gCopySourceCategory = AppCategory(gCopySourceBundleID);
NSString *clipText = [NSPasteboard.generalPasteboard stringForType:NSPasteboardTypeString];
NSUInteger len = clipText.length;
NSString *preview = Preview(clipText);
// Only log if source is a terminal or browser
if ([gCopySourceCategory isEqualToString:@"other"]) return;
NSLog(@"πŸ“‹ COPY | source: %@ [%@] signing_id:%@ category:%@ | %lu chars | \"%@\"",
gCopySourceAppName, gCopySourceBundleID, gCopySourceSigningID,
gCopySourceCategory, (unsigned long)len, preview);
}
// ─────────────────────────────────────────────────────────────────────────────
#pragma mark - Paste logging
// ─────────────────────────────────────────────────────────────────────────────
static void LogPaste(NSString *trigger) {
NSRunningApplication *dest = NSWorkspace.sharedWorkspace.frontmostApplication;
NSString *destBundleID = dest.bundleIdentifier ?: @"<unknown>";
NSString *destAppName = dest.localizedName ?: @"<unknown>";
NSString *destSigningID = SigningID(dest);
NSString *destCategory = AppCategory(destBundleID);
// Only log if source or destination is a terminal/browser
BOOL sourceInteresting = gCopySourceCategory && ![gCopySourceCategory isEqualToString:@"other"];
BOOL destInteresting = ![destCategory isEqualToString:@"other"];
if (!sourceInteresting && !destInteresting) return;
NSString *clipText = [NSPasteboard.generalPasteboard stringForType:NSPasteboardTypeString];
NSUInteger len = clipText.length;
NSString *preview = Preview(clipText);
NSLog(@"πŸ“Ž PASTE | trigger: %@ | source: %@ [%@] (%@) β†’ dest: %@ [%@] (%@) | %lu chars | \"%@\"",
trigger,
gCopySourceAppName ?: @"<unknown>", gCopySourceBundleID ?: @"<unknown>", gCopySourceCategory ?: @"unknown",
destAppName, destBundleID, destCategory,
(unsigned long)len, preview);
}
// ─────────────────────────────────────────────────────────────────────────────
#pragma mark - Drag pasteboard monitoring
// ─────────────────────────────────────────────────────────────────────────────
static NSInteger gLastDragChangeCount = 0;
static void CheckDragPasteboard(void) {
NSPasteboard *dragPB = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag];
NSInteger current = dragPB.changeCount;
if (current == gLastDragChangeCount) return;
gLastDragChangeCount = current;
NSString *dragText = [dragPB stringForType:NSPasteboardTypeString];
if (!dragText || dragText.length == 0) return;
NSUInteger len = dragText.length;
NSString *preview = Preview(dragText);
NSRunningApplication *front = NSWorkspace.sharedWorkspace.frontmostApplication;
NSString *appName = front.localizedName ?: @"<unknown>";
NSString *bundleID = front.bundleIdentifier ?: @"<unknown>";
NSString *category = AppCategory(bundleID);
// Only log if the drag involves a terminal or browser
if ([category isEqualToString:@"other"]) return;
NSLog(@"πŸ“¦ DRAG | app: %@ [%@] (%@) | %lu chars | \"%@\"",
appName, bundleID, category, (unsigned long)len, preview);
}
// ─────────────────────────────────────────────────────────────────────────────
#pragma mark - AX terminal watcher (drop detection)
// ─────────────────────────────────────────────────────────────────────────────
static NSMutableDictionary<NSNumber *, NSValue *> *gAXObservers;
static NSInteger gLastSeenDragPBChangeCount = 0;
static void AXCallback(AXObserverRef observer, AXUIElementRef element,
CFStringRef notification, void *refcon)
{
NSString *notifName = (__bridge NSString *)notification;
if (![notifName isEqualToString:(__bridge NSString *)kAXValueChangedNotification])
return;
pid_t termPID = 0;
AXUIElementGetPid(element, &termPID);
if (termPID <= 0) return;
// Check if the drag pasteboard changed since we last saw it β€” indicates a fresh drop
NSPasteboard *dragPB = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag];
NSInteger dragPBCount = dragPB.changeCount;
if (dragPBCount == gLastSeenDragPBChangeCount) return;
gLastSeenDragPBChangeCount = dragPBCount;
NSString *dragText = [dragPB stringForType:NSPasteboardTypeString];
if (!dragText || dragText.length == 0) return;
NSRunningApplication *termApp =
[NSRunningApplication runningApplicationWithProcessIdentifier:termPID];
NSString *destBundleID = termApp.bundleIdentifier ?: @"<unknown>";
NSString *destAppName = termApp.localizedName ?: @"<unknown>";
NSUInteger len = dragText.length;
NSString *preview = Preview(dragText);
NSLog(@"🎯 DROP | source: drag pasteboard β†’ dest: %@ [%@] (%@) | %lu chars | \"%@\"",
destAppName, destBundleID, AppCategory(destBundleID),
(unsigned long)len, preview);
}
static void WatchTerminal(NSRunningApplication *app) {
pid_t pid = app.processIdentifier;
NSNumber *pidKey = @(pid);
if (gAXObservers[pidKey]) return;
AXObserverRef observer = NULL;
AXError err = AXObserverCreate(pid, AXCallback, &observer);
if (err != kAXErrorSuccess || !observer) return;
AXUIElementRef appElement = AXUIElementCreateApplication(pid);
AXObserverAddNotification(observer, appElement, kAXValueChangedNotification, NULL);
CFRelease(appElement);
CFRunLoopAddSource(CFRunLoopGetMain(),
AXObserverGetRunLoopSource(observer),
kCFRunLoopCommonModes);
gAXObservers[pidKey] = [NSValue valueWithPointer:observer];
}
static void StartTerminalWatchers(void) {
gAXObservers = [NSMutableDictionary new];
gLastSeenDragPBChangeCount = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag].changeCount;
// Watch existing terminals
for (NSRunningApplication *app in NSWorkspace.sharedWorkspace.runningApplications) {
if (app.bundleIdentifier && [gTerminalBundleIDs containsObject:app.bundleIdentifier]) {
WatchTerminal(app);
NSLog(@" πŸ‘ Watching %@ (pid %d)", app.bundleIdentifier, app.processIdentifier);
}
}
// Watch for new terminal launches
[NSWorkspace.sharedWorkspace.notificationCenter
addObserverForName:NSWorkspaceDidLaunchApplicationNotification
object:nil
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification *note) {
NSRunningApplication *app = note.userInfo[NSWorkspaceApplicationKey];
if (app.bundleIdentifier && [gTerminalBundleIDs containsObject:app.bundleIdentifier]) {
WatchTerminal(app);
NSLog(@" πŸ‘ Now watching %@ (pid %d)", app.bundleIdentifier, app.processIdentifier);
}
}];
}
// ─────────────────────────────────────────────────────────────────────────────
#pragma mark - Main
// ─────────────────────────────────────────────────────────────────────────────
int main(int argc, const char *argv[]) {
@autoreleasepool {
NSApplication *app = [NSApplication sharedApplication];
[app setActivationPolicy:NSApplicationActivationPolicyAccessory];
InitConfig();
// Check Accessibility permission
NSDictionary *opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @YES};
if (!AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts)) {
NSLog(@"⚠️ Grant Accessibility permission in System Settings, then re-run.");
while (!AXIsProcessTrusted()) {
[NSThread sleepForTimeInterval:1.0];
}
NSLog(@"βœ… Accessibility permission granted!");
}
NSLog(@"╔══════════════════════════════════════════╗");
NSLog(@"β•‘ Clipboard & Drag Pasteboard Monitor β•‘");
NSLog(@"β•‘ @ajpc500 / delivr.to β•‘");
NSLog(@"β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•");
NSLog(@"");
NSLog(@"Monitored terminals:");
for (NSString *bid in [gTerminalBundleIDs.allObjects sortedArrayUsingSelector:@selector(compare:)]) {
NSLog(@" β€’ %@", bid);
}
NSLog(@"");
NSLog(@"Monitored browsers:");
for (NSString *bid in [gBrowserBundleIDs.allObjects sortedArrayUsingSelector:@selector(compare:)]) {
NSLog(@" β€’ %@", bid);
}
NSLog(@"");
NSLog(@"Filtering to events involving the above apps only.");
NSLog(@"Monitoring clipboard copies, pastes (⌘+V), and drag pasteboard changes…");
// ── 1. Poll general pasteboard for copy detection ──
__block NSInteger lastChangeCount = NSPasteboard.generalPasteboard.changeCount;
[NSTimer scheduledTimerWithTimeInterval:0.3 repeats:YES block:^(NSTimer *t) {
NSInteger current = NSPasteboard.generalPasteboard.changeCount;
if (current != lastChangeCount) {
lastChangeCount = current;
RecordCopySource();
}
}];
// ── 2. Poll drag pasteboard for drag activity ──
gLastDragChangeCount = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag].changeCount;
[NSTimer scheduledTimerWithTimeInterval:0.3 repeats:YES block:^(NSTimer *t) {
CheckDragPasteboard();
}];
// ── 3. AX watchers on terminals for drop detection ──
NSLog(@"");
NSLog(@"Terminal AX watchers:");
StartTerminalWatchers();
NSLog(@"");
// ── 4. Global key monitor for ⌘+V (paste detection) ──
[NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
handler:^(NSEvent *event) {
BOOL cmdDown = (event.modifierFlags & NSEventModifierFlagCommand) != 0;
NSString *chars = [event.charactersIgnoringModifiers lowercaseString];
if (cmdDown && [chars isEqualToString:@"v"]) {
LogPaste(@"⌘+V");
}
}];
[app run];
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment