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)

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

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

forge 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 0

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.

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.

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

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

On this page