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>.ipa

This 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:

<app>.yaml
PLAYBOOK.md
README.md
SOURCES.md
FilePurpose
<app>.yamlThe definition: patch operations + the hooks: block. One canonical file per app.
dylib/The hook dylib sources and build.sh.
PLAYBOOK.mdThis app's runbook — copy the template in the last step below.
README.mdFeatures, build, apply, and verify instructions.
SOURCES.mdAttribution 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 itself

Generate this block from your sources instead of writing it by hand:

forge hooks manifest --dir dylib/ --required hooks-required.txt

This 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-run

This 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>.yaml

cross-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.ipa

Sideload 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.

On this page