Find the configuration mistakes that get App Store and Google Play builds
refused, before you upload: one Dart command lints Info.plist, the privacy
manifest, entitlements and AndroidManifest.xml, and fails CI on them.
The GitHub Action on storehound-demo, a Flutter app configuration with common mistakes.
- A missing purpose string or a privacy manifest that never reaches the bundle
compiles, passes every test, and only shows up as an
ITMS-email after upload. - A vague purpose string or an unjustified background mode costs you a whole review cycle.
- Play refuses an update below the target API deadline, usually during an urgent fix.
It reads the files you ship and answers in about a second. No Xcode, no macOS runner, no build.
dart pub global activate storehoundOr pin it per project with flutter pub add --dev storehound and
run dart run storehound.
This package was renamed. Replace imaji_store_preflight with storehound in
dev_dependencies (or dart pub global activate storehound), run
storehound instead of imaji_store_preflight, and rename
imaji_store_preflight.yaml to storehound.yaml. Rules and options are
unchanged.
cd my_flutter_app
storehoundIf ~/.pub-cache/bin is not on your PATH, run
dart pub global run storehound. Exit code 0 means clean, 1
means findings at or above --fail-on (default error), 2 means a usage or
configuration problem.
A fresh flutter create app gets one warning: no privacy manifest yet. Here
it is after flutter pub add image_picker and a one-line camera purpose
string (--brief drops the why/fix text):
$ storehound --brief
privacy-manifest-present — The app target ships a PrivacyInfo.xcprivacy
warning ios/Runner/Info.plist
No privacy manifest at ios/Runner/PrivacyInfo.xcprivacy (or the path
configured under `paths.privacy_manifest`).
purpose-string-missing — Linked plugins have the purpose strings they need
error ios/Runner/Info.plist
`NSPhotoLibraryUsageDescription` is missing, but image_picker needs it.
warning ios/Runner/Info.plist
`NSMicrophoneUsageDescription` is missing; image_picker needs it only if you
record video (image_picker README, "iOS").
purpose-string-quality — Purpose strings name a feature and give an example, not restate the permission
error ios/Runner/Info.plist:5
`NSCameraUsageDescription` only restates the permission; name the feature
that uses it and give an example. Current text: "This app needs camera
access."
✗ 15 rules checked: 2 errors, 2 warnings, 0 info. Failing: findings at or above `error`.
A key a plugin needs in every app is an error; a key its README asks for
only when you use one feature, like the microphone for recording video, is a
warning that names the feature. The exit code is 1, so CI fails. Without --brief each finding also says
why it matters, how to fix it, and links Apple's or Google's documentation.
--format json and --sarif give the same findings in machine-readable form.
--format markdown writes them as a summary for $GITHUB_STEP_SUMMARY or a
pull request comment, and --format github as pull request annotations.
This job fails the build on errors, shows each finding as an annotation on the pull request and writes a job summary:
name: Store preflight
on: [push, pull_request]
jobs:
storehound:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: saimskywalker/storehound@v0No macOS runner needed. The JSON report is uploaded as the storehound-report
artifact. With sarif: true and security-events: write the findings also go
to code scanning (a public repository or GitHub Advanced Security).
More on CI:
inputs, permissions, a pull request comment, and how android-target-sdk
behaves on a Dart-only runner.
With storehound on your PATH (dart pub global activate storehound), add
this to .pre-commit-config.yaml; it runs when a store-facing file changes:
repos:
- repo: https://github.com/saimskywalker/storehound
rev: v0.2.2
hooks:
- id: storehound
# entry: dart run storehound # when it is a dev dependency| Rule | Catches |
|---|---|
purpose-string-missing |
ITMS-90683 "Missing purpose string" for plugins like image_picker, camera, geolocator |
purpose-string-quality |
Guideline 5.1.1(ii): a purpose string that only restates the permission |
privacy-manifest-present |
ITMS-91053: no PrivacyInfo.xcprivacy, or one missing from Copy Bundle Resources |
privacy-manifest-values |
Data types, purposes and reason codes that are not Apple's exact values |
privacy-manifest-tracking |
NSPrivacyTracking contradicting the entries or tracking domains |
privacy-manifest-sdk-data |
"Collects nothing" while Firebase, AdMob, Sentry or another known SDK is linked |
tracking-usage-description |
"Add for Review" refused over NSUserTrackingUsageDescription vs declared tracking |
background-modes-justified |
Guideline 2.5.4: background modes with no feature behind them |
push-entitlement |
Push SDK linked without aps-environment, so notifications never arrive |
export-options-build-number |
Xcode silently bumping CFBundleVersion at export |
bundle-localizations |
CFBundleLocalizations out of sync with your ARB locales |
android-target-sdk |
targetSdk below Google Play's current deadline |
android-16kb-page-size |
A build toolchain without 16 KB page-size support |
android-picker-queries |
file_picker without an OPEN_DOCUMENT <queries> entry |
unreadable-file |
A config file that does not parse, so rules would silently pass |
storehound --explain <rule-id> prints the full why, fix, options
and documentation links for one rule.
Pins only your app cares about go under checks: in storehound.yaml, each
with a reason, and are reported like any rule as check:<name>:
checks:
no-backup:
file: android_manifest
element: application
attribute: android:allowBackup
equals: "false"
reason: "Tokens live in shared preferences."They work on Info.plist keys and AndroidManifest.xml attributes; see
Project checks
for the operators.
It should not be. CI runs it with --fail-on info on
a production-shaped fixture
(two flavors, SwiftPM only, AGP 9, separate Debug and Release entitlements,
a large commented Info.plist) and expects no findings at all, and the
tests change one thing at a time in it to check that each mistake is still
caught. If it reports something on your app that is not a real problem,
please open an issue with an anonymised copy of the lines involved.
- Rules and the store messages they prevent, including planned rules
- Command line, configuration and library use
- GitHub Actions and a pull request comment
- FAQ, limitations and comparisons
- pub.dev page and API docs
- CHANGELOG and issues
The most useful contributions are a rejection you hit that no rule catches
(open an issue with the anonymised reviewer wording or ITMS- code and the
public Apple or Google documentation) and corrections to the data tables.
CONTRIBUTING.md
explains how to add a rule; issues labelled
good first issue
are a good place to start. Questions and ideas that are not bugs go to
Discussions.
Apple, App Store, iOS and Xcode are trademarks of Apple Inc. Google Play and Android are trademarks of Google LLC. This project is independent and is not affiliated with, sponsored by or endorsed by Apple or Google; the names are used only to say which store rules a check is about.
MIT © saimskywalker. See LICENSE.
