Disabling Raycast Shortcuts in Specific macOS Apps

I love Raycast. It has been an integral part of my setup for years. That said, it still has ways to go when it comes to flexibility.

One of the features it is currently missing is the ability to disable its global shortcuts in specific apps where you may not want them.

For me, this gets annoying when I use the macOS Screen Sharing app to control a remote Mac. I use Caps Lock as a Hyper key via Karabiner-Elements, with Raycast commands bound to those combos.

This setup is identical on both my local and remote Mac. So when I control the remote Mac through the Screen Sharing app and press Hyper+C, I expect that shortcut to be sent to the remote Mac, where it can do whatever that Mac is configured to do, in my case – switch to Chrome.

Instead, local Raycast captures it and I end up switching from controlling the remote Mac to a local Chrome window.

What I gotHyper + CScreen Sharing appRemote MacLocal RaycastshortcutLocal Chromeintended pathshortcut handled remotelycaptured firstlocal Mac reactsWhat I wantedHyper + CScreen Sharing appRemote MacLocal RaycastshortcutLocal Chromeintended pathshortcut handled remotelycaptured firstlocal Mac reacts

There is no built-in Raycast setting for excluding specific apps.

At first I was thinking about a blunt workaround: quit Raycast when the Screen Sharing app becomes frontmost, then relaunch it when I switch away.

For my use case, that would probably work. But I would keep it only as a last resort — killing and relaunching an app just to scope shortcuts looks bad even if it works.

Before doing that, I wanted to try the potentially cleaner solution: leave Raycast running, but intercept the shortcuts only in the app where I want Raycast to be disabled.

The Cleaner Solution

The working prototype is a tiny Swift process with a CGEvent tap, so it can see key events before Raycast handles them.

When a shortcut-like key event arrives, it checks whether the frontmost app is on an exclude list. If it is, it copies the event, posts the copy directly to that app’s process ID, and swallows the original local event so Raycast never handles it.

If the frontmost app is not on the exclude list, it does nothing. In effect, Raycast shortcuts are disabled in selected apps and enabled everywhere else.

Frontmost app is excludedHyper + CExcludedfrontmost app?Frontmost apphandles shortcutLocal RaycastinterceptedYesswallow + postToPidNodo nothingFrontmost app is not excludedHyper + CExcludedfrontmost app?Frontmost apphandles shortcutLocal RaycastinterceptedYesswallow + postToPidRaycast shortcuts stay active

The best part (and the part I was worried about) was that Karabiner was not a blocker. The event tap sees the transformed event, so a Caps Lock Hyper chord shows up as Ctrl+Opt+Shift+Cmd+C.

Once I saw that, the whole approach felt viable.

The Scope

If the goal is to make Raycast inactive in specific apps, there is a tempting version of this that redirects every key while an excluded app is active. I do not think that is the right default.

Plain typing has a lot of edge cases: keyboard layouts, dead keys, input methods, repeat behavior, text composition, and app-specific handling. Shortcuts are much cleaner. They are mostly key codes plus modifier flags.

So the useful scope is narrower:

  • redirect shortcut-like chords
  • leave ordinary typing alone
  • avoid raw modifier transitions unless you have a good reason

The matcher below redirects keys that include at least one of Cmd, Ctrl, or Opt. Shift is allowed as an extra modifier, but Shift+T by itself is not redirected. That still catches Cmd+T, Cmd+Shift+T, Ctrl+Opt+T, and Hyper chords without hijacking normal uppercase typing.

For my use case, I wanted the block to be pretty complete. If Screen Sharing is frontmost, I want shortcut-like chords to bypass local Raycast—and any other app for that matter—entirely.

For less all-encompassing setups, you may want exceptions such as Cmd+Tab or Cmd+Space. I left that part commented out in the script.

The Code

Here is the version that fits my use case. The excludedBundleIDs list and the shortcut matcher are the two things most likely to need adjusting.

import AppKit
import ApplicationServices
import Foundation

let excludedBundleIDs: Set<String> = [
    "com.apple.ScreenSharing",
    "com.apple.screensharing.agent"
]

let verboseLogging = false
var eventTap: CFMachPort?

func log(_ message: String) {
    let formatter = DateFormatter()
    formatter.dateFormat = "HH:mm:ss.SSS"
    print("[\(formatter.string(from: Date()))] \(message)")
    fflush(stdout)
}

func isExcludedApp(_ app: NSRunningApplication?) -> Bool {
    guard let app else { return false }

    if let bundleID = app.bundleIdentifier,
       excludedBundleIDs.contains(bundleID) {
        return true
    }

    let name = app.localizedName?.lowercased() ?? ""
    return name.contains("screen sharing")
}

func frontmostExcludedApp() -> NSRunningApplication? {
    let app = NSWorkspace.shared.frontmostApplication
    return isExcludedApp(app) ? app : nil
}

let modifierKeyCodes: Set<Int64> = [
    54, // right command
    55, // left command
    56, // left shift
    57, // caps lock
    58, // left option
    59, // left control
    60, // right shift
    61, // right option
    62, // right control
    63  // fn
]

