Docs

shotfleet documentation

Localized App Store and Google Play screenshots from one English Maestro flow: capture, check, export to fastlane.

Last checked against version 0.1.0 on 2026-10-02.

Requirements

Install

curl -fsSL https://shotfleet.com/install.sh | sh
shotfleet activate <your key>    # from your Gumroad receipt email (check works without one)
shotfleet doctor                 # checks Maestro, Java, Xcode, Android SDK

The installer downloads the Apple silicon archive, compares it with its published SHA-256 checksum, and puts a link to shotfleet on your PATH. shotfleet check is free for everyone, and run is free for up to 2 languages. A licence unlocks every language and export; see licence and refunds.

Quickstart

Once Maestro and a simulator build exist, in your project folder:

shotfleet init
shotfleet init path/to/MyApp.app path/to/app-debug.apk   # or name a simulator build (Debug-iphonesimulator) and/or an APK yourself

shotfleet init without arguments only reads folders, never builds. For iOS it takes the newest Debug-iphonesimulator app in Xcode's default DerivedData folder (~/Library/Developer/Xcode/DerivedData) whose info.plist points at an Xcode project in this folder (or one folder below it, or above it). For Android it takes the newest APK under app/build/outputs/apk/debug (or <module>/build/outputs/apk/debug, one folder below). It prints which build it chose and how old it is. If it finds nothing it exits with the exact xcodebuild and ./gradlew assembleDebug commands to run; then run shotfleet init again.

  1. Edit the generated flow.yaml: Maestro steps in English. Navigate by the text users see and add a takeScreenshot: for each store image. No appId is needed: shotfleet uses each build's own id, so one flow serves iOS and Android.
  2. Try one language: shotfleet run --locales en
  3. Run them all: shotfleet run (finds ./shotfleet.toml; both platforms if the file has both, --platform ios for one).
  4. Review out/index.html (out-ios/ and out-android/ if the config has both platforms). It is a grid with a row per language and a column per screen, and problems are outlined in red.
  5. Export to fastlane: shotfleet export out --fastlane fastlane/ (out-ios or out-android if the config has both). That writes deliver's screenshots/<locale>/ or supply's metadata/android/<locale>/images/phoneScreenshots/. Languages that failed a check are skipped, so a bad screenshot never reaches a listing.
shotfleet run --locales en
shotfleet run
shotfleet run --platform ios
shotfleet export out --fastlane fastlane/

Uploading

shotfleet stops at fastlane's folders; fastlane uploads them. Screenshots only, nothing else touched:

# App Store: replaces the screenshots of every language folder in fastlane/screenshots
fastlane deliver --skip_binary_upload --skip_metadata --overwrite_screenshots --force

# Google Play: uploads metadata/android/<locale>/images/phoneScreenshots
fastlane supply --skip_upload_apk --skip_upload_aab --skip_upload_metadata --skip_upload_changelogs --skip_upload_images

Run shotfleet check fastlane/ right before uploading: export already skips languages that failed, and the check also catches folders you edited by hand.

How it works

shotfleet reads the translations compiled into your app and rewrites every selector in the flow to match any translation of that English label. If "Next" has two keys, German matches (Nächste|Weiter). Selectors that use id: pass through untouched.

Fresh state: each language starts from a clean install. On iOS that is clearState; on Android shotfleet clears the app data itself, because clearState would also erase the app language.

Check screenshots from any tool

Already have screenshots from fastlane snapshot or screengrab, StoreScreens or by hand? Check them without capturing anything:

shotfleet check fastlane/ --app path/to/MyApp.app          # deliver: screenshots/<locale>/
shotfleet check fastlane/ --app path/to/app-release.apk    # supply: metadata/android/<locale>/images/phoneScreenshots/

Store codes are mapped back to your app's languages (de-DE to de, no to nb, zh-CN to zh-Hans). The report (check.json and index.html) goes to ./shotfleet-check (change it with --report), and your fastlane folders are never touched. After capture or on an existing folder, every screenshot's text is compared against your app's own strings. On iOS, shotfleet reads the text straight from the app while each screenshot is on screen; otherwise, and for screenshots made by other tools, it uses macOS's built-in text recognition. It reports:

