Automate iOS app builds and App Store uploads with Claude Code and the Xcode CLI
Contents
I saw a post on X saying someone used Claude Code to invoke Xcode CLI and automate everything from building an app to publishing it on the App Store. I thought “surely not”, but it looks like it actually works.
I had already tried automation through an MCP server for Android in another article, and if iOS can be handled the same way, development efficiency should improve a lot.
This article summarizes the overall Xcode CLI flow and the main points when calling it from Claude Code.
Overall flow
| Step | Tool | Example command |
|---|---|---|
| Build | xcodebuild | xcodebuild clean build -scheme MyApp |
| Archive | xcodebuild | xcodebuild archive -archivePath ./MyApp.xcarchive |
| Export IPA | xcodebuild | xcodebuild -exportArchive -exportOptionsPlist options.plist |
| Install on device | xcrun devicectl | xcrun devicectl device install app --device <ID> MyApp.ipa |
| Upload to App Store | xcrun altool | xcrun altool --upload-app -f MyApp.ipa --apiKey <KEY> |
Because everything can be done from the CLI, you can tell Claude Code “build it and upload it to the App Store” and, in theory, it can run the whole flow.
Prerequisites
Some things still need to be set up manually. The CLI alone is not enough.
Required environment
- macOS and Xcode must be installed
- You must be enrolled in the Apple Developer Program (12,980 yen/year)
Certificates and provisioning profiles
Set them up in Xcode under “Signing & Capabilities”, or create them manually in the Apple Developer Portal.
- Distribution Certificate
- App Store Provisioning Profile
If they are not in your keychain, the build may succeed but archiving will fail.
App Store Connect API key
You need an App Store Connect API key for upload automation.
- App Store Connect -> Users and Access -> Keys
- Create a new key under the “App Store Connect API” tab
- Keep the
.p8file, Key ID, and Issuer ID
Build and archive with xcodebuild
Clean the project
xcodebuild clean \
-project MyApp.xcodeproj \
-scheme MyApp \
-configuration Release
Create an archive
xcodebuild archive \
-project MyApp.xcodeproj \
-scheme MyApp \
-configuration Release \
-archivePath ./build/MyApp.xcarchive \
-destination 'generic/platform=iOS'
If you use a workspace (.xcworkspace), change -project to -workspace.
Export the IPA
xcodebuild -exportArchive \
-archivePath ./build/MyApp.xcarchive \
-exportOptionsPlist exportOptions.plist \
-exportPath ./build
Example exportOptions.plist
App Store settings:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
For development use (Ad Hoc), change method to ad-hoc.
Install on a device
iOS 17 and later: xcrun devicectl
From Xcode 15 onward, you can use Apple’s devicectl command.
# List devices
xcrun devicectl list devices
# Install the app
xcrun devicectl device install app \
--device <DEVICE-ID> \
MyApp.ipa
# Launch the app
xcrun devicectl device process launch \
--device <DEVICE-ID> \
com.example.MyApp
iOS 16 and earlier: ios-deploy
Use the third-party ios-deploy tool.
# Install
brew install ios-deploy
# List devices
ios-deploy -c
# Install and launch the app
ios-deploy -b MyApp.app -d
Note: ios-deploy does not work on iOS 17 and later. Use devicectl for iOS 17+.
Upload to the App Store with altool
Store credentials in the keychain
Saving credentials avoids typing them every time.
Recommended: use an App Store Connect API key
xcrun notarytool store-credentials "my-api-key" \
--key /path/to/AuthKey_XXXXXXXXXX.p8 \
--key-id <KEY_ID> \
--issuer <ISSUER_ID>
If you use an app-specific password
xcrun altool --store-password-in-keychain-item "my-app-password" \
-u your@email.com \
-p <APP_SPECIFIC_PASSWORD>
Upload
xcrun altool --upload-app \
-f ./build/MyApp.ipa \
--apiKey <KEY_ID> \
--apiIssuer <ISSUER_ID>
Or:
xcrun altool --upload-app \
-f ./build/MyApp.ipa \
-u your@email.com \
-p @keychain:my-app-password
Validation only
xcrun altool --validate-app \
-f ./build/MyApp.ipa \
--apiKey <KEY_ID> \
--apiIssuer <ISSUER_ID>
Running validation first can catch errors early.
Tips for using it from Claude Code
1. Set environment variables
Putting the API key path and IDs in environment variables makes Claude Code prompts shorter.
# ~/.zshrc
export APPLE_API_KEY_PATH="$HOME/.apple/AuthKey_XXXXXXXX.p8"
export APPLE_API_KEY_ID="XXXXXXXXXX"
export APPLE_API_ISSUER_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
2. Keep exportOptions.plist in the repo
If exportOptions.plist sits in the project root, Claude Code does not have to guess.
3. Script the whole flow
It is annoying to type multiple commands every time, so it helps to wrap them in a shell script.
#!/bin/bash
# scripts/build-and-upload.sh
set -e
xcodebuild clean archive \
-project MyApp.xcodeproj \
-scheme MyApp \
-configuration Release \
-archivePath ./build/MyApp.xcarchive \
-destination 'generic/platform=iOS'
xcodebuild -exportArchive \
-archivePath ./build/MyApp.xcarchive \
-exportOptionsPlist exportOptions.plist \
-exportPath ./build
xcrun altool --upload-app \
-f ./build/MyApp.ipa \
--apiKey $APPLE_API_KEY_ID \
--apiIssuer $APPLE_API_ISSUER_ID
echo "Upload complete!"
Then you can just tell Claude Code to run scripts/build-and-upload.sh.