Usage
Full CLI and web GUI reference for forge: patch, inspect, validate, hooks, and analysis commands, plus signing identity and profile setup.
Complete walkthrough of patching, signing, and distributing an IPA with
ipa-forge — via the forge CLI and the local web GUI. Not installed yet?
See Installation first (pipx install ipa-forge is the
recommended path).
End-to-end workflow
Inspect the IPA
forge inspect path/to/App.ipaPrints the bundle id, version, main executable, and the executable inventory in
bottom-up signing order. This tells you the bundle_id and version your
patch definition must target, and the exact executable names to patch.
Write a patch definition
See Patch Reference — every operation type, field, and matching rule. At minimum:
target:
bundle_id: "com.example.app"
version: { exact: "1.2.3" }
patches:
- id: "swap-asset"
type: resource_replace
path: "asset.txt"
source: "assets/patched_asset.txt"Dry-run (safe, no mutation, no signing)
forge patch --ipa path/to/App.ipa --patches patches.yaml \
--output patched.ipa --dry-run--identity and --profile are not needed for a dry run. Every operation
must pass the dry-run gate before anything would be applied; the result line
tells you how many operations would run. If the definition's target doesn't
match the IPA, you get a warning instead of a silent no-op.
Patch and re-sign for real
forge patch --ipa path/to/App.ipa --patches patches.yaml \
--identity "Apple Development" \
--profile path/to/profile.mobileprovision \
--output patched.ipa --verbose--profile is repeatable — supply one per app extension/watch app, matched by
each target's own bundle id:
forge patch --ipa App.ipa --patches patches.yaml \
--identity "Apple Development" \
--profile main.mobileprovision \
--profile extension.mobileprovision \
--output patched.ipaThe output is a standard-structure Payload/<App>.app IPA that AltStore
Classic can install and refresh.
(Optional) Produce an AltStore source.json entry
forge export-source --ipa patched.ipa \
--download-url https://example.com/patched.ipa \
--output source.jsonEmits an AltStore Classic source entry (name, bundleIdentifier, version,
buildVersion, downloadURL, size, sha256) for hosting the patched IPA
yourself.
Install on a device
Follow the manual AltStore Classic checklist in Device Testing.
CLI reference
Run any command with --help for the full option list.
forge inspect <ipa>
Prints bundle metadata and the executable inventory (bottom-up signing order, with each target's kind and bundle-relative path).
forge validate <ipa>
Validates that the IPA is a well-formed archive with exactly one
Payload/*.app, that Info.plist is parseable, and that the main executable
is present — without patching or signing anything.
forge patch
Prop
Type
--identity/--profile are required unless --dry-run or --no-sign is
set — those two flags are the only ways to produce output without a
Keychain identity and provisioning profile on hand.
Exit codes: 0 success, 1 any error (with a single-line error: message on
stderr — no tracebacks).
forge export-source
Prop
Type
forge gui
Launches the local web GUI on 127.0.0.1:8765 by default (override with
--host/--port). The GUI wraps the same pipeline as forge patch.
forge --version
Prints the installed version.
Hook verification (forge hooks)
Dylib tweaks silently break when a new app version renames/removes hook
targets. Six commands turn that into a checkable report, all built on the
same Mach-O/ObjC analysis engine forge analysis uses.
Fast iteration with --app-dir
Every one of these re-extracts the full IPA (seconds to tens of seconds on a
large app) unless you point it at an already-extracted bundle instead — pass
--app-dir Payload/<App>.app (and then omit --ipa) and the
analyze/verify/fix loop becomes instant:
forge patch --ipa App.ipa --patches patch.yaml --output /tmp/x.ipa --dry-run
forge hooks verify --app-dir /tmp/x.app --patches patch.yaml # no re-extractforge hooks verify
Verify every hook declared in a patch definition's hooks: block against the
app binary. Exit code 0 if every required hook attaches, 1 otherwise.
Prop
Type
forge hooks verify --ipa App.ipa --patches patch.yamlforge hooks extract
Dump the Objective-C class table: class names, superclasses, and
instance/class method lists (chained-fixup aware, full method lists — pipe
to grep for large config classes).
Prop
Type
forge hooks extract --ipa App.ipa --class PlaybackResponse
forge hooks extract --ipa App.ipa --search "Config" --limit 0forge hooks audit
Scan ObjC tweak sources for hook calls and verify each target against the
app binary — catches hooks the author wrote but a newer app version broke.
With --patches, also cross-checks the source scan against the definition's
hooks: block and flags any hook the source calls that isn't declared there
(invisible to --dry-run otherwise). Exits 1 if any are undeclared.
Prop
Type
forge hooks audit --ipa App.ipa --dir dylib/
forge hooks audit --ipa App.ipa --dir dylib/ --patches patch.yamlforge hooks find
Reverse lookup: which classes implement a selector? The first check when a
hook "attaches per verify" but does nothing on device — distinguishes a real
method (swizzle-able) from a bare reference (REFERENCED-ONLY: no IMP
exists, the hook cannot attach). Also lists selectors containing the lookup
text, to catch renames.
Prop
Type
forge hooks find someFeatureAction: --ipa App.ipa
# -> -[MainOverlayViewController someFeatureAction:] (real)
# -> REFERENCED-ONLY ... no class declares it (the hook cannot attach)forge hooks manifest
Emit a hooks: YAML block from the hook calls in tweak sources, to paste (or
write directly) into a patch definition.
Prop
Type
forge hooks manifest --dir dylib/ --required hooks-required.txt
forge hooks manifest --dir dylib/ --inplace patch.yamlThe scanner recognizes any <prefix>HookInstance/HookClass/ConfigBool/AddInstanceMethod
call with an inline NSClassFromString(@"X") (or a file-scoped
cls = NSClassFromString(@"X") assignment), plus class names passed through
a local resolver helper — any function whose body calls NSClassFromString
and that is invoked as resolver("X") at the hook call site. Helper/loop-
based hooks whose class is passed into another function before reaching the
hook call are not traced and must be declared manually in the definition's
hooks: block instead.
forge hooks diff
Compare hook attachment between two IPAs — the porting aid. Shows every hook
whose status changed, highlighting ones that regressed (would no-op on the
new version). Exit 1 if a required hook regressed.
Prop
Type
forge hooks diff --old prev.ipa --new next.ipa --patches patch.yamlA class or selector that the class-table walk missed (a parser gap, not a
rename) is now reported honestly: when the name exists as a string somewhere
in the binary, missing-class / missing-selector become unverified with
a "string present … the walk missed it" hint — the old manual
strings <binary> | grep cross-check is automated. Swift-mangled (_TtC…)
classes absent from the parsed table are reported unverified, never
mislabeled as system classes.
forge patch --dry-run also verifies the definition's hooks: block
automatically and prints an attach summary; required: true hooks that
can't attach fail the run before anything mutates. See
Patch Reference → "The hooks block".
Porting a dylib patch set to a new app version
If your patch set injects a hook dylib, declare its hook targets in the
definition's hooks: block (see Patch Reference → "The hooks
block"). Then porting is:
# 1. bump target.version in the definition
# 2. dry-run: reports every hook that would silently no-op on the new binary
forge patch --ipa New.ipa --patches patch.yaml --output /tmp/x.ipa --dry-run
# 3. for each flagged hook, find what replaced the class/selector
forge hooks extract --ipa New.ipa --search "<old class substring>"
# 4. fix the tweak source, rebuild, re-run the dry-run until required hooks passA concrete patch set's PLAYBOOK.md (see Adding a New App)
is a worked example of this loop end-to-end for a specific app.
Reverse engineering any IPA (forge analysis)
forge hooks above answers one question — does this declared hook
attach? For general-purpose static analysis of an IPA (class-dump, string
extraction, symbols, security posture, and a broader version-to-version
diff that isn't tied to a patch definition's hooks: block), see
Reverse Engineering — the single source for
that command surface, to avoid duplicating it here.
Signing identity & profile
ipa-forge re-signs with your own Apple development credentials — it never manages your Apple account.
Identity
security find-identity -v -p codesigning lists the codesigning identities
in your Keychain. Pass either the full SHA-1 hash or a unique substring of
the name (e.g. "Apple Development"). Ambiguous or unmatched substrings
fail loudly, listing candidates.
Profile
Profiles installed by Xcode live under
~/Library/MobileDevice/Provisioning Profiles/*.mobileprovision. Inspect
one with security cms -D -i <profile>.
Authorization
The profile's Entitlements.application-identifier must authorize the
bundle id being signed — exactly, or via a wildcard TEAMID.* profile.
forge patch validates profiles up front (expiry, bundle-id match) and fails
with an actionable error before touching your IPA if they don't qualify.
Profile selection rules (see signing/profile.py::ProfilePool):
- An exact bundle-id match wins; otherwise a wildcard (
*) profile; otherwise a single supplied profile is used for everything (the legacy behavior); otherwise an error listing the available patterns. - Two profiles authorizing the same bundle id (or two wildcards) is an ambiguity error, not a silent first-win.
- In single-profile mode, if the lone profile doesn't authorize an app extension's bundle id you get a warning (it is still embedded, matching legacy behavior) — supply a matching profile for that extension to silence it.
The web GUI
forge gui → open http://127.0.0.1:8765.
The GUI is deliberately minimal — it does not accept an arbitrary patch definition. It drops an IPA, auto-detects which locally-discovered patch set applies, and produces an unsigned output for AltStore. There is no identity/profile input in the UI at all.
Drop the IPA
The GUI reads the bundle id and version, then looks up a matching patch set
by scanning patches/<app>/<app>.yaml under the patch-set root (see
below). If none matches the bundle id, patching is disabled with an error.
If one matches the bundle id but targets a different version, you get a
non-blocking warning — hook verification is the safety net either way.
Patch
One Patch button. The request always sets no_sign — the GUI never
signs; it produces the same unsigned, standard-structure output as
forge patch --no-sign, ready for AltStore to sign at install.
Download
The result shows how many operations applied and a Download link for
the patched .ipa.
The GUI needs a patch-set root to find anything
Patch-set discovery scans patches/<app>/<app>.yaml relative to the
installed package by default — for a pip/pipx install that directory
almost certainly doesn't exist, so the GUI will report "no patch set found"
for every IPA until you point it at one:
export IPA_FORGE_PATCHES_DIR=/path/to/your/patches
forge guiWhere /path/to/your/patches contains one <app>/<app>.yaml per app (see
Adding a New App). The CLI has no such
restriction — forge patch --patches any/path/to/definition.yaml works
regardless of location. Use the CLI for a one-off patch; set up a patch-set
root only if you want the drop-and-go GUI flow for apps you patch repeatedly.
The GUI is a single-user localhost tool: requests are processed in-process and outputs are kept for download (bounded, oldest evicted). It is not designed as a hosted multi-user service.
A second page, /analysis (linked from this page's subtitle), gives the same
read-only reverse-engineering views as forge analysis — no patch set or
signing involved, works on any IPA. See
Reverse Engineering.
What you get back
forge patch writes a standard Payload/<App>.app zip. The manifest
(--verbose or the GUI result card) lists every applied operation, every
file added/modified/removed, the Mach-O files touched, the profile used, and
the output SHA-256 — use it to confirm the patch did what you expected before
installing.