skill
simshot
App Store screenshot capture CLI (simctl only, no XCUITest)
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/kichiemon/simshot itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 21, 2026simshot
Capture App Store-ready screenshots from the iOS Simulator with simctl only — no XCUITest, no flaky test runners, no hangs.
⭐ If simshot makes your next release easier, consider starring the repo. Questions or ideas? Join the Discussions.
Your app implements a tiny #if DEBUG launch-argument handler; simshot does the rest: build, boot simulators, override the status bar, launch each scene, capture, and resize to App Store sizes.
simshot shoot --project MyApp.xcodeproj --scheme MyApp \
--bundle-id com.example.myapp \
--devices iphone-17-pro-max,ipad-pro-13 \
--langs ja,en --shots shots.json --resize
Why simshot?
fastlane snapshot drives the UI through XCUITest, which means maintaining test targets, fighting flaky element queries, and dealing with runner crashes. simshot inverts the model: the app itself navigates to each scene (reading a documented launch-argument protocol), and simshot just drives simctl with a hard timeout on every step.
| simshot | fastlane snapshot | |
|---|---|---|
| UI automation | None — simctl only | XCUITest runner |
| Hangs | Hard timeout on every command | Can hang CI |
| Ruby | No | Ruby + fastlane + gems |
| Test target | #if DEBUG handler in your app | Dedicated UI test target |
| Status bar override | Built in | Plugin |
| Resize to App Store sizes | Built in (--resize) | Separate tooling |
| Multi-language | --langs ja,en | Per-locale setup |
| Dependency footprint | Zero | Dozens of gems |
Xcode 27 / Device Hub: simshot drives
xcrun simctlonly and never opensSimulator.app, so it keeps working on Xcode 27, where Device Hub replaces the Simulator app.
How it works
xcodebuildbuilds the app for the generic simulator (or use an existing.appwith--app-path).- For each device × language:
simctl bootstatus <udid> -bboots (and waits).simctl status_bar <udid> override --time "9:41" --batteryState charged --batteryLevel 100 ...prettifies the status bar.- The app is installed (previous install removed first for a clean state).
- For each shot, simshot launches the app with the scene protocol args, waits for the scene to settle, then takes
simctl io <udid> screenshot.
- Raw captures land in
<output>/raw/<device>/<lang>/, and (with--resize) App Store-ready copies — alpha flattened, resized to the matching size — in<output>/<device>/<lang>/.
Every external command runs through a timeout + retry wrapper, so a crashed app or stuck simulator can never hang CI.
Install
Eight ways to get simshot — pick one:
| # | Path | Command | Needs |
|---|---|---|---|
| 1 | Install script (curl) | curl -fsSL …/install.sh | bash | macOS |
| 2 | Homebrew (tap) | brew tap kichiemon/homebrew-tap && brew install simshot | Homebrew |
| 3 | Single binary (curl) | download simshot-macos-{arm64,x86_64} | macOS |
| 4 | Nix | nix profile install github:kichiemon/simshot | Nix |
| 5 | mise | mise use -g github:kichiemon/simshot | mise |
| 6 | npm | npm i -g simshot | Node.js 14+ |
| 7 | Mint | mint install kichiemon/simshot | Xcode |
| 8 | Build from source | swift build -c release | Xcode |
| + | Agent skill | npx skills add kichiemon/simshot | npx |
Install script (curl, macOS)
One-liner that detects your architecture, downloads the matching prebuilt binary from GitHub Releases, verifies its sha256, and installs it to /usr/local/bin (or ~/.local/bin):
curl -fsSL https://raw.githubusercontent.com/kichiemon/simshot/main/install.sh | bash
Customize with SIMSHOT_VERSION (release tag) and SIMSHOT_INSTALL_DIR:
SIMSHOT_VERSION=v0.2.2 SIMSHOT_INSTALL_DIR=~/.local/bin \
curl -fsSL https://raw.githubusercontent.com/kichiemon/simshot/main/install.sh | bash
Install as an agent skill (npx)
npx skills add kichiemon/simshot
Installs simshot as a reusable agent skill (for Claude Code / opencode and other skill-aware agents). Once installed, skill-aware agents can use simshot automatically — just ask it to capture App Store screenshots, and the agent builds and runs simshot shoot for you (see skills/simshot/SKILL.md).
Target a specific agent, for example OpenAI Codex:
npx skills add kichiemon/simshot -a codex
This installs the skill into .agents/skills/ (Codex's project skill directory). The skill is kept in sync across three locations — skills/simshot/SKILL.md (canonical), .claude/skills/simshot/SKILL.md (Claude Code), and .agents/skills/simshot/SKILL.md (Codex and other .agents/skills/ agents) — so all skill-aware agents get identical instructions.
Build from source
git clone https://github.com/kichiemon/simshot.git
cd simshot
swift build -c release
# binary at .build/release/simshot — symlink or copy it into your PATH
ln -s "$(pwd)/.build/release/simshot" /usr/local/bin/simshot
Single binary (curl, macOS)
Grab the prebuilt binary for your architecture directly from GitHub Releases — no Swift toolchain required:
# Apple Silicon (arm64)
curl -sL https://github.com/kichiemon/simshot/releases/download/v0.2.2/simshot-macos-arm64 -o /usr/local/bin/simshot && chmod +x /usr/local/bin/simshot
# Intel (x86_64)
curl -sL https://github.com/kichiemon/simshot/releases/download/v0.2.2/simshot-macos-x86_64 -o /usr/local/bin/simshot && chmod +x /usr/local/bin/simshot
Nix
The repo ships a flake.nix that fetches the prebuilt single binary (no source build):
nix run github:kichiemon/simshot # run without installing
nix profile install github:kichiemon/simshot # install into your profile
mise
mise installs simshot from the GitHub Releases assets. The github backend is recommended (it also verifies SLSA provenance / artifact attestations):
mise use -g github:kichiemon/simshot # install into your global config
simshot --version # v0.2.2
The legacy ubi backend (mise use ubi:kichiemon/simshot) resolves the same simshot-macos-{os}-{arch} release assets and also works, but it is deprecated in mise and will be removed in mise 2027.1.0.
Homebrew (tap, recommended)
simshot ships a tap formula that installs the prebuilt release binary. First time:
brew tap kichiemon/homebrew-tap
brew install simshot
Formula source:
kichiemon/homebrew-tap(Formula/simshot.rb):
class Simshot < Formula
desc "App Store screenshot capture CLI (simctl only, no XCUITest)"
homepage "https://github.com/kichiemon/simshot"
version "0.2.2"
license "MIT"
on_macos do
on_arm do
url "https://github.com/kichiemon/simshot/releases/download/v0.2.2/simshot-macos-arm64.tar.gz"
sha256 "d211202ff00a0f1dda9b89ffd502741c842431189439e3df7a7292997b3d395a"
end
on_intel do
url "https://github.com/kichiemon/simshot/releases/download/v0.2.2/simshot-macos-x86_64.tar.gz"
sha256 "1a2cbad0b7dc1ec50ebd1be22d5c6e0a6e5a1ce69670ebffbc311a9f960ee9b8"
end
end
def install
bin.install "simshot"
end
end
Mint
Mint builds and installs simshot from source via the Mintfile (requires Xcode):
brew install mint
mint install kichiemon/simshot
npm (npmjs.com)
npm i -g simshot
npx simshot shoot
Requires Node.js 14+ on macOS (arm64/x86_64). The package downloads the matching prebuilt single binary from GitHub Releases in postinstall (see npm/) and verifies its sha256. Published on npm as simshot (0.2.2).
Quick start
-
Add the scene protocol handler to your app (5 minutes).
-
Verify the environment and list simulators:
simshot doctor # diagnose xcode-select / Xcode / simctl / runtimes simshot devices # list available simulators -
Create a
shots.json(seeexamples/shots.json) or use--scenes:simshot shoot --project MyApp.xcodeproj --scheme MyApp \ --bundle-id com.example.myapp \ --devices iphone-17-pro-max,ipad-pro-13 \ --langs ja,en --scenes home,detail,settings \ --output appstore --resize
Screenshots land in appstore/<device>/<lang>/NN_name.png.
Commands
| Command | Description |
|---|---|
simshot shoot <options> | Build, boot, capture, and resize screenshots. |
simshot devices | List available simulators (name + UDID). |
simshot doctor | Diagnose the local Xcode / simulator environment (exit 1 on problems). |
simshot init | Generate a commented simshot.yml config scaffold. |
simshot verify [dir] | Check captured screenshots against App Store requirements (exit 1 on problems). |
simshot version | Print the version. |
simshot help | Show help. |
simshot doctor
Checks xcode-select, Xcode, simctl, iOS runtimes, and available simulators, and exits 1 if anything is broken. Run it when setting up a new machine or before filing a bug.
$ simshot doctor
✅ xcode-select: /Applications/Xcode.app/Contents/Developer
✅ Xcode: Xcode 27.0
✅ simctl: found
✅ iOS runtimes: 7 installed
✅ Available simulators: 36 available
✅ All checks passed.
simshot init
Generates a commented simshot.yml scaffold that documents the options you'd pass to simshot shoot. Interactive by default; --yes writes it with placeholder values.
simshot init --yes # → simshot.yml (commented template)
simshot verify
Checks the tree simshot shoot produced against Apple's screenshot rules; anything App Store Connect would reject fails here first (exit 1), so it works as a CI gate.
simshot shoot --project MyApp.xcodeproj --scheme MyApp \
--bundle-id com.example.myapp --devices iphone-17-pro-max \
--langs ja,en --scenes home,detail --resize
simshot verify appstore --devices iphone-17-pro-max --langs ja,en
simshot verify — appstore
✅ iphone-17-pro-max/ja 3 files 1290×2796 (6.7inch)
❌ iphone-17-pro-max/en 2 files 1290×2796 (6.7inch)
❌ iphone-17-pro-max/en/02_detail.png
image has an alpha channel — App Store Connect rejects these
→ run shoot with `--resize` to write flattened App Store copies
It flags dimensions that aren't an accepted size, an alpha channel, mixed sizes inside one folder, and empty or missing device/language folders. An output tree with nothing to check fails too (no device folders at all, a raw/-only tree from a shoot run without --resize, or a device folder with no language folders), because checking zero files is not the same as every file being publishable. raw/ is skipped, --devices/--langs require those folders to exist, and --json gives the same report for tooling.
simshot shoot reference
BUILD
--project <path.xcodeproj> Xcode project to build (or --workspace)
--workspace <path.xcworkspace> Xcode workspace to build
--scheme <name> Scheme to build
--app-path <path.app> Skip building; use an existing .app bundle
REQUIRED
--bundle-id <id> App bundle identifier
--devices <list> Simulator names or UDIDs (comma-separated)
SHOTS
--shots <config.json> Shot config file ({ "shots": [...] })
--scenes <list> Shortcut: one shot per scene, e.g. home,trace
--wait <secs> Default settle time per shot for --scenes (default: 6)
LOCALIZATION
--langs <list> Languages (default: en), e.g. ja,en
--locales <map> lang=locale overrides, e.g. ja=ja_JP,en=en_US
OUTPUT
--output <dir> Output directory (default: appstore)
--resize Write App Store-ready copies (alpha removed)
--derived-data <dir> DerivedData path (default: ~/.simshot/DerivedData)
SIMULATOR
--timeout <secs> Timeout for external commands (default: 300)
--status-bar-time <t> Status bar clock (default: 9:41)
--status-bar-battery <n> Status bar battery %, 0-100 (default: 100)
--no-ui-testing Do not pass --ui-testing to the app
--no-clean Do not uninstall before installing
--keep-running Do not shut down simulators afterwards
MISC
--verbose, -v Verbose output
--help, -h Show help
Devices
--devices accepts either device names or UDIDs:
simshot shoot ... --devices iphone-17-pro-max,ipad-pro-13
simshot shoot ... --devices 67DF6727-31BC-4246-9FC0-313A22FB2A6C
Names are matched case-insensitively and hyphen/dash-insensitively. When the same name exists on several iOS runtimes, the newest runtime wins. Output subdirectories use the name you passed (or the device slug when using a UDID).
Shot config
shots.json gives full control over names, scenes, and settle times. Every field except name and scene is optional.
{
"shots": [
{ "name": "04_home.png", "scene": "home", "wait": 8 },
{ "name": "01_trace.png", "scene": "trace", "strokes": 1, "wait": 6 },
{ "name": "06_store.png", "scene": "store", "wait": 8 },
{ "name": "07_store_tip.png", "scene": "store", "scrollBottom": true, "wait": 8 }
]
}
| Field | Type | Default | Meaning |
|---|---|---|---|
name | string | — | Output filename, e.g. 04_home.png |
scene | string | — | Scene name passed as --screenshot-scene |
strokes | int | nil | Passed as --screenshot-strokes |
scrollBottom | bool | false | Passes --screenshot-scroll-bottom |
wait | int | 6 | Seconds to wait after launch before capturing |
uiTesting | bool | true | Whether to pass --ui-testing |
The file may be either a bare array of shots or { "shots": [...] }.
Screenshot scene protocol
simshot launches your app with a fixed argument set. Your app owns the scene names and what they do — simshot only passes them through.
| Argument | Meaning |
|---|---|
--ui-testing | Tell the app this is an automated session (skip onboarding etc.). |
--screenshot-scene <scene> | Navigate to the named scene on launch. |
--screenshot-strokes <N> | Perform N interactions (e.g. draw N strokes). Optional. |
--screenshot-scroll-bottom | Scroll the scene to the bottom before capturing. Optional. |
In addition, simshot always passes -AppleLanguages (lang) and -AppleLocale locale so the app renders in the requested language.
App-side implementation (SwiftUI)
import SwiftUI
struct HomeView: View {
@State private var path: [Character] = []
@State private var showSettings = false
var body: some View {
NavigationStack(path: $path) {
content
.onAppear {
#if DEBUG
handleScreenshotScene()
#endif
}
}
.sheet(isPresented: $showSettings) {
SettingsView()
}
}
#if DEBUG
/// Screenshot-capture support: interpret simshot's launch arguments.
private func handleScreenshotScene() {
let args = ProcessInfo.processInfo.arguments
guard let index = args.firstIndex(of: "--screenshot-scene"),
index + 1 < args.count else { return }
let scene = args[index + 1]
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
switch scene {
case "detail":
if let first = characters.first {
path = [first]
}
case "settings":
showSettings = true
default:
break // "home" is the default screen
}
}
}
#endif
}
Because the handler lives behind #if DEBUG, it never ships in a release build.
Resizing
--resize writes App Store-ready copies to <output>/<device>/<lang>/. simshot:
- Matches the raw capture against Apple's published screenshot sizes — exact dimensions first, then aspect ratio within 1%. A capture that is already an accepted size keeps its own resolution instead of being upscaled.
- Flattens any alpha channel onto white.
- Resizes with high-quality interpolation (LANCZOS-equivalent) and writes PNG.
Sizes accepted for current devices, in either orientation (verify also accepts Apple's older sizes, down to the 3.5-inch iPhone):
| Size | Device |
|---|---|
| 1320×2868 | iPhone 16 Pro Max / 17 Pro Max (6.9") |
| 1290×2796 | iPhone 14 Pro Max / 15 Plus / 16 Plus (6.7") |
| 1260×2736 | iPhone Air (6.5") |
| 1284×2778 | iPhone 12 Pro Max / 13 Pro Max |
| 1242×2688 | iPhone 11 Pro Max / XS Max (6.5") |
| 1206×2622 | iPhone 16 Pro / 17 Pro (6.3") |
| 1179×2556 | iPhone 15 / 16 / 17 (6.1") |
| 1170×2532 | iPhone 12 / 13 / 14 |
| 1125×2436 | iPhone X / XS / 11 Pro |
| 1080×2340 | iPhone 12 mini |
| 1242×2208 | iPhone 6 Plus / 8 Plus |
| 750×1334 | iPhone 8 / SE (2nd, 3rd gen) |
| 2064×2752 | iPad Pro 13-inch |
| 2048×2732 | iPad Pro 12.9-inch |
| 1668×2420 | iPad Pro 11-inch (M4, M5) |
| 1668×2388 | iPad Pro 11-inch (2018-2022) |
| 2266×1488 | iPad mini (6th gen, A17 Pro) |
| 1640×2360 | iPad Air 11-inch / iPad (10th gen, A16) |
| 1668×2224 | iPad Pro 10.5-inch / iPad (9th gen) |
| 1536×2048 | iPad 9.7-inch |
| 768×1024 | iPad mini |
A 10.2-inch iPad screen captures at 2160×1620, which App Store Connect does not accept (that display class submits at 1668×2224), so the raw capture is resized to the iPad 13-inch size rather than passed through.
If no target matches (within 1% aspect ratio), simshot warns and keeps the raw capture.
Output layout
appstore/
├── raw/
│ └── iphone-17-pro-max/
│ ├── ja/
│ │ ├── 04_home.png
│ │ └── 01_trace.png
│ └── en/
│ └── 04_home.png
└── iphone-17-pro-max/ # --resize: App Store-ready copies
└── ja/
└── 04_home.png
Reliability
- Timeout: every external command (xcodebuild, simctl, …) is killed if it exceeds
--timeout. A hung simulator can never stall a CI job. - Retries:
launch,install, andscreenshotretry on failure. - Per-device isolation: a failing device is reported and skipped without aborting the rest of the matrix.
- Clean state: the app is uninstalled before install, and
--keep-running/--no-cleanare opt-outs if you want to preserve state.
Development
swift build # build
swift test # run tests
swift format lint --recursive Sources Tests # code style
swift run simshot shoot --help