check without --app relies on language detection, which cannot judge short screens; pass --app for the full check.

Config

The whole config can be this (shotfleet init writes it):

[ios]
app = "path/to/MyApp.app"

[android]                 # optional: the same file and flow serve both platforms
apk = "path/to/app-debug.apk"

Everything else is read from the build or has a default; set it only to change it. A typo or a wrong type is refused with a one-line message before anything runs.

Config keys, the section they go in and their defaults
KeySectionDefaultWhat it does
appiosnoneThe simulator build (Debug-iphonesimulator).
apkandroidnoneThe debug APK.
flowios, androidflow.yamlThe English Maestro flow, shared by both platforms (an appId: header is set for you).
outios, androidout, or out-ios and out-android when the file has both platformsOutput folder.
parallelios, android4Simulators (emulators on Android).
localesios, androidevery language the app shipsLanguages to capture, for example ["de", "ja"].
base_localeios, androidenThe language your flow is written in.
stringsios, androidread from the buildTranslation files as globs, relative to the config (see below).
bundle_idiosread from the appThe bundle id. On Android the package is read from the APK.
device_typeiosthe 6.9" iPhone (com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro-Max)The simulator device type.
runtimeiosnewest installedThe simulator runtime, for example com.apple.CoreSimulator.SimRuntime.iOS-26-5.
device_typesiosnoneSeveral sizes from one flow, for example ["iPhone 17 Pro Max", "iPad Pro 13-inch (M5)"]; output goes to out/<device>/, check out and export out handle both, and the App Store takes both in one folder.
devicesandroidphone["phone", "tablet"]: phone 1080x1920 and tablet 1440x2560 (both 9:16, so Play can recommend the listing); output goes to out/<device>/, exported to phoneScreenshots and tenInchScreenshots.
[ios]
flow = "flow.yaml"
out = "out"
parallel = 4
locales = ["de", "ja"]
base_locale = "de"
device_types = ["iPhone 17 Pro Max", "iPad Pro 13-inch (M5)"]

[android]
devices = ["phone", "tablet"]

Flutter, React Native, .NET MAUI, Unity and translation-platform exports

These frameworks do not keep their translations in a form shotfleet can read inside the built app, so point it at the source files instead. Add strings to either section (globs, relative to the config):

strings = "lib/l10n/*.arb"                      # Flutter
strings = "src/locales/*/translation.json"      # React Native / Expo with i18next
strings = ["Resources/Strings/*.resx"]          # .NET MAUI

Supported: .arb, i18next and react-intl .json (nested keys, _one and _other plurals), .xcstrings, .strings, .stringsdict, Android and Compose strings.xml, XLIFF (.xlf, .xliff), .resx, gettext .po, .properties, and CSV with one column per language (Unity Localization, spreadsheets). ICU plurals and placeholders (%d, %1$@, {name}, {{name}}) are understood. The language comes from @@locale, the file name (app_de.arb, Resources.de.resx) or the folder (locales/de/, values-de/). check takes the same files:

shotfleet check fastlane/ --strings 'lib/l10n/*.arb'

Hooks