func isActualModifierKey(_ keyCode: Int64) -> Bool {
    modifierKeyCodes.contains(keyCode)
}

func isShortcutLike(keyCode: Int64, flags: CGEventFlags) -> Bool {
    guard !isActualModifierKey(keyCode) else { return false }

    let hasCommand = flags.contains(.maskCommand)
    let hasControl = flags.contains(.maskControl)
    let hasOption = flags.contains(.maskAlternate)

    // Shift is allowed as an extra modifier, but Shift alone is too close to typing.
    return hasCommand || hasControl || hasOption
}

func shouldRedirect(type: CGEventType, event: CGEvent) -> Bool {
    guard type == .keyDown || type == .keyUp else { return false }

    let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
    let flags = event.flags

    /*
    If you want to keep a few local macOS shortcuts active even while an excluded
    app is frontmost, you could add a carve-out like this:

    let hasCommand = flags.contains(.maskCommand)
    let hasControl = flags.contains(.maskControl)

    if hasCommand && keyCode == 48 { return false }  // Cmd+Tab
    if hasCommand && keyCode == 49 { return false }  // Cmd+Space
    if hasCommand && keyCode == 50 { return false }  // Cmd+`
    if hasControl && keyCode == 123 { return false } // Ctrl+LeftArrow
    if hasControl && keyCode == 124 { return false } // Ctrl+RightArrow
    */

    return isShortcutLike(keyCode: keyCode, flags: flags)
}

func callback(
    proxy: CGEventTapProxy,
    type: CGEventType,
    event: CGEvent,
    refcon: UnsafeMutableRawPointer?
) -> Unmanaged<CGEvent>? {
    if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
        log("event tap disabled; re-enabling")
        if let eventTap {
            CGEvent.tapEnable(tap: eventTap, enable: true)
        }
        return nil
    }

    guard let excludedApp = frontmostExcludedApp() else {
        return Unmanaged.passUnretained(event)
    }

    guard shouldRedirect(type: type, event: event) else {
        return Unmanaged.passUnretained(event)
    }

    let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
    if verboseLogging {
        let name = excludedApp.localizedName ?? "excluded app"
        log("posting keyCode=\(keyCode) to \(name) pid=\(excludedApp.processIdentifier)")
    }

    if let copiedEvent = event.copy() {
        copiedEvent.postToPid(excludedApp.processIdentifier)
    } else {
        log("failed to copy event before repost")
    }

    // Swallow the original event so local Raycast shortcuts do not handle it.
    return nil
}

let eventMask =
    (1 << CGEventType.keyDown.rawValue) |
    (1 << CGEventType.keyUp.rawValue)

guard let tap = CGEvent.tapCreate(
    tap: .cghidEventTap,
    place: .headInsertEventTap,
    options: .defaultTap,
    eventsOfInterest: CGEventMask(eventMask),
    callback: callback,
    userInfo: nil
) else {
    log("failed to create event tap")
    log("grant Input Monitoring permission to this binary or Terminal, then retry")
    exit(1)
}

eventTap = tap

let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)

log("Raycast shortcut filter running")
log("redirecting Cmd/Ctrl/Opt shortcut chords while an excluded app is frontmost")
log("press Ctrl+C in this terminal to stop")

CFRunLoopRun()

Save it in ~/raycast-shortcut-filter.swift.

Compile it:

swiftc ~/raycast-shortcut-filter.swift -o ~/raycast-shortcut-filter

Run it:

~/raycast-shortcut-filter

On the first run, macOS will probably prompt for Accessibility permissions.

Running as a Service

The obvious next step for having it run in the background automatically is a LaunchAgent.

First, move the compiled binary somewhere more permanent:

mkdir -p ~/bin
mv ~/raycast-shortcut-filter ~/bin/raycast-shortcut-filter
chmod +x ~/bin/raycast-shortcut-filter

Then create ~/Library/LaunchAgents/com.shortcut-filter.plist (USERNAMEneeds to be an actual username):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>com.shortcut-filter</string>

	<key>ProgramArguments</key>
	<array>
		<string>/Users/USERNAME/bin/raycast-shortcut-filter</string>
	</array>

	<key>RunAtLoad</key>
	<true/>

	<key>KeepAlive</key>
	<true/>

	<key>StandardOutPath</key>
	<string>/tmp/raycast-shortcut-filter.log</string>

	<key>StandardErrorPath</key>
	<string>/tmp/raycast-shortcut-filter.log</string>
</dict>
</plist>

To load:

launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.shortcut-filter.plist

To reload after plist changes:

launchctl bootout "gui/$(id -u)" ~/Library/LaunchAgents/com.shortcut-filter.plist
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.shortcut-filter.plist

To stop it entirely:

launchctl bootout "gui/$(id -u)" ~/Library/LaunchAgents/com.shortcut-filter.plist

So far it has been running well. Almost forgot I had it.

built with SvelteKit. deployed on Azure Static Web Apps.