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.
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.
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.