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)
Check the definition first, without an IPA:
forge lint patches.yamlThen check it against the actual bundle:
forge patch --ipa path/to/App.ipa --patches patches.yaml \
--output patched.ipa --dry-run --manifest dry-run.json--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 lint <definition.yaml>
Checks YAML/JSON schema, unique operation IDs, binary pattern/replacement
syntax and equal lengths, source existence relative to the definition's
parent directory, and duplicate class/selector hooks. resource_replace
requires a source file; resource_add can stage a directory. No IPA or
signing credentials are needed. Exit 0 means the definition passes these
checks; exit 1 prints one line on stderr. Matching the target, locating
executables, counting byte matches, and hook attachment still need a dry-run.
forge cache [--clear]
Shows the content-addressed Mach-O analysis cache's location, entry count,
and size. --clear deletes its entries. Unchanged binaries reuse their
analysis even after extraction to a different directory; changed binaries
have different SHA-256 keys. The cache retains at most 16 entries.
Set FORGE_NO_CACHE=1 to bypass it for a command. FORGE_CACHE_DIR overrides
the cache root; otherwise it uses $XDG_CACHE_HOME/ipa-forge or
~/.cache/ipa-forge.
forge cache
FORGE_NO_CACHE=1 forge hooks find someMethod: --ipa App.ipa
forge cache --clearforge verify-output --base <ipa> --output <ipa> [--manifest <json>]
With an applied manifest, requires the input SHA-256 to match and checks the
output SHA-256 when recorded,
compares Payload file inventories against files_added/files_removed, and
rejects changed files outside files_modified. For each recorded binary
operation it replays every edit against the base, checks the original bytes,
and requires the entire output binary to equal the replayed bytes. File
paths in new manifests are relative to the app root. A directory added or
removed by a resource operation covers its descendants.
forge patch --ipa App.ipa --patches patch.yaml --no-sign \
--output patched.ipa --manifest manifest.json
forge verify-output --base App.ipa --output patched.ipa --manifest manifest.jsonThis strict comparison is intended for unsigned byte-patch outputs. Signing
and load-command injection can change bytes beyond the recorded byte edits;
such differences fail verification. Resource/plist changes are allowed only
in declared files; their exact contents are covered by the recorded output
hash. A dry-run manifest is a plan, not an applied record. Without a manifest,
the command requires byte-identical Payload files and inventories. Exit 1
reports a verification failure; exit 0 reports success.
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. Five 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 skips extraction:
unzip -q App.ipa -d /tmp/app-analysis
forge hooks verify --app-dir /tmp/app-analysis/Payload/Example.app --patches patch.yamlforge hooks verify
Verify every hook declared in a patch definition's hooks: block against the
app binaries. Exit code 1 when a required hook has a blocking status;
unverified parser gaps are reported but never fail the command.
Prop
Type
forge hooks verify --ipa App.ipa --patches patch.yaml --json
forge hooks verify --ipa App.ipa --patches patch.yaml --types
# [ok ] SPTAuthSessionImplementation -[logoutWithReason:]
# - (void)logoutWithReason:(long long)arg1 v24@0:8q16--types shows the signature of the method each hook actually binds to (its
own class, or the ancestor it inherits from). Write the hook block with
exactly those argument and return types — see hooks audit below for the
automatic check.
JSON contains a hooks array with class name, selector, kind, status, detail,
required/added flags, encoding (the bound method's raw type encoding, empty
when it wasn't decoded) and type_issues. --required-only filters out attaching hooks in
either format; the exit status still considers all required hooks.
forge 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.
--json returns hooks and undeclared arrays and preserves that exit status.
It also checks every inline hook block (…Hook…(cls, @selector(x:), ^ret(id self, T a) {…}))
against the hooked method's type encoding and exits 1 on an ABI mismatch:
an object-typed parameter where the method passes an integer/BOOL (ARC
would retain a non-pointer), a void block on a value-returning method (the
caller reads garbage), integer width mismatches (NSInteger vs
unsigned char), or more parameters than the method passes. Style
differences (id vs NSString *) and omitted trailing parameters are fine
and never flagged; structs, C pointers and unsized typedefs aren't judged.
Blocks passed through variables or helper functions aren't seen — check
those with hooks verify --types.
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. Pass several selectors to analyze the bundle once.
--json emits an array of lookup records with selector, instance_classes,
class_classes, status, and similar.
Prop
Type
forge hooks find someFeatureAction: otherMethod: --ipa App.ipa --json
# Omit --json for the text report with method owners and REFERENCED-ONLY warnings.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 resolver helper — any function, in any scanned file, whose body calls
NSClassFromString, invoked as resolver("X") at the hook call site or
assigned as cls = resolver("X"). Each call site reports its own class. 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 classes (_TtC…)
are parsed (the walker masks the Swift flag bits in class_t.data); one
absent from the table is reported unverified — usually a rename — never
mislabeled as a system class.
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 analysis classdump --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. For common queries:
forge analysis classdump --ipa App.ipa --json
forge analysis classdump --ipa App.ipa --search '^App' --names-only
forge analysis classdump --ipa App.ipa --class AppConfig --methods-matching '^(try|can|should)'--json preserves the metadata as structured objects; --names-only prints
class names, and --methods-matching filters method selectors. These filters
also apply to JSON output.
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
(--manifest PATH, --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.