Adding a Feature
Conventions for adding a feature to an existing ipa-forge hook dylib, key, default, implementation, settings row, hooks declaration.
Every feature in a hook dylib follows the same five steps, in order: a key, a default, the implementation, a settings row, and a hooks declaration.
Give it a key
Every user-facing feature is an NSUserDefaults key. Use your patch set's
existing prefix and enabled-check macro:
#define kMyModNewFeature @"MyModNewFeature"Declare it in the shared header (dylib/<App>Hook.h).
Set a default
Register the default in the constructor (dylib/<App>Hook.m), inside the
registerDefaults: dictionary. Default new features ON — unless the
feature changes behavior in a way users might not expect, in which case
default it off.
Features built from reverse engineering default OFF
If you had no reference source to check your work against and haven't confirmed it on-device yet, default the feature OFF and label it beta in the settings row. Flip it to default-ON once a device pass confirms it works.
Implement it
Add the feature to the file that already owns its area (AdBlock.m,
SessionProtection.m, Settings.m, …), or create a new one. Gate it on the
key at the top of its init function:
void MyModNewFeatureInit(void) {
if (!myModEnabled(kMyModNewFeature)) { os_log(myLog(), "...: disabled"); return; }
// hooks...
}Share a method's hook across features
If your feature needs a method another feature already hooks, add your
logic to that file and call a shared helper instead of hooking it again —
a second hookInstance call on the same method silently replaces the
first at runtime, so only one owner per method.
No reference source for the API you need, or the object graph is too deep to hardcode an accessor chain? Walk the object graph instead of guessing property names: start from a known, reachable anchor (a touched view, the current view controller) and do a bounded breadth-first search for anything whose shape matches what you need — responds to the right selectors, regardless of class or property name. This keeps working across internal renames that would break a hardcoded chain.
Before you write a hook target, confirm it exists:
forge hooks extract --ipa <ipa> --class <Class>
forge hooks extract --ipa <ipa> --search <regex>For a selector that seems missing, cross-check with forge analysis strings — the class-table parser can under-report methods on some
code-generated classes (protobuf message types, for example), so a real
selector can still show up as a plain string even when the parser missed it
as a method.
Stick to pure ObjC-runtime swizzling for the hook itself.
Avoid C-level function rebinding
Low-level rebinding (e.g. fishhook-style symbol patching) is a real crash source — it has caused crashes in this project before. Use it only if there's truly no ObjC-level way to intercept the call, defer it until after launch, and guard every resolved original function pointer.
Wrap the init in the dylib's existing @try-isolation helper so a failure
degrades instead of taking the app down.
Add a settings row
Add the toggle to the in-app settings screen: title, one-line detail, and the key. Note that toggles are read at hook-install time, so the screen should say "changes apply on relaunch."
Declare the hook
Add the new hook target(s) to the definition's hooks: block (<app>.yaml).
Mark required: true when the feature would silently die if the
class/selector gets renamed in a future app version.
forge hooks manifest --dir dylib/ --required hooks-required.txt # direct calls
# helper/loop-based hooks: add these to the yaml by handConfirm nothing was missed
Even a hook that looks "obviously fine" should go through:
forge hooks audit --ipa <ipa> --dir dylib/ --patches <app>.yamlThis fails (exit 1) if any hook the source calls isn't in the hooks:
block — a gap that --dry-run alone can't see, by construction.
Verify and commit
dylib/build.sh
forge hooks audit --ipa <base>.ipa --dir dylib/ --patches <app>.yaml
# -> all source hooks declared, 0 undeclared
forge patch --ipa <base>.ipa --patches <app>.yaml --dry-run
# -> the new hook attaches (or shows an honest unverified for a system API)
forge patch --ipa <base>.ipa --patches <app>.yaml --no-sign --output <out>.ipaThen test on a real device — the dylib's os_log subsystem shows the
feature's ready/disabled line. No device available yet? Default the
feature OFF and mark it beta in the settings row, and treat it as unproven
until it's actually been confirmed. Update the patch set's README.md
feature list, and commit — including the patch submodule itself, if the
patch set lives in a separate git repo.
Debugging a feature that doesn't work
Does the hook attach? The install log prints every hooked -[Class method] line — check the feature's target is in there.
Does the code path fire? Add an os_log inside the hook block and
reproduce.
Is it a server-controlled surface? Config-flag hooks chain to the server's value when off — log the effective value to rule this out.
Did the hook point drift? forge hooks diff --old <old>.ipa --new <new>.ipa --patches <yaml> shows what regressed between versions.