Breaking Into Spotlight
Getting my own code running inside macOS Spotlight on Apple Silicon.
Note: you can check out a demo project for this post here.
I wanted my own code running inside macOS Spotlight, so I could change what happens when you type.
Now, this is much easier said than done. Especially on Apple Silicon.
- 01The plan: get a dylib into a process I don’t own
- 02Easy mode: DYLD_INSERT_LIBRARIES
- 03SIP is in the way
- 04Getting a foothold
- 05Just call dlopen
- 06Bootstrapping a real thread
- 07Signing the start routine
- 08The whole machine, start to finish
- 09Pointing it at Spotlight
- 10The entitlements trap
- 11Built for the wrong chip
- 12Finding the hook: SPQueryTask
- 13Swizzling search, and the missing submit hook
The plan: get a dylib into a process I don’t own
First problem: you can’t “edit” a running app. Spotlight is a compiled, code-signed binary if you change it on disk, macOS just refuses to launch it. And I don’t want to touch a file anyway, I want to change a process that’s already running, live, in memory.
The unit of “code you can load into a live process” on macOS is a dynamic library, and dylibs have one lovely feature: a function marked __attribute__((constructor)) runs the instant the library loads, before anyone calls into it. So my whole payload can be this:
#include <stdio.h>
__attribute__((constructor))
static void wedged_in(void) {
printf("[payload] hello from inside the process!\n");
}Which means the whole project collapses to a single question: how do I get Spotlight to call dlopen on that file?
Easy mode: DYLD_INSERT_LIBRARIES
macOS ships a mechanism for exactly this: the dynamic linker loads any dylib named in DYLD_INSERT_LIBRARIES into a process at launch. It’s essentially the same thing as LD_PRELOAD on Linux.
clang -dynamiclib -o payload.dylib payload.c
DYLD_INSERT_LIBRARIES=./payload.dylib ./MyDummyApp
# [payload] hello from inside the process!There it is. So now point the same thing at something Apple ships:
DYLD_INSERT_LIBRARIES=./payload.dylib /System/.../Spotlight
# ...nothing.Nothing. The linker drops the variable. This is going to be the heart of many of the errors we see. The kernel is going to be playing hard to get. And we’ll mostly be trying to work backwards to figure out what the actual error is. This one is SIP.
SIP is in the way
SIP is a kernel-enforced policy that restricts even root. When the linker launches a restricted binary (anything Apple-signed and protected), it strips the “dangerous” environment variables on the way in, and DYLD_INSERT_LIBRARIES is top of that list. That’s where my variable went.
It also blocks nearly everything else I’m about to try, so it has to come down. You can only toggle it from the recovery environment. A running system isn’t allowed to disable its own guardrails, which is the entire point of them. While I’m in there I also relax AMFI, and the reason for that turns into a whole wall of its own later:
csrutil disable
# back in the booted OS:
sudo nvram boot-args="amfi_get_out_of_my_way=1"
sudo rebootNow the variable works on system binaries again, but only at launch. Spotlight is already up, and I don’t want to kill and relaunch it. Mainly because it’s an OS-managed process and it can be killed and relaunched any time by macOS. I want to climb into the process that’s running right now. I need a way to reach into a live process.
Getting a foothold
macOS is a Mach kernel wearing a BSD userland, and Mach’s central abstraction is the port. The one I want is a process’s task port. If I can hold it, I can read and write that process’s memory and spawn threads inside it.
So I carve out two regions in the target. A stack, since a fresh thread needs one, and a page for my code. Write my bytes in, and flip the code page executable:
task_t task;
task_for_pid(mach_task_self(), pid, &task); // the capability
mach_vm_address_t stack = 0, code = 0;
mach_vm_allocate(task, &stack, STACK_SIZE, VM_FLAGS_ANYWHERE);
mach_vm_allocate(task, &code, sizeof(shellcode), VM_FLAGS_ANYWHERE);
mach_vm_write(task, code, (vm_offset_t)shellcode, sizeof(shellcode));
vm_protect(task, code, sizeof(shellcode), false,
VM_PROT_READ | VM_PROT_EXECUTE);Before anything as ambitious as a dylib, I just want to prove a thread over there runs my bytes. So the whole shellcode, for now, is three instructions. Drop a recognizable marker in x0 and spin:
movz x0, #0x7274
movk x0, #0x6d6f, lsl #16 ; x0 = 0x6d6f7274: An arbitrary number that just says "mort" in ascii
spin: b spinNow I point a thread at it, but here is where we have to be very careful. Because of a chip feature called Pointer Authentication (PAC). On Apple Silicon a lot of pointers aren’t plain addresses anymore. A 64-bit pointer only needs about 48 bits to name any real address, which leaves the top bits spare, so the CPU tucks a small cryptographic signature up there. Before it uses a signed pointer (jumps to it, returns through it), it recomputes that signature and checks it. It’s there to stop an attacker from redirecting execution by overwriting a pointer, which, I’ll admit, is a fair description of what I’m about to do lol.

