Skip to content

feat(tweaks): configurable injection path and optional tweak loader - #223

Open
Mr-Tomahawk wants to merge 1 commit into
claration:mainfrom
Mr-Tomahawk:feat/tweak-injection-options
Open

feat(tweaks): configurable injection path and optional tweak loader#223
Mr-Tomahawk wants to merge 1 commit into
claration:mainfrom
Mr-Tomahawk:feat/tweak-injection-options

Conversation

@Mr-Tomahawk

Copy link
Copy Markdown

Adds control over two things that were previously hardcoded when applying tweaks: which loader gets bundled, and where user tweak dylibs/frameworks land and are loaded from.

Tweak loader

New --tweak-loader <ellekit|none> on plumesign sign (default ellekit, i.e. current behavior).

  • ellekit — unchanged: the bundled ElleKit/CydiaSubstrate compatibility deb is installed and CydiaSubstrate references in tweaks are patched to point at it.
  • none — nothing is bundled. User tweak dylibs/frameworks are still copied in and injected, but no loader deb is installed and no CydiaSubstrate patching happens. This is what you want for tweaks that link nothing (or ship their own hooking), and for tweaks meant to run against a substitute/substrate already present on the target.

SignerOptions::tweak_loader carries this, so it's usable from the library too.

Injection path

Two new flags, both opt-in — if neither is passed, behavior is byte-for-byte the old one (copy into Frameworks/, inject @rpath/<leaf>):

  • --tweak-inject-path <@rpath|@executable_path> (default @rpath)
  • --tweak-inject-folder </|Frameworks/> (default /)

Passing either switches to explicit mode, where the destination directory and the LC_LOAD_DYLIB string are derived from the pair — e.g. @executable_path + / copies the dylib next to the executable and injects @executable_path/MyTweak.dylib; @rpath + Frameworks/ injects @rpath/Frameworks/MyTweak.dylib. Frameworks resolve their executable from the inner Info.plist and get the same treatment.

When @rpath is selected in explicit mode, an LC_RPATH of @executable_path is added to the app executable first so the load path actually resolves.

Mach-O support

Needed for the above:

  • MachO::rpaths() / MachO::add_rpath(), with LC_RPATH insertion done in place in the load-command padding (errors out rather than corrupting the binary if there isn't room).
  • plumesign macho --list-rpaths and --add-rpath <RPATH> to drive those from the CLI.
  • The manual lc_str parser is generalised to take the offset-field position instead of assuming 8, since LC_RPATH and LC_LOAD_DYLIB both need it.
  • Dylib injection no longer double-writes: add_dylib already flushes, so the redundant write_changes() call after it is gone.

Compatibility

Defaults are unchanged everywhere. Tweak::new() keeps its signature and maps to ellekit + legacy injection; new_with_loader() / new_with_options() are the new entry points.

Add --tweak-loader to choose between bundling ElleKit/CydiaSubstrate and
bundling nothing, plus --tweak-inject-path/--tweak-inject-folder to control
where user tweak dylibs and frameworks are copied and what load path is
written into the app executable.

Supporting Mach-O work: LC_RPATH listing and insertion (rpaths()/add_rpath())
exposed through the machO subcommand as --list-rpaths/--add-rpath, and
LC_* string parsing generalised to any load command offset field.
@claration

Copy link
Copy Markdown
Owner

Something I need to know is that what's actually the point of the injection path options for tweaks? To me, yes they're in different directories but achieve the same functionality. Is it for tweak detection or what?

@Mr-Tomahawk

Copy link
Copy Markdown
Author

tweak detection is one aspect yes. but sometimes due to app structures a tweak will successfully inject in one path but not the other. to be honest its not too critical, but I figure'd might as well include the option. the idea was to bring it to parity with the "Esign" side loading app, which I have used extensively. The paths are more of an edge case, but one thats nice to have at least for me, though I understand why it may not be the case for others

@claration

claration commented Jul 31, 2026

Copy link
Copy Markdown
Owner

sometimes due to app structures a tweak will successfully inject in one path but not the other

is there an example of this?

@Mr-Tomahawk

Copy link
Copy Markdown
Author

sometimes due to app structures a tweak will successfully inject in one path but not the other

is there an example of this?

not off the top of my head but I've had cases where a tweak will fail to inject unless it was in frameworks folder, or root folder. in those rare cases being able to choose is beneficial, though it may have been a tweak issue or a side load tool issue (see claration/Feather#256 as an example, it may have been something like that). However it's also nice for apps with poor tweak detection, who check for tweaks solely by scanning one dir but might overlook another, this works nicely. However I can see why it might not be merge worthy, my main concern is the ability to choose to not inject ellekit or anything at all into a tweak since not all tweaks depend on it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants