#2826·yabai

Proposal: standalone Apple Silicon scripting-addition loader with SIP enabled

Author: retX0Created Sep 11, 2026Updated Sep 11, 2026

Question

Would you consider replacing the current Apple Silicon scripting-addition injection path with a standalone arm64e payload mapped into Dock through debugserver/RSP?

I am opening an issue first because pull requests are disabled for this repository, and because this is a larger architectural change whose maintenance trade-offs should be discussed before asking for a merge.

Main benefit: SIP stays enabled

With this approach, all SIP protections can remain enabled except Debugging Restrictions. The required Recovery configuration is:

bash
csrutil enable --without debug

In particular, it does not require:

  • unrestricted filesystem access / --without fs;
  • the -arm64e_preview_abi boot argument.

The loader still requires root and CSR_ALLOW_TASK_FOR_PID, as expected for attaching to Dock.

Why change the current path?

The existing Apple Silicon loader injects shellcode that calls dlopen on the arm64e payload bundle inside Dock. On current macOS, that path depends on the arm64e preview ABI boot argument and broader SIP configuration.

The proposed implementation instead:

  1. builds the scripting-addition payload as a standalone arm64e Mach-O with zero unresolved symbols;
  2. embeds that image in the loader;
  3. maps its segments directly into Dock, preserving layout and reserving full vmsize including BSS;
  4. uses debugserver's Remote Serial Protocol to save a Dock thread's state, start the payload, wait for initialization, and restore the exact thread state;
  5. resolves runtime APIs from inside Dock through dlsym.

The debugserver/RSP transport and thread-hijack flow are adapted from my retX0/amfree project.

macOS 26 behavior restored

The standalone port also contains the macOS 26 fixes required for the complete scripting-addition command set, including:

  • correct Dock private calling conventions (x20 for Swift/private self);
  • space create/destroy/reorder and cross-display movement;
  • window move, opacity/fade, layer, sticky, shadow, scale and ordering;
  • window-to-space and window-list-to-space;
  • proxy in/out transactions;
  • 16 KiB alignment for the animation patch;
  • window focus through WindowManager.shared, synchronouslyRequestLayoutControl(), and synchronouslyActivateWindow(UInt32) instead of the old ShowFront helper pattern.

Verification

Tested on Apple Silicon, macOS 26.6.2 (25G83):

  • clean build: pass;
  • handshake: 2.1.31, capabilities 0x7f;
  • standalone payload: zero unresolved symbols;
  • every scripting-addition opcode was behaviorally exercised;
  • same-display and cross-display space operations passed;
  • opacity fade basic/retarget/cancel/multi-window tests passed;
  • focus activated the application, made the requested NSWindow key, and moved it to WindowServer rank 0;
  • proxy ordering changed in/out and captured system alpha changed 1.00 -> 0.00 -> 1.00;
  • Dock PID remained unchanged throughout the final regression run.

Implementation branch:

https://github.com/retX0/yabai/tree/feat/dock-hijack-loader

The work is split into three commits for review:

  1. debugserver/RSP injection support;
  2. standalone arm64e payload and Mach-O mapper;
  3. removal of obsolete loader prerequisites.

Trade-offs / questions

This adds more loader code and depends on Xcode's debugserver. The Dock patterns/private Swift symbols also remain OS-version-sensitive, as with the existing scripting addition.

Before doing further integration work, I would like feedback on:

  1. Is keeping SIP enabled except for debugging a worthwhile reason to replace the current arm64e dlopen path?
  2. Is the debugserver/Xcode dependency acceptable for scripting-addition users?
  3. Would you prefer this as one architecture change, smaller incremental changes, or maintained only as an external fork?