Patching Raycast to Support Longer Pastes

cantpaste.gif

I like Raycast's Ask AI mode a lot: you type a question into the spotlight search bar followed by Tab and your assistant of choice (in this case, Gemini 2.5 Flash Lite) gives you the answer. However, for some reason, there is a max char limit on this field, which is really annoying when I am pasting stack traces or code in for a quick answer. Raycast's solution to this is to use the full "AI Chat" experience, but that ruins the point of the "Quick AI". So onto disassembling and patching it is.

Locating the Ask AI UI

JS??

My first thought was that the Spotlight "Ask AI" UI might be a web view backed by a JS bundle with a front-end maxlength or similar client-side restriction.

Raycast ships a web bundle at /Applications/Raycast.app/Contents/Resources/RaycastWeb_RaycastWeb.bundle/Contents/Resources/js/index.js

The limit might live within a React input prop like maxLength or a literal string mentioning character limits

Listing the bundle contents and confirmed that Raycast has both

  • a native Mach-O code at Contents/MacOS/Raycast
  • a web bundle at Contents/Resources/RaycastWeb_RaycastWeb.bundle/.../js/index.js

Searching the JS bundle for likely indicators found no obvious Spotlight/Ask-AI paste limit and no obvious maxlength gate tied to the root search box:

> rg -n "maxlength|maxLength|character limit|char limit|too long|Ask AI" Contents/Resources/RaycastWeb_RaycastWeb.bundle/Contents/Resources/js/index.js
<nothing>

strings!

Now, time for strings

> strings Contents/MacOS/Raycast | rg -n "maxlength|maxLength|character limit|char limit|too long|Ask AI"

16639:Set up a hotkey to access Ask AI from anywhere
16671:The text you are trying to paste is too long
16703:Start a new chat after the specified timeout when opening Ask AI or Quick AI
20055:Querying shortcuts through child process (legacy) took too long
20057:Querying shortcuts through child process took too long
20062:Querying shortcuts through Scripting Bridge took too long
20341:Keyword is too long
22521:Show Ask AI hint in root search
30157:maxLength
31159:statement too long
39365:input is too long
40479:Data too long
57019:read:maxLength:
62787:write:maxLength:
70741:maxLength
71118:maxLength
74032:maxLength
83776:Ask AI

This suggested that the limit was native and not front-end JS and also told us that Raycast uses a dedicated paste-specific character limit.

Objective-C / Swift metadata

Using otool on the Mach-O to inspect class metadata and ivars:

> otool -ov Contents/MacOS/Raycast | rg -n -C 40 "pasteMaxCharactersCount"
109046-                    types   0x1d45f0 (0x1027b4200 extends past end of file)
109047-                    imp     0x0
109048-                    name    0x7e6100 (0x102dc5d18 extends past end of file)
109049-                    types   0x1d45e4 (0x1027b4200 extends past end of file)
109050-                    imp     0x0
109051-                    name    0x7e60fc (0x102dc5d20 extends past end of file)
109052-                    types   0x1af818 (0x10278f440 extends past end of file)
109053-                    imp     0x0
109054-                    name    0x7e60f8 (0x102dc5d28 extends past end of file)
109055-                    types   0x248144 (0x102827d78 extends past end of file)
109056-                    imp     0x0
109057-                    name    0x7e60f4 (0x102dc5d30 extends past end of file)
109058-                    types   0x1af850 (0x10278f490 extends past end of file)
109059-                    imp     0x0
109060-                    name    0x7e60e8 (0x102dc5d30 extends past end of file)
109061-                    types   0x1af844 (0x10278f490 extends past end of file)
109062-                    imp     0x0
109063-                    name    0x7e60e4 (0x102dc5d38 extends past end of file)
109064-                    types   0x1af838 (0x10278f490 extends past end of file)
109065-                    imp     0x0
109066-                    name    0x7e60d8 (0x102dc5d38 extends past end of file)
109067-                    types   0x1af82c (0x10278f490 extends past end of file)
109068-                    imp     0x0
109069-                    name    0x7e60d4 (0x102dc5d40 extends past end of file)
109070-                    types   0x1af850 (0x10278f4c0 extends past end of file)
109071-                    imp     0x0
109072-                    name    0x7e60c8 (0x102dc5d40 extends past end of file)
109073-                    types   0x1af844 (0x10278f4c0 extends past end of file)
109074-                    imp     0x0
109075-                optionalClassMethods 0x100000000 __mh_execute_header
109076-                instanceProperties 0x100000000 __mh_execute_header
109077-        ivars          0x102d8d520
109078-            entsize   32
109079-            count     5
109080-            offset    0x102f21f60 90
109081-            name      0x102872ef0 calculatesHorizontalIntrinsicContentSize
109082-            type      0x102785910
109083-            alignment 0
109084-            size      1
109085-            offset    0x102f21f68 96
109086:            name      0x102872ff0 pasteMaxCharactersCount
109087-            type      0x102785910
109088-            alignment 3
109089-            size      9
109090-            offset    0x102f21f70 112
109091-            name      0x102873010 reachedPasteMaxCharacterCountHandler
109092-            type      0x102785910
109093-            alignment 3
109094-            size      16
109095-            offset    0x102f21f78 128
109096-            name      0x102872bf0 changeHandler
109097-            type      0x102785910
109098-            alignment 3
109099-            size      16
109100-            offset    0x102f21f80 144
109101-            name      0x102872c00 shouldChangeHandler
109102-            type      0x102785910
109103-            alignment 3
109104-            size      16
109105-        weakIvarLayout 0x100000000 __mh_execute_header
109106-        baseProperties 0x102d8d5c8
109107-            entsize    16
109108-            count      2
109109-            name       0x102873040 placeholderAttributedString
109110-            attributes 0x102873060 T@"NSAttributedString",N,&
109111-            name       0x10278cac0 intrinsicContentSize
109112-            attributes 0x10278cae0 T{CGSize=dd},N,R
109113-Meta Class
109114-    isa        0x0 _OBJC_METACLASS_$_NSObject
109115-    superclass 0x102f21f98
109116-    cache      0x0 __objc_empty_cache
109117-    vtable     0x100000000 __mh_execute_header
109118-    data       0x102d8d4c8
109119-        flags          0x81 RO_META
109120-        instanceStart  40
109121-        instanceSize   40
109122-        reserved       0x0
109123-        ivarLayout     0x100000000 __mh_execute_header
109124-        name           0x102872fc0 _TtC9RaycastUI19SingleLineTextField
109125-        baseMethods    0x100000000 __mh_execute_header
109126-        baseProtocols  0x102d8d4b8
zimengx@U7 /A/Raycast.app
>

We see some more names, notably _TtC9RaycastUI19SingleLineTextField, which represents the native class RaycastUI.SingleLineTextField

Its ivars included:,pasteMaxCharactersCount, reachedPasteMaxCharacterCountHandler, changeHandler, and shouldChangeHandler

This meant that the Spotlight input isn't actually a stock AppKit search field but a custom field with an explicit paste cap.

Live-process inspection with LLDB

Knowing this, we can inspect the live Raycast process:

> pgrep -fl Raycast

1213 /bin/sh /Users/zimengx/Documents/Scripts/Raycast/launchSchoolBrowser.sh
1317 /Applications/Raycast.app/Contents/Extensions/RaycastAppIntents.appex/Contents/MacOS/RaycastAppIntents -LaunchArguments eyJlbmhhbmNlZFNlY3VyaXR5IjpmYWxzZSwidHlwZSI6MSwic2VydmljZU5hbWUiOiJjb20ucmF5Y2FzdC5tYWNvcy5SYXljYXN0QXBwSW50ZW50cyJ9
14807 /Applications/Raycast.app/Contents/MacOS/Raycast

Ignoring 1213 and 1317 for obvious reasons, we attach with LLDB and enumerate windows:

> lldb -b -p 14807 \
  -o 'expr -l objc++ -O -- @import AppKit; id app=(id)[NSApplication sharedApplication]; NSArray<NSWindow*> *wins=(NSArray<NSWindow*> *)[app windows]; NSMutableArray *a=[NSMutableArray array]; for (NSWindow *w in wins) { [a addObject:[NSString stringWithFormat:@"%@|%@|%@|level=%ld|key=%@|main=%@", NSStringFromClass([w class]), [w title], [w isVisible]?@"visible":@"hidden", (long)[w level], [w isKeyWindow]?@"yes":@"no", [w isMainWindow]?@"yes":@"no"]]; } a' \
  -o 'detach' -o 'quit'

We found RaycastApp.MainWindow, then traversing that window's view tree we hit our previously found RaycastUI.SingleLineTextField

This confirmed that the visible Spotlight input uses the custom text field with the paste limit ivar.

Selector and method mapping

We mapped the SingleLineTextField methods using Objective-C metadata

MethodLocation
placeholderAttributedString0x101f93b78
textDidChange:0x101f93cac
textView:shouldChangeTextInRange:replacementString:0x101f921b8

The method of interest was RaycastUI.SingleLineTextField textView:shouldChangeTextInRange:replacementString:, as it forwards into a helper that enforces the policy.

Applying the Patch

Disassembly

> otool -tvV Contents/MacOS/Raycast | sed -n '/101f93cac/,/101f93ea4/p'
> otool -tvV Contents/MacOS/Raycast | sed -n '/101f921b8/,/101f922f0/p'
0000000101f93cac        stp     x28, x27, [sp, #-0x60]!
0000000101f93cb0        stp     x26, x25, [sp, #0x10]
0000000101f93cb4        stp     x24, x23, [sp, #0x20]
0000000101f93cb8        stp     x22, x21, [sp, #0x30]
0000000101f93cbc        stp     x20, x19, [sp, #0x40]
0000000101f93cc0        stp     x29, x30, [sp, #0x50]
0000000101f93cc4        add     x29, sp, #0x50
0000000101f93cc8        mov     x25, x5
0000000101f93ccc        mov     x19, x4
0000000101f93cd0        mov     x21, x3
0000000101f93cd4        mov     x24, x2
0000000101f93cd8        mov     x23, x0
0000000101f93cdc        mov     x0, #0x0
0000000101f93ce0        bl      0x1025b5688 ; symbol stub for: _$sScMMa
...truncated

There was a critical portion of the helper around 0x101f92220

0000000101f92220 ldr x25, [x25]
0000000101f92224 mov x0, x22
0000000101f92228 mov x1, x21
0000000101f9222c bl  _$sSS5countSivg
0000000101f92230 cmp x25, x0
0000000101f92234 b.ge 0x101f9228c

where x25 is the configured pasteMaxCharactersCount, x0 is the count of the incoming replacement string. So if limit >= pasted_length, it would branch to the normal allow path, otherwise, fall through to the "too long" path

Immediately after the failed comparison, the code loads the reachedPasteMaxCharacterCountHandler closure and invokes it, then returns false.

We could've found where the limit was set, and then adjust that, but that would've required discovering the runtime value, as well as identify every controller that configures the field. Patching the policy is much easier.

Calculating the file offset

I mapped the VM address to a file offset. The patch target had virtual address 0x101f92234. The computed file offset was0x1f92234

xxd -g 4 -l 32 -s $((0x1f92230)) Contents/MacOS/Raycast

01f92230: 3f0300eb ca020054 687c00f0 08b947f9

Where ca020054 encodes the conditional branch.

Patching bytes

I replaced the conditional branch with an unconditional branch to the same allow target.

cp Contents/MacOS/Raycast Contents/MacOS/Raycast.bak-paste-limit
printf '\x16\x00\x00\x14' | dd of=Contents/MacOS/Raycast bs=1 seek=$((0x1f92234)) conv=notrunc

With the patched bytes:

01f92230: 3f0300eb 16000014 687c00f0 08b947f9

Re-signing and cleanup

Because the app bundle was modified, we have to re-sign it with an ad-hoc signature:

codesign --force --sign - /Applications/Raycast.app

Now we can paste text as long as we want into Raycast!! Pasting a long piece of text into patched Raycast

I have to say, this is one of the funner things I've done recently, RE is always thrilling 🙂.

← blog