Nothing here is on by default: a config without these tables behaves exactly as above. Hooks run your own commands (/bin/sh, in the config's folder, output on stderr) around a run and an export:

[hooks]
before_run   = "make build-sim"                 # a string, or a list of commands run in order
after_run    = 'curl -sS -X POST -H "Content-type: application/json" "$SLACK_WEBHOOK_URL" --data "{\"text\":\"shotfleet $SHOTFLEET_PLATFORM: $SHOTFLEET_PROBLEMS problem(s)\"}"'
before_export = "git diff --quiet"              # a non-zero exit stops the export
after_export  = 'fastlane deliver --skip_binary_upload --skip_metadata --overwrite_screenshots --screenshots_path "$SHOTFLEET_FASTLANE/screenshots"'
# hook_timeout_s = 1800                         # default 600 per command

A failing before_* hook stops the command (exit 2); a failing after_* hook makes the exit code 1, so CI notices. Variables: SHOTFLEET_EVENT, _VERSION, _CONFIG, _PLATFORM, _OUT, _BUNDLE_ID; before_run and after_run also get _LOCALES; after_run gets _EXIT, _PROBLEMS, _REPORT (<out>/check.json); the export hooks get _FASTLANE, _REPORT (and _EXIT after the export). --no-hooks skips them for a dry run.

Overrides

Overrides replace what shotfleet would tap for one language. They are the fix for a collision warning ('Next' -> 'İleri' also means 'Advanced') and for labels that are not in your app's strings:

[overrides.tr]                         # the language as shotfleet names it: tr, pt-BR, zh-Hans...
"Next" = "İleri"                       # literal text
"Advanced" = "re:Gelişmiş.*"           # "re:" = a regular expression
"Search" = "Ara"

Inputs

Inputs set the text a flow types, per language. Write inputText: ${name} in the flow and give the values in the config:

[inputs]                               # typed in every language that has no value of its own
name = "Alex"
[inputs.ja]                            # the language as shotfleet names it; pt covers pt-BR too
name = "田中"

The value goes into that language's flow as a Maestro env: entry. This works on iOS; Android runs type the [inputs] defaults only, because Maestro hands any flow to any emulator. The values are tested as flow text, not yet run on a device, so check the first screenshots in a script like Japanese or Arabic. Text you type is not translated by itself: it stays the same in every language unless you set [inputs] (iOS only).

Plugins and the rest

shotfleet foo runs shotfleet-foo from your PATH (like git and gh), when foo is not a built-in command. run, check and export take --json and use stable exit codes, so they script like any other tool; SHOTFLEET_OCR or --ocr-command swaps the text reader; strings = "glob" swaps the translation source.

In CI

run, check and export take --json: the result goes to stdout as JSON (exit, screenshots, problems for run and check; exit, exported, skipped for export) and progress goes to stderr. Exit codes: 0 all good, 1 problems found, 2 setup error (the message says what to fix), 130 interrupted.

Checking needs only a Mac (it uses macOS text recognition), with no simulators, so it fits a pull-request job on GitHub's macOS runners. check is free and needs no licence. This recipe has not been run on GitHub yet; the command itself is the one tested above.

# .github/workflows/screenshots.yml
on: pull_request
jobs:
  check-screenshots:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - run: xcodebuild -scheme MyApp -sdk iphonesimulator -configuration Debug -derivedDataPath build build
      - run: curl -fsSL https://shotfleet.com/install.sh | sh   # Apple silicon runner
      - run: shotfleet check fastlane/ --app build/Build/Products/Debug-iphonesimulator/MyApp.app --json > check.json
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: screenshot-check
          path: |
            check.json
            shotfleet-check/

To use run or export in CI, set SHOTFLEET_KEY to your licence key (keep it in a secret). It is checked online on each run and uses none of your 3 activations; if the licence server cannot be reached the job is not failed. With an invalid key, export refuses and run captures only 2 languages.

With AI agents

shotfleet mcp is an MCP server with check, run and doctor tools. Add it to Claude Code with claude mcp add shotfleet -- shotfleet mcp, or to any MCP client as the command shotfleet mcp.

There are also three agent skills that ship with the program: shotfleet (install, set up, run, check and troubleshoot), shotfleet-ci (add shotfleet check to CI) and shotfleet-review (turn a check report into a fix list). Copy them into a skills folder:

shotfleet skill install
shotfleet skill install --project
shotfleet skill install --dir <folder>

shotfleet skill install copies them into ~/.claude/skills, --project into ./.claude/skills, and --dir <folder> into another agent's skills folder. Skills that are already there are kept unless you pass --force.

Limits

Manuel Lorenzo Parejo · Licence and refunds · Privacy