Adding a New App
End-to-end playbook for porting a new app to ipa-forge, patch set layout, hook dylib conventions, and the device-test loop.
This is the "give me an IPA, build a working patch set" procedure. It's
generic — a per-app runbook (PLAYBOOK.md) is what you write at the end to
capture the specific commands for one app.
Inspect the IPA
Confirm your input is a decrypted .ipa — one already exported for
sideloading, not a raw App Store download (those need a jailbroken device
or a store purchase to decrypt first; forge can dry-run an encrypted IPA
without erroring, but the binary won't run once installed).
forge inspect <ipa>.ipaThis prints the bundle id, version, main executable, and every embedded
extension/framework — record these, you'll need them for the definition's
target block.
forge hooks extract --ipa <ipa>.ipa --class <MainClass>Confirms the binary parses correctly before you invest time writing hooks
against it. Already have an extracted bundle from a previous run? Pass
--app-dir Payload/<App>.app instead of --ipa to skip re-extraction —
every forge hooks command accepts it.
Create the patch set directory
Name the directory after the app, not the version:
| File | Purpose |
|---|---|
<app>.yaml | The definition: patch operations + the hooks: block. One canonical file per app. |
dylib/ | The hook dylib sources and build.sh. |
PLAYBOOK.md | This app's runbook — copy the template in the last step below. |
README.md | Features, build, apply, and verify instructions. |
SOURCES.md | Attribution and research lineage. |
The version lives inside the YAML's target.version, not in the directory
name. When the app updates, edit target.version in place in the same
patches/<app>/ directory and re-verify — one directory always holds the
current definition for that app.
The definition's shape:
target: { bundle_id: "...", version: { exact: "..." } }
patches:
- resource_remove: # strip extension/watch-app ids AltStore would otherwise
# append the Team ID to at install (IXErrorDomain Code=2)
- resource_add: # stage the hook dylib into Frameworks/
- dylib_inject: # LC_LOAD_WEAK_DYLIB, so a load failure degrades the
# app instead of crashing it at launch
hooks: [ ... ] # every hook the dylib relies on (see below)Write the hook dylib
Write hooks in plain Objective-C, using runtime swizzling — no Swift, no
substrate-style tweak frameworks. Foundation/UIKit/Security cover almost
everything; reach for AVFoundation or Photos only if a specific feature
genuinely needs them. Copy the plumbing pattern from an existing dylib
(<prefix>Hook.h's hookInstance/hookClass inline helpers, os_log for
logging).
Three conventions keep this reliable:
Install hooks after launch, not at load time
Have the dylib's constructor do nothing but schedule the real install on the
main run loop (dispatch_async(dispatch_get_main_queue())). By the time that
runs, every image is loaded — this sidesteps the whole class of load-time
crashes that come from hooking too early.
Isolate every feature's init
Wrap each feature's setup in a @try/@catch helper (e.g. safeInit(name, block)). One feature failing logs and degrades gracefully instead of
crashing the app.
One hook per method, one file per feature
If two features need the same method, put the logic in whichever file
already owns that hook and call a shared helper — a second hookInstance
call on the same method silently replaces the first one at runtime.
build.sh should stage the compiled dylib at build/<Name>Hook.dylib,
where the definition expects it.
Declare and verify hooks
List every hook target in the definition's hooks: block:
hooks:
- class: "SomeClass"
selector: "someMethod:"
required: true # fail the run if this one can't attach
- class: "SomeOther"
selector: "addedThing"
added: true # the tweak adds this method itselfGenerate this block from your sources instead of writing it by hand:
forge hooks manifest --dir dylib/ --required hooks-required.txtThis recognizes direct NSClassFromString calls and resolver-helper
patterns (any function whose body calls NSClassFromString, invoked as
myClass("X") at the hook call site). Hooks where the class name flows
through a second function before reaching the hook call aren't traced —
add those to the YAML by hand.
Then verify:
forge patch --patches <app>.yaml --ipa <ipa> --dry-runThis checks every declared hook against the main binary and every embedded
framework, and fails the run if a required hook can't attach. Read
Patch Reference
for what each status (ok, unverified, missing-class, ...) means.
Run forge hooks audit before every commit
forge patch --dry-run only checks hooks that are declared in the YAML —
a hook your source calls but the YAML never mentions is invisible to it.
forge hooks audit --ipa <ipa> --dir dylib/ --patches <app>.yamlcross-checks your source against the declared block and catches that gap.
Porting to a new app version later, forge hooks diff --old prev.ipa --new next.ipa --patches <app>.yaml shows exactly which hooks regressed.
Build and test on device
patches/<app>/dylib/build.sh
forge patch --ipa <base>.ipa --patches patches/<app>/<app>.yaml \
--output /tmp/x.ipa --dry-run # confirm the hooks gate passes first
forge patch --ipa <base>.ipa --patches patches/<app>/<app>.yaml \
--no-sign --output <delivery-dir>/<App>Mod_<version>_unsigned.ipaSideload the output via AltStore and check the device logs — give the dylib
a unique os_log subsystem (<prefix>Log), and log every hook attach at
install plus each feature's ready/disabled state. That turns "does it
work" into something you can read off the console instead of guessing.
Crashes at launch
Build a do-nothing test dylib (constructor logs one line via os_log, weak
load command, same resource-strip operations) and inject that instead. If
it launches fine, the crash is in your hook code — bisect features,
starting with anything that does C-level function rebinding (riskiest by
far). If the test dylib also crashes, the problem is in injection, signing,
or the strip operations, not your hooks.
Write the runbook
Copy an existing PLAYBOOK.md as your template and fill in this app's
commands, hook-verification results, and log subsystem. Write README.md
(features, build, apply, a table of what's been verified) and SOURCES.md
crediting your own research.