ElleKit: a function hooker that patches pages and reassembles the original function
yet another tweak injector / tweak hooking library for darwin systems
At a glance
- What is it?
- ElleKit is a C and Objective-C function hooker for arm64 Darwin systems, with an arm64 assembler, a JIT inline assembly path for Swift, and reimplementations of the Substrate and libhooker APIs. It suits people writing system-level tweaks and instrumentation for Darwin, and it needs a jailbroken or otherwise instrumented arm64 device to run at all.
- Who is it for?
- ElleKit is worth studying if you write hooks on Darwin and want the mechanism rather than a wrapper, because it shows the whole trick in the open, from the branch patch inside 128MB to the exception port beyond it, and because it implements the Substrate and libhooker APIs so existing tweak code ports over.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 84 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Five things in one library, all of them low level
ElleKit describes itself with an unflattering name and a precise list. The repository description calls it yet another tweak injector and tweak hooking library for Darwin systems, and the project documentation opens with what this is.
There are five items in that list. It is a C function hooker that patches memory pages directly. It is an Objective-C function hooker. It is an arm64 assembler. It is a JIT inline assembly implementation for Swift. And it is a Substrate and libhooker API reimplementation.
That last item is what makes the project usable by people who did not write it. The Theos substrate.h header is the interface that a large body of existing Darwin tweak code is written against, and libhooker is the alternative API. By reimplementing both, ElleKit can be dropped in behind code that expects either one, which is why a jailbreak can switch its hooking backend without rewriting its tweaks.
The primary language is Swift, though the assembly and the hooker itself are C, and the licence is BSD-3-Clause. The requirements are short and hard: an arm64 device running the XNU kernel.
The repository tree shows how much is in the package beyond the hooker. There is ellekit/ for the library, ellekitc/ presumably for a command side, injector/ and loader/ for getting code into a process, launcher/ and launchd/ for the startup path, a launchd-plist/ directory with a LAUNCHDAEMON.md guide, sb/ and packaging/ for distribution, logger/, test/, and an Xcode project.
The 128MB boundary decides which hooking technique you get
This is the most interesting thing in the project documentation, and it is worth reading twice because the same operation has two completely different implementations depending on one number.
ElleKit will only ever patch the functions you give it. If you hook a function within 128MB of address space, it makes a simple branch instruction and patches the function with it.
If you hook beyond 128MB of address space, it sets up an exception port to catch all breakpoint exceptions and handle them. It then patches the target with a brk #1 instruction. ElleKit saves the hooks' target and replacement function, and when the exception handler is called, it changes the thread state to redirect execution to the target, then resumes execution. The project documentation says this might sound extremely inefficient but it is not too bad.
The reason for the 128MB figure is the reach of the arm64 branch instruction. A branch can only reach a limited distance, so a hook whose replacement is nearby can be a single branch. When the replacement is out of reach, the hook has to trap instead: a breakpoint exception, caught by the exception port, and the handler does the redirection in software.
The consequences for a user are concrete. The status section notes that it works on functions with 1 instruction beyond 128MB of address space, and that it allows making faster hooks for functions within 128MB of address space. So a hook close to its target runs at native branch speed, and one far away runs through an exception on every call. The first is the common case in a normal process image, where the code and the replacement are close together, and the second is what you hit when the replacement lives in an injected dylib far from the target.
The original function is rebuilt in a new page
Hooking a function means the original implementation still has to be callable, so ElleKit writes the original function to a new memory page and then provides a pointer to it.
The way it does that is the part worth studying. If you use LHHookFunctions, it uses one page for the orig functions, which is faster than allocating a new one for each hook. The orig functions are assembled like this:
// Insert address to target function
movk x16, target_addr % 65536)
movk x16, (target_addr / 65536) % 65536 lsl: 16
movk x16, ((target_addr / 65536) / 65536) % 65536, lsl: 32
movk x16, ((target_addr / 65536) / 65536) / 65536, lsl: 48
// Jump first instruction (the branch to the replacement, aka what we patched)
add x16, x16, 4
// Execute the skipped instruction
[4 first unpatched bytes of the target function]
// Call the target function
br x16Read that as an algorithm. The address of the target is materialised in the x16 register in four chunks, because movk writes a 16-bit immediate at a given shift, so a 64-bit address takes four instructions. The address is then adjusted by 4, which is the size of the branch instruction that was written over the target's first instruction, so x16 ends up pointing at the second instruction of the original function. The four unpatched bytes of the target are then executed inline, and a branch to x16 calls the real function.
The point of the arrangement is that only the first instruction of the target is ever overwritten. Everything after it survives in place, and the relocated copy exists only to restore that first instruction and transfer control. One page holding all the orig functions, rather than one page per hook, keeps the allocation count down, which is what the project documentation means by faster.
What the arm64 assembler and the Swift JIT path add
The assembler is a listed capability, and the project documentation enumerates the instructions it handles: add, sub, b, bl, br, blr, movz, movk, svc, csel with all parameters, ldr and str, and ret and nop.
That list is a good indication of scope. The branch family, b, bl, br and blr, is what a hook needs to redirect control. movz and movk are the immediate materialisation pair, the same pair used in the orig function assembly above, which is how a 64-bit address gets built in a register. svc is the system call instruction, which matters for a library that may need to make a system call from injected code. ldr and str give it memory access, csel gives conditional selection, and ret and nop are the trivial terminators.
The JIT inline assembly implementation for Swift is the part with no equivalent in the older hooking libraries. Swift functions are not ordinary C functions, so a conventional hook that assumes a known calling convention and a known prologue has to guess. A JIT that assembles the replacement inline sidesteps that by generating the trampoline in place.
On the Objective-C side the status section lists what is supported: hooking Objective-C messages with the original implementation being kept, and hooking Objective-C class pairs, that is MSHookClassPair. The first is the standard case of replacing an implementation and still being able to call the original. The second is the harder one, where two classes are merged so that instances of one respond as the other, and it is the operation that breaks most hand-rolled hookers.
Building it, and the two package flavours
The build instructions are short. To build a dynamic library, use Xcode 14. To build the library and the package, run make deb. For the macOS library, set MAC=1. It can also be used as a Swift package, which the Package.swift at the root of the tree provides.
The Makefile explains what those variables do. MAC=1 switches the destination to generic/platform=macOS and puts the products in build/Release; without it the destination is generic/platform=iOS and the products land in build/Debug-iphoneos or the release equivalent. Setting RELEASE selects the Release configuration, and the version string is derived from git describe, with a fallback to always, so an untagged checkout still gets a usable version. The version is rewritten for the deb package format, and the debug build gets a +debug suffix so a local build cannot be mistaken for a release one.
The common build options disable code signing entirely, with CODE_SIGNING_ALLOWED and CODE_SIGNING_REQUIRED set to NO and an empty CODE_SIGN_IDENTITY, which is what an unsigned jailbreak component needs.
There are two package targets, deb-ios-rootful and deb-ios-rootless, and the difference is where the files land. The rootful target builds for iphoneos-arm with an empty install prefix, and the rootless target builds for iphoneos-arm64 with the prefix /var/jb, which is the rootless jailbreak layout. The clean and build targets run xcodebuild for several schemes: ellekit, injector, launchd, loader and safemode-ui, so the package contains the library and the pieces that get it into a running system.
Loading it at startup and who depends on it
A hooker that is not loaded is useless, and the project documentation points at a launchd-plist/LAUNCHDAEMON.md for a guide on installing a LaunchDaemon that loads ElleKit on startup. The tree backs this up with a launchd/ directory, a launcher/ directory, a loader/ directory and an injector/ directory, which is the set of components you would expect from a project whose job includes getting code into a process that did not ask for it.
The used-by list is the strongest evidence of what this is for. Dopamine, described as a jailbreak for A12 to A15 and M1 on iOS 15.0 to 16.6.1. palera1n, a jailbreak for A8 to A11 on iOS 15.0 and later. And meowbrek2, a jailbreak for A8 to A11 on iOS 15.0 to 15.8.1.
All three are jailbreaks, and all three use ElleKit as their hooking layer, which is a meaningful architectural choice for the project: the hooker sits underneath the jailbreak's own instrumentation, and the jailbreak is what makes the hooker possible in the first place.
The tested configurations section is where the practical limits are, and it contains one line that matters more than the others. macOS Ventura is listed for arm64 and arm64e, with a note that ellekit is now unsupported on the latest macOS due to codesigning changes on Apple's side. iOS 15 to 27 on arm64 is tested, and iOS 15 to 17 on arm64e.
That codesigning note is the single most important practical fact in the project documentation. The library's macOS support was a differentiator, and it has been withdrawn because Apple changed how code is signed, not because anything in the hooking mechanism broke. On iOS the equivalent pressure comes from the jailbreak itself rather than from codesigning.
Where ElleKit is the wrong tool
The first limit is the platform, and it is absolute. An arm64 device running the XNU kernel is the requirement, which means iOS, iPadOS and macOS on Apple silicon, and in practice a jailbroken or otherwise instrumented system, since the injector and loader components exist to place the library into a process that has to be modified to accept it. There is no Linux, no Windows and no Intel Mac path.
The second limit is the macOS withdrawal. The documentation says it plainly: ellekit is now unsupported on the latest macOS due to codesigning changes on Apple's side. A team that wanted a Darwin hooker for macOS instrumentation now has to read that line before planning around it, and the supported window in the tested configurations is Ventura.
The third limit is the API surface. The project documentation gives a list of what can be hooked, and it is a list of C functions, Objective-C messages and class pairs, plus a specific set of assemblable instructions. There is no Swift method hooking, no C++ support, and no promise about a function whose prologue the library cannot decode. Can hook most C functions on arm64 and arm64e is the phrasing, and most is doing some work in that sentence.
The fourth is that this is a low-level component with no stability promise. There are no GitHub releases, the versioning comes from git describe at build time, and the project documentation is a status list rather than a documentation set. The Usage.md and Installation.md files in the tree are where the operational detail lives, and the API reference for a project like this is the source itself, which is normal for hooking libraries and a real cost when you are depending on one.
The fifth is the nature of the tool. ElleKit is a hooking engine: it redirects control flow and rewrites memory pages. That is exactly what makes it useful for instrumentation, hooking and studying software behaviour, and it is also what makes it the wrong foundation for anything that needs to be trusted, since a library whose job is to replace functions at runtime is not something you build a product on without understanding it line by line.
Editorial conclusion
ElleKit is worth studying if you write hooks on Darwin and want the mechanism rather than a wrapper, because it shows the whole trick in the open, from the branch patch inside 128MB to the exception port beyond it, and because it implements the Substrate and libhooker APIs so existing tweak code ports over. It is the wrong tool for anything that is not a Darwin arm64 system, and the wrong tool for a modern stock macOS, where the documentation says it is no longer supported because of Apple's codesigning changes. Before you build on it, check the three things the project states plainly: the requirement of an arm64 device running the XNU kernel, the tested configurations of iOS 15 to 27 on arm64 and iOS 15 to 17 on arm64e with macOS Ventura noted as unsupported on the latest macOS, and the two ways the patch works, since a hook within 128MB of address space becomes a simple branch while one beyond it needs a brk #1 instruction and an exception port to catch it.
Frequently asked questions
What are the requirements for building ElleKit?
The project states one hard requirement, an arm64 device running the XNU kernel. To build a dynamic library you use Xcode 14, to build the library and the package you run make deb, and for the macOS library you set MAC=1. It can also be consumed as a Swift package.
How does ElleKit hook a function that is more than 128MB away?
Within 128MB of address space it patches the function with a simple branch instruction. Beyond that range it sets up an exception port to catch breakpoint exceptions, patches the target with a brk #1 instruction, and when the handler fires it changes the thread state to redirect execution to the target and then resumes it.
What does the original function look like after ElleKit hooks it?
ElleKit writes the original implementation to a new memory page and provides a pointer to it, and with LHHookFunctions it uses one page for all the orig functions. The relocated code materialises the target address in x16 with movk, adjusts it past the patched instruction, executes the four unpatched bytes of the target inline, and branches to the real function.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tealbathingsuit-ellekit)Was this useful?