App Store Compliance Playbook

The guard scans the app’s built artifact surface (Info.plist, AndroidManifest.xml, entitlements, gradle, PrivacyInfo.xcprivacy) which store review inspects regardless of what generated it. This page documents the checks specific to the framework that generated the app, on top of the native checks.

Detection

Framework Detected by
Flutter pubspec.yaml present within 12 levels of the project root
React Native / Expo package.json within 12 levels with a dependency on react-native or expo
Ionic / Capacitor / Cordova package.json within 12 levels with a dependency on @capacitor/*, @ionic/*, or cordova-*, OR a capacitor.config.* / config.xml file within 12 levels

These depths come from the find_app calls in agent-os/hooks/app-store-compliance-guard.sh. Unity, .NET MAUI, Tauri mobile, and Kotlin Multiplatform are searched within 6 levels.

A project can match more than one framework flag (a Capacitor app that also imports expo polyfills, for example); every matching check runs. Detection scans EVERY package.json/config.xml within depth, not only the first, so a monorepo whose root package.json is tooling-only still finds the real app deeper in the tree. A config.xml only counts as Cordova evidence when it carries a <widget> or xmlns:cdv marker, since that filename collides with unrelated tooling configs.

Every framework-specific finding below is gated on IOS_TARGET_ACTIVE, not raw file-tree presence. A committed ios/ folder is common in a real cross-platform repo even when the current build is Android-only, so file presence alone cannot tell flutter build apk apart from an iOS build. When the invoking command is known (hook mode) and clearly targets Android only (build apk/appbundle, assembleRelease, bundleRelease, run-android, run:android, --platform android, capacitor android, with no ios/ipa/xcodebuild token present), the command overrides the file-tree signal and these Apple-only checks stay silent. 2.0.0 also requires every other command in the line to be a known harmless one, so it never silences more than 1.0.0 did. Standalone mode (no command context) falls back to plain file-tree detection.

Submission commands the guard recognizes

fastlane deliver/pilot/supply/submit, eas submit, eas build, xcrun altool, xcrun notarytool, transporter, gradlew bundleRelease/assembleRelease, bundletool, xcodebuild archive, flutter build ipa/appbundle/apk, cap sync/build/run (with or without npx), ionic capacitor build/run, cordova build (with or without --release), Unity ... -buildTarget iOS/Android, dotnet publish -f net8.0-ios/android, tauri ios/android build.

Flutter checks

Known limitation. The guard cannot see whether a purpose-string value that exists only in a localized InfoPlist.strings file was also copied into the real Info.plist shipped in the archive (ITMS-90683). Check this by hand before submitting.

React Native / Expo checks

Ionic / Capacitor / Cordova checks

Unity, .NET MAUI, Tauri mobile, and Kotlin Multiplatform

Added for issue 837. Each one is detected, named on the Frameworks. line, and has a bad and a clean project in the test suite.

Framework Detected by Source now read Submit command that runs the scan
Unity ProjectSettings/ProjectVersion.txt .cs Unity -batchmode ... -buildTarget iOS or Android
.NET MAUI a .csproj with UseMaui or a net8.0-ios style target .cs, .xaml, .csproj dotnet publish -f net8.0-ios or net8.0-android
Tauri mobile tauri.conf.json, tauri.conf.json5 or Tauri.toml .rs, .toml tauri ios build, tauri android build, with or without cargo or npx
Kotlin Multiplatform the multiplatform plugin in a Gradle file commonMain Kotlin, as before the Gradle and xcodebuild archive commands already covered

Four behaviours to know.

What stays out of reach

These are limits, not bugs waiting for a fix. Plan around them.

Known gaps (found by Codex and Qwen adversarial review, not yet fixed)

These are logged, not hidden. Track them before treating this guard as complete coverage for a cross-platform team; the checks that exist are a real improvement over native-only, not a finished answer.

The honest limit

The guard reads Swift, Kotlin, XML, Gradle, Plist, Dart, JS, TS, C#, and Rust source text. It does not execute the app, does not parse the Dart or JS AST, and cannot see runtime behavior (whether an OTA update actually changes the UI, for example). Treat every finding as a lead to verify, not a guaranteed defect, and treat a clean run as “nothing detectable from source text”, not a guarantee of approval.