{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreiceul4tr2g2jc24kzwzddrwajf57tvmm5kprdkubsvvjt222panqq",
    "uri": "at://did:plc:2u26gaflouttm3uj6jkgroyz/app.bsky.feed.post/3mf74ev6adsw2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreiaocy4zofz3tilnwrvxi4l3kwyfd5douc7geb7pimq5ljabz7nlpy"
    },
    "mimeType": "image/png",
    "size": 2305755
  },
  "description": "If you have built a macOS app that intercepts media keys or system-wide keyboard events it can be a nightmare: the tap appears to install successfully, but after re-signing the binary and launching via the Dock or Finder, events never fire. Here’s what was happening to me, and how I fixed it.",
  "path": "/til/2026/02/19/cgevent-taps-and-code-signing-the-silent-disable-race/",
  "publishedAt": "2026-02-19T07:51:51.000Z",
  "site": "https://danielraffel.me",
  "textContent": "If you’ve built a macOS tool that intercepts media keys or system-wide keyboard events using `CGEvent.tapCreate()`, you may have hit this: the tap appears to install successfully, but after re-signing the app and launching via the Dock/Finder (or `open`), events never fire. Pressing volume keys does nothing. No crash. No error. The tap just goes quiet.\n\nHere’s what was happening for me and what fixed it.\n\n### Quick repro\n\n  * Re-sign the app → launch via Finder/Dock or `open MyApp.app` → tap installs but no events fire\n  * Launch the Mach-O directly (`.../Contents/MacOS/MyApp`) → events fire normally\n\n\n\n## The Setup\n\nMedia keys (play/pause, volume, brightness) often show up as “system defined” events (`NX_SYSDEFINED`) at the CoreGraphics layer. I intercepted them with a session event tap:\n\n\n    let eventMask: CGEventMask = (1 << CGEventType.systemDefined.rawValue)\n\n    let tap = CGEvent.tapCreate(\n        tap: .cgSessionEventTap,\n        place: .headInsertEventTap,\n        options: .defaultTap,\n        eventsOfInterest: eventMask,\n        callback: eventCallback,\n        userInfo: nil\n    )\n\nIn my case, `NSEvent.addGlobalMonitorForEvents(matching: .systemDefined)` was not reliable for the media keys I needed, so I used a CoreGraphics event tap.\n\n## **The Bug (What I Observed)**\n\nAfter re-signing a new build (e.g. `codesign --force --deep`) and launching via Finder/Dock or open MyApp.app:\n\n  * `tapCreate()` returned a non-nil CFMachPort\n  * `CGEvent.tapIsEnabled()` initially returned true\n  * then… no callbacks ever fired\n\n\n\nLaunching the binary directly worked:\n\n\n    /Applications/MyApp.app/Contents/MacOS/MyApp\n\nIn my setup, the failure correlated with Launch Services launches after re-signing, and behaved like a permission/trust evaluation issue: the tap existed, but didn’t receive events, and the usual \"disabled\" callback path wasn’t dependable.\n\n## **The Fix**\n\n  1. **Deployment: launch the binary directly (avoid open)**\n\n\n\n\n    nohup /Applications/MyApp.app/Contents/MacOS/MyApp \\\n      >> ~/Library/Logs/MyApp.log 2>&1 & disown\n\nThe app still appears in the Dock and behaves like a normal app launch; the main difference is stdout/stderr goes to the log file.\n\n  1. **Safety net: continuously verify tap health and recover**\n\n\n\n\n    Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in\n        guard let tap = self.eventTap else { return }\n\n        if !CGEvent.tapIsEnabled(tap: tap) {\n            CGEvent.tapEnable(tap: tap, enable: true)\n\n            // If tapEnable doesn't stick, reinstall the tap:\n            // remove from RunLoop, create new tap, re-add.\n            if !CGEvent.tapIsEnabled(tap: tap) {\n                self.reinstallEventTap()\n            }\n        }\n    }\n\nAnd handle both disable events in the callback:\n\n\n    if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {\n        if let tap = info?.tap {\n            CGEvent.tapEnable(tap: tap, enable: true)\n        }\n        return nil\n    }\n\n## **What to Check First (Permissions)**\n\nBefore installing the tap, verify Input Monitoring (“ListenEvent”) permission:\n\n\n    if !CGPreflightListenEventAccess() {\n        CGRequestListenEventAccess()\n    }\n\nInput Monitoring is required to  _listen_ for global events. Also note: if you use `.defaultTap` (not `.listenOnly`), you may additionally need Accessibility permission for full interception/interaction. Missing permissions can look identical to the \"tap installed but no events\" symptom.\n\n## **Summary**\n\n**Symptom**| **Likely cause**\n---|---\nTap non-nil but no events, direct Terminal launch works| Identity/permission/trust differences after re-signing + Launch Services launch path (observed)\nTap is nil| Not permitted for that tap location/type, or creation failed\nTap dies mid-session| Timeout/user-input disable; recover in callback and via health checks\nNSEvent monitor misses keys| Not reliable for your target keys; use CoreGraphics tap\n\n**Key insight:** a non-nil tap is not a healthy tap. Always verify `tapIsEnabled` at runtime, not just at install time.\n\n## Working assumptions (based on observed behavior)\n\nI can’t prove the exact internal mechanism, but given the repeatable pattern in my setup, I’m operating under these assumptions:\n\n  * **TCC decisions are tied to code identity** , and re-signing can effectively create a \"new\" identity that requires re-evaluation (or re-granting) for Input Monitoring / Accessibility.\n  * **Launching via Launch Services** (`open`, Finder/Dock) is more likely to trigger that identity/permission re-evaluation than launching the Mach-O directly.\n  * When this happens, **a`CGEvent` tap can exist but be functionally inert** (no callbacks), and the normal \"tap disabled\" callback path may not reliably fire—so **health checks + reinstall** are the pragmatic mitigation.\n\n\n\nIf the issue reproduces, the fastest way to validate the hypothesis is to compare:\n\n  * direct exec vs `open`\n  * `tapIsEnabled` over time\n  * current TCC grants for Input Monitoring / Accessibility after each re-sign\n\n\n\n## Optional mitigation: delay tap installation\n\nIn my setup, adding a short delay before installing the tap reduced failures after re-sign + Launch Services launch. I treat this as a best-effort workaround (timing-dependent), not the core fix.\n\n\n    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {\n        self.installEventTap()\n    }\n\nOr (often safer): install immediately, then recheck/reinstall after a short delay:\n\n\n    installEventTap()\n    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {\n        verifyOrReinstallTap()\n    }",
  "title": "CGEvent Taps and Code Signing: The Silent Disable Race",
  "updatedAt": "2026-02-23T23:56:07.473Z"
}