The program counter I hand the kernel, “start the thread here”, is one of those signed pointers. So I can’t pass the bare address. The CPU would authenticate it, find no valid signature, and trap. I have to hand over a properly signed pointer. Which sounds impossible: the signing key lives inside the CPU and can’t be read, so how am I supposed to produce a signature the target will accept?
The critical detail this whole technique rests on is that PAC doesn’t have one key, it has several, and their scope is everything:

Code pointers are signed with the IA key, and on macOS IA is shared system-wide, not per-process. (The per-process keys are IB, DB, and GA, for return addresses and high-value pointers inside a single program.) This single concept is why all of this can work: because IA is shared, a code pointer I sign in my process authenticates perfectly inside the target. I never touch the target’s secrets. I sign the PC with IA and the signature is good everywhere:
arm_thread_state64_t ts = {};
__darwin_arm_thread_state64_set_pc_fptr( // sign the entry PC with the IA key...
ts, ptrauth_sign_unauthenticated((void *)code, ptrauth_key_asia, 0));
__darwin_arm_thread_state64_set_sp(ts, stack + STACK_SIZE / 2);
thread_act_t thread;
thread_create_running(task, ARM_THREAD_STATE64, // ...and since IA is shared, the target accepts it
(thread_state_t)&ts, count, &thread);And the injector, pointed at the dummy:
[*] code @ 0x102db8000 stack @ 0x102db4000
[*] launched. polling for marker...
[+] marker seen, our code ran inside the target.That direct launch is all my own app needs, but it falls over on Spotlight, and the thread state is why. To start a thread you hand the kernel a block of register values: where to begin (the PC), what stack to use. On my own app I can build that block and pass it straight to thread_create_running. A hardened process like Spotlight won’t take a hand-built state like that. The kernel only accepts one that has first been run through thread_convert_thread_state, which rewrites it into the kernel’s own internal form. And that conversion has to be done against a real thread that already exists in the target. So the launch grows from one call into four:
arm_thread_state64_t ts = {}, mts = {};
__darwin_arm_thread_state64_set_pc_fptr(
ts, ptrauth_sign_unauthenticated((void *)code, ptrauth_key_asia, 0));
__darwin_arm_thread_state64_set_sp(ts, stack + STACK_SIZE / 2);
thread_act_t thread;
thread_create(task, &thread); // a blank, suspended thread in the target
thread_convert_thread_state(thread, 2, ARM_THREAD_STATE64,
(thread_state_t)&ts, count,
(thread_state_t)&mts, &mcount);
thread_terminate(thread); // drop the placeholder
thread_create_running(task, ARM_THREAD_STATE64,
(thread_state_t)&mts, mcount, &thread); // launch with the converted stateRead it as one piece of setup plus the real launch. thread_create makes a blank, suspended thread inside Spotlight. It never runs a single instruction; it exists only so the conversion has a real target thread to work against. thread_convert_thread_state (direction 2, “convert from self”) rewrites my state into the form the kernel will accept. thread_terminate throws that placeholder away, because on macOS 14.4+ you can’t load a state into an existing thread and resume it, so the placeholder can’t be the one that runs. And thread_create_running starts a fresh thread with the converted state, which finally lands on my code. Notice what this is not: it doesn’t re-sign the PC. IA is shared, so my signature was already valid over there. The whole dance is only about the shape of the state, not the keys.
Marker seen. My bytes are running on a thread inside another process, so I swap the spin for a dlopen.
Just call dlopen
Those three marker instructions become a call to dlopen(path, RTLD_LAZY). Path in x0, flag in x1, branch:
mov x1, #1 ; RTLD_LAZY
adr x0, lib ; the dylib path, sitting at the end of the blob
adr x9, dlopen_ptr
ldr x9, [x9]
blr x9 ; dlopen(path, RTLD_LAZY)Thread crashed: EXC_BAD_ACCESS (SIGSEGV)
faulting address: 0x808 <- a near-null dereferenceIt faults on a near-null address, 0x808, deep inside libc, before doing anything. The thread I made is a bare Mach thread, and a bare Mach thread is not a pthread. Mach threads are a macOS primitive that don’t have any of what POSIX threads have, like thread-local storage or errno, which libSystem quietly assumes is there. So the first time dlopen reaches for its thread-local slot, it follows a pointer that was never set up and lands down at 0x808. Basically, dlopen expects that the thread it’s running on is a pthread, but we gave it a Mach thread.
Bootstrapping a real thread
So the shellcode can’t make the call itself, it has to spin up a real pthread first (luckily there’s a pthread_create_from_mach_thread function for this exact situation) and let that thread do the dlopen. Our ASM grows into two routines: stage A creates the pthread, stage B loads the dylib:
_start:
sub sp, sp, #16
mov x0, sp ; &new_thread
mov x1, xzr ; attr = NULL
adr x2, _thread ; start routine
mov x3, xzr
adr x9, pcreate_ptr
ldr x9, [x9]
blr x9 ; pthread_create_from_mach_thread(...)
movz x0, #0x7274
movk x0, #0x6d6f, lsl #16
spin: b spin
_thread: ; runs on the real pthread
str x30, [sp, #-16]!
mov x1, #1 ; RTLD_LAZY
adr x0, lib
adr x9, dlopen_ptr
ldr x9, [x9]
blr x9 ; dlopen(path, RTLD_LAZY)
ldr x30, [sp], #16
retThe two function pointers and the path get written into placeholder slots at runtime, resolved in my process. It’s understanding why we actually ptrauth_strip. The reason is that our assembly uses a regular branch instruction, so the pointer we pass in cannot be signed, since a regular branch does not expect a signed pointer. It would just crash. I’m honestly not sure about this, but found out that there’s an instruction called BLRA that branches on an authenticated pointer. I have no idea how that instruction actually works, so I figured I’d just strip the authenticated pointer and use a regular branch. Then everything gets copied in at fixed offsets:
uint64_t pcreate = (uint64_t)ptrauth_strip(
dlsym(RTLD_DEFAULT, "pthread_create_from_mach_thread"),
ptrauth_key_function_pointer);
uint64_t dl = (uint64_t)ptrauth_strip((void *)dlopen,
ptrauth_key_function_pointer);
memcpy(shellcode + 48, &pcreate, 8);
memcpy(shellcode + 88, &dl, 8);Thread crashed
faulting address: 0x10495c038 <- back inside my own injected code pageWE’RE GETTING CLOSER!!!! But the new pthread dies the instant it starts, and the fault lands right back inside my own shellcode page. That’s Pointer Authentication again: _pthread_start does an authenticated branch to the start routine I handed it, the pointer is unsigned, the check fails, and the branch ends up somewhere it shouldn’t. This time it’s about the pointer I passed to pthread, not the entry PC.
Signing the start routine
Same thing as the entry PC, one layer in. Before _pthread_start branches to my _thread routine it authenticates that pointer against the process’s key, and I handed it a raw one. The fix is a single instruction, paciza x2. paciza is an asm instruction that signs a pointer in a register with the IA key. So now our routine is signed:
adr x2, _thread ; start routine
paciza x2 ; <-- sign it. this one line is the whole fix.
mov x3, xzr
adr x9, pcreate_ptrRebuild, re-inject, and in the dummy app’s log:
[payload] hello from inside the process!WE HAVE SUCCESSFULLY INJECTED OUR DYLIB. There’s only one issue though... this is on a dummy app. Now we actually have to point it at Spotlight...
The whole machine, start to finish
Now that every piece is in place, it’s worth stepping through exactly what happens when I run the injector against a PID, because it’s a lot of small parts and the order is the whole trick:
One, task_for_pid gives me the target’s task port, the capability to touch its memory and its threads. Two, I allocate a stack and a code page in the target and mark the code page executable. Three, I write in the two-stage shellcode, with the three placeholder slots patched: the stripped addresses of pthread_create_from_mach_thread and dlopen, and the payload path. Four, I sign the entry PC with the IA key, which, because IA is shared system-wide, the target authenticates as valid with no re-keying needed.
Five, I create a running thread with that state and let it go. It lands on stage A, the bare Mach thread, whose only job is to call pthread_create_from_mach_thread with a properly paciza-signed start routine, then spin. Six, the kernel spins up a real pthread and drops it onto stage B. Seven, stage B, now a civilized POSIX thread with all the bookkeeping libc wants, calls dlopen on my path. Eight, the linker maps the dylib, sees the constructor attribute, and runs it.
So: Spotlight.
Repointing at Spotlight
Alright, Spotlight’s PID in place of the dummy’s. No way this works, right?
sudo ./inject <spotlight-pid> ./payload.dylib
[!] thread_create_running: (os/kern) protection failureaaaaaaaand kernel failure. A hardened system process is a different animal but the same beast🏀. I think we’re close.
The entitlements trap
One thing I haven’t really mentioned is the concept of entitlements and code signing, which is something unique to macOS. Basically, binaries can be signed with permissions. For invasive function calls, binaries must have the correct permission to call those functions, or the kernel will error. My gut said I needed more privilege, so I added entitlements like com.apple.system-task-ports or task_for_pid-allow. And I was basically stuck at this step for days.
After genuinely scouring the depths of the internet, I found this article that basically said Apple apps won’t load if they use entitlements they aren’t supposed to have.
clang -arch arm64e -framework Cocoa -o inject inject.m
codesign -fs - inject # ad-hoc, NO --entitlements at allFor the record: this one cost me the better part of a week, and the error message pointed nowhere near the cause. That one’s on Apple.
Finally, we can build it and try it. both halves have to be arm64e, the injector, so the PAC intrinsics actually emit, and the payload, so Spotlight will accept it:
clang -arch arm64e -dynamiclib -fobjc-arc \
-framework Cocoa -framework WebKit \
-o payload.dylib spotlight_swizzle.mRebuild as arm64e, re-inject, and watch the log stream for the live Spotlight process:
[hack] loaded into SpotlightI’m inside Spotlight. Now the fun part actually making it do something 💀.
Finding the hook: SPQueryTask
A foothold and a printf is nothing by itself. To change what Spotlight does, I need the one method that handles what you type, and then I need to replace it out from under it.
From this point on, it’s classic macOS reverse engineering. There’s almost certainly a method on Spotlight’s text field that swallows your keystrokes in real time and updates the UI as you type (that’s how Spotlight does its live results). My intuition is if I can find that method, swizzle it, and route it to my own implementation whenever the query starts with a prefix like $, I should be able to type any command I want and have Spotlight run it. So I just dumped the symbol table with nm and grepped for anything that smelled like a query:
nm /System/.../Spotlight | grep -i queryOne line jumped out of the list: +[SPQueryTask _queriesForUserQuery:queryContext:]. The name is almost too convenient, a class method (that leading +) that takes a user query and hands back queries. So I opened it in Ghidra, the disassembler of my choice (since it’s free and I’m broke 💀, someone please donate me a Hopper license) to take a better look:
the Ghidra decompilation of +[SPQueryTask _queriesForUserQuery:queryContext:]
/* WARNING: Function: _objc_retain replaced with injection: _objc_retain_fixup */
/* WARNING: Function: _objc_retainAutoreleasedReturnValue replaced with injection:
_objc_retain_fixup */
/* WARNING: Function: _objc_release replaced with injection: _objc_release_fixup */
/* WARNING: Function: _objc_autoreleaseReturnValue replaced with injection: _objc_retain_fixup */
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
undefined8
+[SPQueryTask__queriesForUserQuery:queryContext:]
(undefined8 param_1,undefined8 param_2,undefined8 param_3,undefined8 param_4)
{
code *pcVar1;
undefined8 uVar2;
undefined8 uVar3;
undefined8 uVar4;
undefined8 uVar5;
ulong uVar6;
ulong uVar7;
long lVar8;
long lVar9;
long lVar10;
ulong uVar11;
long *plVar12;
long lVar13;
long lVar14;
long lVar15;
long lVar16;
ulong unaff_x30;
undefined8 local_130;
long lStack_128;
long *local_120;
undefined8 uStack_118;
undefined8 local_110;
undefined8 uStack_108;
undefined8 uStack_100;
undefined8 uStack_f8;
undefined1 auStack_f0 [128];
long local_70;
local_70 = *_DAT_1e590abe0;
uVar2 = _DAT_1e588c7f8;
_objc_opt_new();
uVar3 = param_4;
_objc_msgSend$options();
uVar4 = param_4;
_objc_msgSend$isAppOnlySearch();
if ((int)uVar4 == 0) {
uVar6 = DAT_1e68ce660;
_objc_msgSend$disabledGroups();
uVar7 = uVar6;
_objc_msgSend$containsObject:();
_objc_msgSend$enableConversion:(DAT_1e68ce6d0,param_2,(uint)uVar7 ^ 1);
uVar7 = uVar6;
_objc_msgSend$containsObject:(uVar6,param_2,&cfstringStruct_1f0317970);
_objc_msgSend$enableCalculator:(DAT_1e68ce6d0,param_2,(uint)uVar7 ^ 1);
uVar7 = uVar6;
_objc_msgSend$containsObject:(uVar6,param_2,&cfstringStruct_1f0317b70);
_objc_msgSend$enableDictionary:(DAT_1e68ce6e8,param_2,(uint)uVar7 ^ 1);
uVar4 = param_4;
_objc_msgSend$isSearchToolClient();
if ((int)uVar4 == 0) {
if (_queryClasses.onceToken != -1) {
+[SPQueryTask__queriesForUserQuery:queryContext:].cold.1();
}
plVar12 = &_queryClasses.queryClasses;
}
else {
if (_queryClassesSearchTool.onceToken != -1) {
+[SPQueryTask__queriesForUserQuery:queryContext:].cold.2();
}
plVar12 = &_queryClassesSearchTool.queryClasses;
}
lVar14 = *plVar12;
lStack_128 = 0;
local_130 = 0;
uStack_118 = 0;
local_120 = (long *)0x0;
uStack_108 = 0;
local_110 = 0;
uStack_f8 = 0;
uStack_100 = 0;
lVar8 = lVar14;
_objc_msgSend$countByEnumeratingWithState:objects:count:
(lVar14,param_2,&local_130,auStack_f0,0x10);
if (lVar8 != 0) {
lVar15 = *local_120;
do {
lVar13 = 0;
do {
if (*local_120 != lVar15) {
_objc_enumerationMutation(lVar14);
}
lVar16 = *(long *)(lStack_128 + lVar13 * 8);
lVar9 = lVar16;
_objc_msgSend$searchDomain(lVar16);
uVar7 = DAT_1e68ce660;
_objc_msgSend$disabledSearchDomains();
uVar4 = DAT_1e68ce670;
_objc_opt_class(DAT_1e68ce670);
lVar10 = lVar16;
_objc_msgSend$isEqualTo:(lVar16,param_2,uVar4);
if (((int)lVar10 == 0) ||
(uVar11 = uVar6, _objc_msgSend$containsObject:(uVar6,param_2,&cfstringStruct_1f03191d0)
, (uVar11 & 1) == 0)) {
uVar4 = _DAT_1e5899c18;
_objc_msgSend$numberWithUnsignedInt:(_DAT_1e5899c18,param_2,lVar9);
_objc_msgSend$containsObject:(uVar7,param_2,uVar4);
if (((uVar7 & 1) == 0) &&
((lVar10 = lVar16, _objc_msgSend$isQuerySupported:(lVar16,param_2,uVar3),
(int)lVar10 != 0 &&
(uVar4 = param_4, _objc_msgSend$wantsSearchDomain:(param_4,param_2,lVar9),
(int)uVar4 != 0)))) {
_objc_alloc();
uVar4 = param_4;
_objc_msgSend$queryIdent(param_4);
_objc_msgSend$initWithUserQuery:queryGroupId:options:queryContext:
(lVar16,param_2,param_3,uVar4,uVar3,param_4);
uVar4 = DAT_1e68ce6d8;
_objc_msgSend$sharedFeedbackListener(DAT_1e68ce6d8);
_objc_msgSend$setFeedbackListener:(lVar16,param_2,uVar4);
if (lVar16 != 0) {
_objc_msgSend$addObject:(uVar2,param_2,lVar16);
}
}
}
lVar13 = lVar13 + 1;
} while (lVar8 != lVar13);
lVar8 = lVar14;
_objc_msgSend$countByEnumeratingWithState:objects:count:
(lVar14,param_2,&local_130,auStack_f0,0x10);
} while (lVar8 != 0);
}
}
else {
uVar4 = DAT_1e68ce670;
_objc_alloc(DAT_1e68ce670);
uVar5 = param_4;
_objc_msgSend$queryIdent(param_4);
_objc_msgSend$initWithUserQuery:queryGroupId:options:queryContext:
(uVar4,param_2,param_3,uVar5,uVar3,param_4);
uVar3 = DAT_1e68ce6d8;
_objc_msgSend$sharedFeedbackListener(DAT_1e68ce6d8);
_objc_msgSend$setFeedbackListener:(uVar4,param_2,uVar3);
_objc_msgSend$addObject:(uVar2,param_2,uVar4);
}
if (*_DAT_1e590abe0 == local_70) {
if (((unaff_x30 ^ unaff_x30 << 1) >> 0x3e & 1) == 0) {
return uVar2;
}
/* WARNING: Does not return */
pcVar1 = (code *)SoftwareBreakpoint(0xc471,0x19eaca934);
(*pcVar1)();
}
/* WARNING: Subroutine does not return */
___stack_chk_fail(0);
}Don’t actually read all of that. Most of it is lowkey just noise. But if you squint past the noise, the shape is dead simple. The method is handed your query, then it walks every registered query class, tosses the ones that are disabled or don’t support your input, and for each survivor builds a query object straight from your text (the initWithUserQuery:queryGroupId:options:queryContext: call), addObject:s it onto an array, and returns the array. The whole method is just enumerating what to show you, deciding the full list of searches Spotlight runs for whatever you typed.
This is exactly what I want. I can just return my own array from here and I can control what Spotlight does.
So that’s the hook: +[SPQueryTask _queriesForUserQuery:queryContext:], a class method (the + matters, it changes how I have to swizzle it) that runs on every keystroke.
Swizzling search, and the missing submit hook
The runtime doesn’t just let you read methods, it lets you swap their implementations while the process runs (this is what method swizzling is). So I save Spotlight’s original IMP and repoint the selector at a function of mine, and because it’s a class method, I reach for it with class_getClassMethod rather than the instance-method call:
typedef id (*queries_imp_t)(id, SEL, NSString *, id);
static queries_imp_t orig_queriesForUserQuery = NULL;
static void installSwizzle(void) {
Class cls = objc_getClass("SPQueryTask");
Method m = class_getClassMethod(
cls, sel_registerName("_queriesForUserQuery:queryContext:"));
orig_queriesForUserQuery = (queries_imp_t)method_getImplementation(m);
method_setImplementation(m, (IMP)my_queriesForUserQuery);
NSLog(@"[hack] swizzled +[SPQueryTask _queriesForUserQuery:queryContext:]");
}My replacement peeks at the query. If it’s one of my commands, I stash it and hand back an empty array so Spotlight shows no normal results; otherwise I call straight through to the original:
static id my_queriesForUserQuery(id self, SEL _cmd,
NSString *userQuery, id ctx) {
if ([userQuery isKindOfClass:NSString.class] && isCommand(userQuery)) {
gPendingCommand = [userQuery copy]; // remember for the Enter press
return @[]; // suppress normal results
}
gPendingCommand = nil;
return orig_queriesForUserQuery(self, _cmd, userQuery, ctx);
}One last problem. _queriesForUserQuery: fires on every keystroke, there is no “user pressed Enter” method anywhere to swizzle. So I split the job in two: the swizzle handles detection and result-suppression, and a standalone NSEvent local key monitor catches the actual Enter (and Esc, to dismiss). Pending command plus Enter means run it, and swallow the keypress:
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
handler:^NSEvent *(NSEvent *e) {
if (e.keyCode == 53) { hideAllOverlays(); return e; } // Esc
if (e.keyCode == 36 || e.keyCode == 76) { // Return
if (gPendingCommand) {
NSString *cmd = gPendingCommand;
gPendingCommand = nil;
runCommand(cmd);
return nil; // swallow Enter
}
}
return e;
}];And here’s our init func for our dylib:
__attribute__((constructor)) static void pluginMain(void) {
@autoreleasepool {
NSLog(@"[hack] loaded into %@", NSProcessInfo.processInfo.processName);
dispatch_async(dispatch_get_main_queue(), ^{
installSwizzle();
installKeyMonitor();
});
}
}All so I can type terminal commands into Spotlight and watch it execute. Worth it.