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

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

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

The 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.json

Emits 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 --clear

forge 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.json

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

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

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

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

A 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 pass

A 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 gui

Where /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.

On this page