#!/usr/bin/env bash # neutralize.sh — Neutralize tracker/ad SDK entry points in decoded APK smali # # Replaces SDK method bodies with stubs (return-void, return 0, return-object null) # and optionally disables manifest components. # # Exit codes: # 0 — success (methods patched) # 1 — error (invalid input, missing files) # 2 — no targets found set -euo pipefail usage() { cat < [OPTIONS] Neutralize tracker and ad SDK entry points in a decoded APK directory. The decoded directory must be the output of 'apktool d' and contain a smali/ directory and AndroidManifest.xml. Arguments: Path to the apktool-decoded APK directory Options: --ads Neutralize only ad SDK entry points --trackers Neutralize only tracker/analytics SDK entry points --all Neutralize both ads and trackers (default) --dry-run Show what would be patched without modifying files --backup Create .smali.bak backups before patching (default) --no-backup Do not create backup files --manifest Patch AndroidManifest.xml to disable SDK components (default) --no-manifest Skip manifest patching --targets-file Load additional targets from a file (one per line: :) --replay Replay patches from a previous neutralize-manifest.json --save-manifest Save neutralize-manifest.json after patching (default) --no-save-manifest Do not save neutralize-manifest.json -h, --help Show this help message Output: Machine-readable lines: PATCHED::::: MANIFEST_DISABLED::: DRY_RUN:WOULD_PATCH::::: DRY_RUN:WOULD_DISABLE::: EOF exit 0 } # ===================================================================== # Argument parsing # ===================================================================== DECODED_DIR="" NEUTRALIZE_ADS=false NEUTRALIZE_TRACKERS=false NEUTRALIZE_ALL=true DRY_RUN=false DO_BACKUP=true DO_MANIFEST=true TARGETS_FILE="" DO_REPLAY=false DO_SAVE_MANIFEST=true while [[ $# -gt 0 ]]; do case "$1" in --ads) NEUTRALIZE_ADS=true; NEUTRALIZE_ALL=false; shift ;; --trackers) NEUTRALIZE_TRACKERS=true; NEUTRALIZE_ALL=false; shift ;; --all) NEUTRALIZE_ALL=true; shift ;; --dry-run) DRY_RUN=true; shift ;; --backup) DO_BACKUP=true; shift ;; --no-backup) DO_BACKUP=false; shift ;; --manifest) DO_MANIFEST=true; shift ;; --no-manifest) DO_MANIFEST=false; shift ;; --targets-file) shift if [[ $# -eq 0 ]]; then echo "Error: --targets-file requires a file argument" >&2 exit 1 fi TARGETS_FILE="$1"; shift ;; --replay) DO_REPLAY=true; shift ;; --save-manifest) DO_SAVE_MANIFEST=true; shift ;; --no-save-manifest) DO_SAVE_MANIFEST=false; shift ;; -h|--help) usage ;; -*) echo "Error: Unknown option $1" >&2; usage ;; *) DECODED_DIR="$1"; shift ;; esac done if [[ -z "$DECODED_DIR" ]]; then echo "Error: No decoded directory specified." >&2 usage fi if [[ ! -d "$DECODED_DIR" ]]; then echo "Error: Directory not found: $DECODED_DIR" >&2 exit 1 fi # Find smali directories (multidex support: smali/, smali_classes2/, etc.) SMALI_DIRS=() for d in "$DECODED_DIR"/smali*; do if [[ -d "$d" ]]; then SMALI_DIRS+=("$d") fi done if [[ ${#SMALI_DIRS[@]} -eq 0 ]]; then echo "Error: No smali/ directory found in $DECODED_DIR" >&2 echo "Make sure this is an apktool-decoded APK directory." >&2 exit 1 fi MANIFEST="$DECODED_DIR/AndroidManifest.xml" if [[ ! -f "$MANIFEST" ]]; then echo "Warning: AndroidManifest.xml not found in $DECODED_DIR" >&2 DO_MANIFEST=false fi # ===================================================================== # Counters & patch log # ===================================================================== METHODS_PATCHED=0 METHODS_SKIPPED=0 COMPONENTS_DISABLED=0 # Patch log file — captures all PATCHED: and MANIFEST_DISABLED: lines PATCH_LOG_FILE=$(mktemp "${TMPDIR:-/tmp}/neutralize-log-XXXXXX") cleanup_patch_log() { rm -f "$PATCH_LOG_FILE" } trap cleanup_patch_log EXIT # ===================================================================== # patch_method() — Replace a smali method body with a stub # # Arguments: # $1 — smali file path # $2 — method name (e.g., "initialize", "logEvent") # $3 — SDK name (for reporting, e.g., "AdMob", "Firebase") # $4 — smali class descriptor (e.g., "Lcom/google/android/gms/ads/MobileAds;") # # The function finds .method declarations matching the method name, # determines the return type, and replaces the body with appropriate stubs. # ===================================================================== patch_method() { local file="$1" local method_name="$2" local sdk_name="$3" local class_desc="$4" if [[ ! -f "$file" ]]; then return fi # Use awk to find and patch method bodies local tmp_file tmp_file=$(mktemp) local patched=false awk -v method="$method_name" -v sdk="$sdk_name" -v cls="$class_desc" \ -v dry_run="$DRY_RUN" -v src_file="$file" ' BEGIN { in_target = 0 found = 0 } # Match .method line containing our target method name /^\.method / && $0 ~ "[ ;]" method "\\(" { in_target = 1 found = 1 # Extract return type from method descriptor # The descriptor is the last part: (params)ReturnType line = $0 # Find the closing paren and get what follows idx = index(line, ")") if (idx > 0) { ret_type = substr(line, idx + 1) # Remove trailing whitespace gsub(/[[:space:]]+$/, "", ret_type) } else { ret_type = "V" } # Determine stub type if (ret_type == "V") { stub_type = "return-void" stub_body = " .registers 1\n\n return-void" } else if (ret_type == "Z" || ret_type == "I" || ret_type == "S" || \ ret_type == "B" || ret_type == "C" || ret_type == "F") { stub_type = "const/4+return" stub_body = " .registers 1\n\n const/4 v0, 0x0\n\n return v0" } else if (ret_type == "J" || ret_type == "D") { stub_type = "const-wide+return-wide" stub_body = " .registers 2\n\n const-wide/16 v0, 0x0\n\n return-wide v0" } else { # Object or array return type (L...; or [...) stub_type = "const/4+return-object" stub_body = " .registers 1\n\n const/4 v0, 0x0\n\n return-object v0" } if (dry_run == "true") { printf "DRY_RUN:WOULD_PATCH:%s:%s:%s:%s:%s\n", sdk, cls, method, stub_type, src_file > "/dev/stderr" } else { printf "PATCHED:%s:%s:%s:%s:%s\n", sdk, cls, method, stub_type, src_file > "/dev/stderr" } # Print the .method line unchanged print $0 next } # Inside a target method — skip original body until .end method in_target && /^\.end method/ { # Print the stub body, then .end method printf "%s\n", stub_body print "" print $0 in_target = 0 next } # Inside target method — skip all lines (original body) in_target { next } # Outside target method — print line unchanged { print } ' "$file" > "$tmp_file" 2> >(while IFS= read -r line; do echo "$line" if [[ "$line" == PATCHED:* ]]; then METHODS_PATCHED=$((METHODS_PATCHED + 1)) patched=true echo "$line" >> "$PATCH_LOG_FILE" elif [[ "$line" == DRY_RUN:WOULD_PATCH:* ]]; then METHODS_PATCHED=$((METHODS_PATCHED + 1)) patched=true fi done) if [[ "$DRY_RUN" == false ]] && [[ "$patched" == true ]]; then if [[ "$DO_BACKUP" == true ]]; then cp "$file" "${file}.bak" fi mv "$tmp_file" "$file" else rm -f "$tmp_file" fi } # ===================================================================== # find_and_patch() — Find smali files for a class and patch methods # # Arguments: # $1 — smali class path (e.g., "com/google/android/gms/ads/MobileAds") # $2 — comma-separated method names # $3 — SDK name # $4 — smali class descriptor # ===================================================================== find_and_patch() { local class_path="$1" local methods_csv="$2" local sdk_name="$3" local class_desc="$4" IFS=',' read -ra methods <<< "$methods_csv" for smali_dir in "${SMALI_DIRS[@]}"; do local smali_file="$smali_dir/$class_path.smali" if [[ -f "$smali_file" ]]; then for method in "${methods[@]}"; do patch_method "$smali_file" "$method" "$sdk_name" "$class_desc" done fi done } # ===================================================================== # SDK Target Lists # ===================================================================== patch_ad_targets() { # AdMob / Google Mobile Ads find_and_patch "com/google/android/gms/ads/MobileAds" \ "initialize,setRequestConfiguration" \ "AdMob" "Lcom/google/android/gms/ads/MobileAds;" find_and_patch "com/google/android/gms/ads/interstitial/InterstitialAd" \ "load" \ "AdMob" "Lcom/google/android/gms/ads/interstitial/InterstitialAd;" find_and_patch "com/google/android/gms/ads/rewarded/RewardedAd" \ "load" \ "AdMob" "Lcom/google/android/gms/ads/rewarded/RewardedAd;" find_and_patch "com/google/android/gms/ads/rewarded/RewardedInterstitialAd" \ "load" \ "AdMob" "Lcom/google/android/gms/ads/rewarded/RewardedInterstitialAd;" find_and_patch "com/google/android/gms/ads/appopen/AppOpenAd" \ "load" \ "AdMob" "Lcom/google/android/gms/ads/appopen/AppOpenAd;" find_and_patch "com/google/android/gms/ads/AdView" \ "loadAd" \ "AdMob" "Lcom/google/android/gms/ads/AdView;" find_and_patch "com/google/android/gms/ads/AdLoader" \ "loadAd,loadAds" \ "AdMob" "Lcom/google/android/gms/ads/AdLoader;" # Unity Ads find_and_patch "com/unity3d/ads/UnityAds" \ "initialize,load,show" \ "UnityAds" "Lcom/unity3d/ads/UnityAds;" # IronSource / LevelPlay find_and_patch "com/ironsource/mediationsdk/IronSource" \ "init,loadInterstitial,showInterstitial,showRewardedVideo,loadBanner" \ "IronSource" "Lcom/ironsource/mediationsdk/IronSource;" # AppLovin / MAX find_and_patch "com/applovin/sdk/AppLovinSdk" \ "getInstance,initializeSdk" \ "AppLovin" "Lcom/applovin/sdk/AppLovinSdk;" # Meta Audience Network find_and_patch "com/facebook/ads/AudienceNetworkAds" \ "initialize,buildInitSettings" \ "MetaAN" "Lcom/facebook/ads/AudienceNetworkAds;" # Vungle / Liftoff find_and_patch "com/vungle/warren/Vungle" \ "init,loadAd,playAd" \ "Vungle" "Lcom/vungle/warren/Vungle;" find_and_patch "com/vungle/ads/VungleInterstitial" \ "load,show" \ "Vungle" "Lcom/vungle/ads/VungleInterstitial;" find_and_patch "com/vungle/ads/VungleRewarded" \ "load,show" \ "Vungle" "Lcom/vungle/ads/VungleRewarded;" find_and_patch "com/vungle/ads/VungleBanner" \ "load" \ "Vungle" "Lcom/vungle/ads/VungleBanner;" # InMobi find_and_patch "com/inmobi/sdk/InMobiSdk" \ "init" \ "InMobi" "Lcom/inmobi/sdk/InMobiSdk;" find_and_patch "com/inmobi/ads/InMobiInterstitial" \ "load,show" \ "InMobi" "Lcom/inmobi/ads/InMobiInterstitial;" find_and_patch "com/inmobi/ads/InMobiBanner" \ "load" \ "InMobi" "Lcom/inmobi/ads/InMobiBanner;" # Chartboost find_and_patch "com/chartboost/sdk/Chartboost" \ "startWithAppId,cacheInterstitial,showInterstitial,cacheRewardedVideo,showRewardedVideo" \ "Chartboost" "Lcom/chartboost/sdk/Chartboost;" # Pangle / TikTok (legacy API) find_and_patch "com/bytedance/sdk/openadsdk/TTAdSdk" \ "init" \ "Pangle" "Lcom/bytedance/sdk/openadsdk/TTAdSdk;" # Pangle new API find_and_patch "com/pgl/sys/ces/PAGSdk" \ "init" \ "Pangle" "Lcom/pgl/sys/ces/PAGSdk;" # Mintegral find_and_patch "com/mbridge/msdk/MBridgeSDKFactory" \ "getMBridgeSDK" \ "Mintegral" "Lcom/mbridge/msdk/MBridgeSDKFactory;" } patch_tracker_targets() { # Firebase Analytics find_and_patch "com/google/firebase/analytics/FirebaseAnalytics" \ "getInstance,logEvent,setUserId,setUserProperty,setAnalyticsCollectionEnabled" \ "Firebase" "Lcom/google/firebase/analytics/FirebaseAnalytics;" # Adjust find_and_patch "com/adjust/sdk/Adjust" \ "onCreate,trackEvent,addSessionCallbackParameter,addSessionPartnerParameter,setEnabled" \ "Adjust" "Lcom/adjust/sdk/Adjust;" # AppsFlyer find_and_patch "com/appsflyer/AppsFlyerLib" \ "getInstance,init,start,logEvent,setCustomerUserId" \ "AppsFlyer" "Lcom/appsflyer/AppsFlyerLib;" # Mixpanel find_and_patch "com/mixpanel/android/mpmetrics/MixpanelAPI" \ "getInstance,track,trackMap,identify,timeEvent,registerSuperProperties" \ "Mixpanel" "Lcom/mixpanel/android/mpmetrics/MixpanelAPI;" # Amplitude find_and_patch "com/amplitude/api/AmplitudeClient" \ "getInstance,initialize,logEvent,setUserId,setUserProperties" \ "Amplitude" "Lcom/amplitude/api/AmplitudeClient;" # Segment find_and_patch "com/segment/analytics/Analytics" \ "with,track,identify,screen,group,alias" \ "Segment" "Lcom/segment/analytics/Analytics;" # Braze (and legacy Appboy) find_and_patch "com/braze/Braze" \ "configure,logCustomEvent,changeUser,logPurchase" \ "Braze" "Lcom/braze/Braze;" find_and_patch "com/appboy/Appboy" \ "configure,logCustomEvent,changeUser" \ "Braze" "Lcom/appboy/Appboy;" # CleverTap find_and_patch "com/clevertap/android/sdk/CleverTapAPI" \ "getDefaultInstance,pushEvent,onUserLogin,pushProfile,recordEvent" \ "CleverTap" "Lcom/clevertap/android/sdk/CleverTapAPI;" # Flurry find_and_patch "com/flurry/android/FlurryAgent" \ "logEvent,setUserId,onStartSession,onEndSession" \ "Flurry" "Lcom/flurry/android/FlurryAgent;" } # ===================================================================== # patch_manifest() — Disable SDK components in AndroidManifest.xml # ===================================================================== patch_manifest() { if [[ "$DO_MANIFEST" == false ]] || [[ ! -f "$MANIFEST" ]]; then return fi # Known SDK components to disable # Format: "component_substring|sdk_name" local -a AD_COMPONENTS=( "com.google.android.gms.ads.AdActivity|AdMob" "com.google.android.gms.ads.MobileAdsInitProvider|AdMob" "com.google.android.gms.ads.AdService|AdMob" "com.unity3d.ads.adunit.AdUnitActivity|UnityAds" "com.unity3d.ads.adunit.AdUnitTransparentActivity|UnityAds" "com.unity3d.services.ads.adunit.AdUnitActivity|UnityAds" "com.ironsource.sdk.controller.InterstitialActivity|IronSource" "com.ironsource.sdk.controller.ControllerActivity|IronSource" "com.applovin.adview.AppLovinFullscreenActivity|AppLovin" "com.applovin.sdk.AppLovinWebViewActivity|AppLovin" "com.facebook.ads.AudienceNetworkActivity|MetaAN" "com.facebook.ads.InterstitialAdActivity|MetaAN" "com.vungle.warren.ui.VungleActivity|Vungle" "com.chartboost.sdk.CBImpressionActivity|Chartboost" "com.bytedance.sdk.openadsdk.activity.TTFullScreenVideoActivity|Pangle" "com.bytedance.sdk.openadsdk.activity.TTRewardVideoActivity|Pangle" "com.vungle.ads.internal.ui.VungleActivity|Vungle" "com.facebook.ads.AudienceNetworkContentProvider|MetaAN" "com.applovin.sdk.AppLovinInitProvider|AppLovin" "io.bidmachine.BidMachineInitProvider|BidMachine" "com.ironsource.lifecycle.IronsourceLifecycleProvider|IronSource" "com.amazon.device.ads.DTBAdActivity|AmazonAPS" "com.bytedance.sdk.openadsdk.activity.TTInterstitialActivity|Pangle" "com.bytedance.sdk.openadsdk.activity.TTAdActivity|Pangle" "com.bytedance.sdk.openadsdk.activity.TTDelegateActivity|Pangle" "com.mbridge.msdk.activity.MBCommonActivity|Mintegral" "com.mbridge.msdk.reward.player.MBRewardVideoActivity|Mintegral" "com.smaato.sdk.core.SmaatoBroadcastReceiver|Smaato" ) local -a TRACKER_COMPONENTS=( "com.google.android.gms.measurement.AppMeasurementService|Firebase" "com.google.android.gms.measurement.AppMeasurementReceiver|Firebase" "com.google.android.gms.measurement.AppMeasurementContentProvider|Firebase" "com.google.android.gms.measurement.AppMeasurementInstallReferrerReceiver|Firebase" "com.google.android.gms.measurement.AppMeasurementJobService|Firebase" "com.google.firebase.iid.FirebaseInstanceIdReceiver|Firebase" "com.adjust.sdk.AdjustReferrerReceiver|Adjust" "com.appsflyer.SingleInstallBroadcastReceiver|AppsFlyer" "com.appsflyer.MultipleInstallBroadcastReceiver|AppsFlyer" "com.mixpanel.android.mpmetrics.MixpanelFCMMessagingService|Mixpanel" "com.braze.push.BrazeFirebaseMessagingService|Braze" "com.clevertap.android.sdk.pushnotification.CTPushNotificationReceiver|CleverTap" "com.clevertap.android.sdk.pushnotification.CTNotificationIntentService|CleverTap" "com.appsflyer.internal.AFSingleInstallBroadcastReceiver|AppsFlyer" ) local -a components_to_disable=() if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_ADS" == true ]]; then components_to_disable+=("${AD_COMPONENTS[@]}") fi if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_TRACKERS" == true ]]; then components_to_disable+=("${TRACKER_COMPONENTS[@]}") fi if [[ "$DO_BACKUP" == true ]] && [[ "$DRY_RUN" == false ]]; then cp "$MANIFEST" "${MANIFEST}.bak" fi for entry in "${components_to_disable[@]}"; do local component_name="${entry%%|*}" local sdk_name="${entry##*|}" # Check if the component exists in the manifest if grep -q "$component_name" "$MANIFEST"; then # Determine component type local comp_type="unknown" if grep -B1 "$component_name" "$MANIFEST" | grep -q "/dev/null) else saved_options=$(awk -F'"' '/"options"/ { print $4 }' "$MANIFEST_JSON") fi case "$saved_options" in *--all*) NEUTRALIZE_ALL=true ;; *--ads*) NEUTRALIZE_ADS=true; NEUTRALIZE_ALL=false ;; *--trackers*) NEUTRALIZE_TRACKERS=true; NEUTRALIZE_ALL=false ;; esac # Extract class:method pairs into a temporary targets file REPLAY_TARGETS=$(mktemp "${TMPDIR:-/tmp}/replay-targets-XXXXXX") if command -v jq &>/dev/null; then jq -r '.patched_methods[]? | .class + ":" + .method' "$MANIFEST_JSON" > "$REPLAY_TARGETS" 2>/dev/null else # Fallback: parse JSON with awk (handles the simple array-of-objects format) awk ' /"class"/ { gsub(/[",]/, ""); gsub(/^[[:space:]]*class:[[:space:]]*/, ""); class = $2 } /"method"/ { gsub(/[",]/, ""); gsub(/^[[:space:]]*method:[[:space:]]*/, ""); method = $2 if (class != "" && method != "") print class ":" method } ' "$MANIFEST_JSON" > "$REPLAY_TARGETS" 2>/dev/null fi replay_count=$(wc -l < "$REPLAY_TARGETS" | tr -d ' ') echo "Loaded $replay_count method targets from previous manifest." if [[ "$replay_count" -gt 0 ]]; then if [[ -n "$TARGETS_FILE" ]]; then # Merge replay targets into existing targets file cat "$REPLAY_TARGETS" >> "$TARGETS_FILE" else TARGETS_FILE="$REPLAY_TARGETS" fi fi echo fi # ===================================================================== # Main # ===================================================================== if [[ "$DRY_RUN" == true ]]; then echo "=== SDK Neutralizer — DRY RUN ===" else echo "=== SDK Neutralizer ===" fi echo "Decoded directory: $DECODED_DIR" echo "Smali directories: ${SMALI_DIRS[*]}" echo # Patch SDK targets if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_ADS" == true ]]; then echo "--- Neutralizing Ad SDK entry points ---" patch_ad_targets echo fi if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_TRACKERS" == true ]]; then echo "--- Neutralizing Tracker SDK entry points ---" patch_tracker_targets echo fi # Patch custom targets if [[ -n "$TARGETS_FILE" ]]; then echo "--- Neutralizing custom targets from $TARGETS_FILE ---" patch_custom_targets echo fi # Patch manifest patch_manifest # Summary echo echo "=== Neutralization Summary ===" echo "Methods patched: $METHODS_PATCHED" echo "Manifest components disabled: $COMPONENTS_DISABLED" if [[ "$DRY_RUN" == true ]]; then echo echo "DRY RUN — no files were modified." echo "Remove --dry-run to apply changes." fi if [[ "$METHODS_PATCHED" -eq 0 ]] && [[ "$COMPONENTS_DISABLED" -eq 0 ]]; then echo echo "No targets found in the decoded directory." echo "The app may not contain the targeted SDKs, or they may use obfuscated class names." exit 2 fi # ===================================================================== # Save neutralize-manifest.json # ===================================================================== if [[ "$DO_SAVE_MANIFEST" == true ]] && [[ "$DRY_RUN" == false ]] && \ [[ "$METHODS_PATCHED" -gt 0 || "$COMPONENTS_DISABLED" -gt 0 ]]; then MANIFEST_JSON="$DECODED_DIR/neutralize-manifest.json" TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%S") # Build options string OPTIONS_STR="" if [[ "$NEUTRALIZE_ALL" == true ]]; then OPTIONS_STR="--all" else [[ "$NEUTRALIZE_ADS" == true ]] && OPTIONS_STR="${OPTIONS_STR} --ads" [[ "$NEUTRALIZE_TRACKERS" == true ]] && OPTIONS_STR="${OPTIONS_STR} --trackers" OPTIONS_STR="${OPTIONS_STR# }" fi # Build JSON from patch log file using printf (no jq dependency for writing) { printf '{\n' printf ' "timestamp": "%s",\n' "$TIMESTAMP" printf ' "options": "%s",\n' "$OPTIONS_STR" printf ' "patched_methods": [\n' first_method=true while IFS= read -r logline; do if [[ "$logline" == PATCHED:* ]]; then # PATCHED::::: IFS=':' read -r _ p_sdk p_class p_method p_stub p_file <<< "$logline" if [[ "$first_method" == true ]]; then first_method=false else printf ',\n' fi printf ' {"sdk": "%s", "class": "%s", "method": "%s", "stub": "%s", "file": "%s"}' \ "$p_sdk" "$p_class" "$p_method" "$p_stub" "$p_file" fi done < "$PATCH_LOG_FILE" printf '\n ],\n' printf ' "disabled_components": [\n' first_comp=true while IFS= read -r logline; do if [[ "$logline" == MANIFEST_DISABLED:* ]]; then # MANIFEST_DISABLED::: IFS=':' read -r _ c_type c_name c_sdk <<< "$logline" if [[ "$first_comp" == true ]]; then first_comp=false else printf ',\n' fi printf ' {"type": "%s", "name": "%s", "sdk": "%s"}' \ "$c_type" "$c_name" "$c_sdk" fi done < "$PATCH_LOG_FILE" printf '\n ]\n' printf '}\n' } > "$MANIFEST_JSON" echo echo "Patch manifest saved: $MANIFEST_JSON" echo "Use --replay after re-decode to reapply the same patches." fi exit 0