◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-05-28Intermediate

Claude Code × Xcode Cloud — A One-Week Migration of ci_scripts and TestFlight Auto-Delivery

Notes from migrating a long-running indie iOS CI from Fastlane to Xcode Cloud in one week, with the three ci_scripts/ hook scripts in full, TestFlight automation, and dSYM upload — all paired with Claude Code.

Claude Code264Xcode CloudCI/CD19iOS26TestFlightIndie Dev23

✦ Premium Article

I am Masaki Hirokawa, an artist and creator. Since 2014, I have been running iOS wallpaper and ambient apps on my own. Once cumulative downloads crossed fifty million, the hours available for CI maintenance dropped to nearly zero, and I kept my old Fastlane + GitHub Actions pipeline running with the attitude of "it works, do not touch it."

Earlier this year I moved my main development machine to Apple Silicon, and around the same time the certificate sync inside Fastlane match started misbehaving. TestFlight delivery began to fail about once a week. Half of the cause was my local environment, half was the GitHub Actions macOS runner image refresh cycle. As a one-person operation, being paged at seven in the morning by Slack was not a sustainable shape.

So in early May 2026, just after the long Golden Week holidays, I committed to migrating fully to Xcode Cloud. I gave myself one week. Claude Code was the partner I called from the CLI throughout, the three ci_scripts/ hooks went from draft to production, and TestFlight delivery with Crashlytics dSYM upload was running end-to-end by Friday. This article is the record. If you are an indie iOS developer wondering whether to make the same jump, my hope is that the traps I tripped over save you a couple of evenings.

Why Xcode Cloud, now

I picked Xcode Cloud for three reasons.

First, Apple's signing flow stays inside "automatic signing." With Fastlane match, you keep an encrypted repo of signing certificates and decrypt them inside CI. It is fine when it works, but Apple Developer ID renewals and Provisioning Profile regeneration broke the path a few times a year for me. Xcode Cloud uses the team certificates registered on App Store Connect directly, and that entire layer disappears.

Second, the compute included in Apple Developer Program — twenty-five hours of compute time per month — is realistic for an indie running four apps. My pipeline averages six to ten minutes per build and about twenty builds per week. The same workloads on a GitHub Actions macOS runner took twelve to eighteen minutes per build, so the budget comparison was straightforward.

Third, TestFlight delivery can be triggered directly from an Xcode Cloud workflow definition. Compared to calling pilot from Fastlane, the surface area for failure shrinks. The upload_to_testflight step that occasionally hung simply no longer exists.

The trade-off is real: Xcode Cloud is not a free-form CI. If you want to run arbitrary long-running tasks, GitHub Actions or Bitrise are still better suited. But if your scope is "ship iOS apps reliably from a one-person shop," the footprint is very small and the number of moving parts shrinks meaningfully.

What ci_scripts/ actually is

The hook points in Xcode Cloud live in a ci_scripts/ directory at the root of your Xcode project. Three scripts are picked up automatically:

  • ci_post_clone.sh — right after the repository clones, before dependency resolution
  • ci_pre_xcodebuild.sh — immediately before xcodebuild runs
  • ci_post_xcodebuild.sh — right after xcodebuild finishes, success or failure

All three are plain bash. They need a #!/bin/sh -e shebang and the execute bit set. A non-zero exit halts the build, so the discipline is to never return an unexpected exit code.

The first stumbling block is that there is no good way to test these scripts locally — Xcode Cloud launches from the GUI, and xcrun simctl style interactive testing does not apply. I asked Claude Code to "write ci_post_clone.sh so I can dry-run it locally as CI_WORKSPACE=$(pwd) bash -e ci_scripts/ci_post_clone.sh." It came back with a template that fell back on :- for the environment variables Xcode Cloud injects, so the same script could run both in the cloud and locally.

#!/bin/sh -e
 
# Xcode Cloud sets CI_WORKSPACE to the project root.
# For local dry runs, pass the same variable from the caller.
WORKSPACE="${CI_WORKSPACE:-$(pwd)}"
cd "$WORKSPACE"
 
echo "[ci_post_clone] workspace=$WORKSPACE"

Adopting "can I dry-run this with CI_WORKSPACE=$(pwd) bash -e ci_scripts/...?" as a hard rule at the start made the rest of the week noticeably calmer.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦The three ci_scripts/ hooks — ci_post_clone.sh, ci_pre_xcodebuild.sh, ci_post_xcodebuild.sh — shown in full as the working versions I ended up with, along with the exact prompts I gave Claude Code
✦Three concrete pitfalls when leaving Fastlane match for Xcode Cloud's automatic signing, each shown as a Before/After code comparison so the trap is hard to miss
✦How I integrated Crashlytics dSYM upload safely into TestFlight delivery, and the small Xcode Cloud knobs that cut build time from 14 minutes down to 6
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Claude Code2026-10-12
After Moving to SPM, My App Died the Moment It Launched — Reading One dyld Line with Claude Code
Right after moving from CocoaPods to Swift Package Manager, the build passed but the app died on launch. Here is how I traced one dyld line to its cause, the fix, and how to verify it.
⟐ Claude Code2026-09-04
The One File I Keep Out of Claude Code's Reach: project.pbxproj
With Xcode project files, the breakage that still opens costs far more than the breakage that refuses to open. Here is what I measured before moving every edit behind a script, and where the line sits today.
⟐ Claude Code2026-07-29
The MCP Server Connects Fine and Still Hands Back Zero Tools — Catching Silent Capability Drift with a Tool Manifest Diff
A missing credential does not break the MCP handshake. initialize succeeds, tools/list succeeds, and the array comes back empty. Here is the measured behaviour, and a preflight that locks the expected tool surface and fails closed before the agent ever starts.