To anyone who's chased a crash that only reproduces on a physical device knows the fix was never more log statements added by ai but rather a breakpoint, the debugger, and thirty unglamorous minutes of stepping through code line-by-line (terminal lldb technically counts, but barely).
Having spent many years fighting with Xcode professionally, I was thrilled when swift build came out, and then appalled at how badly Xcode adopted it. Eventually I realized that you can set up your entire app as a swift package, and then the Xcode project with a single main source file that calls some `app_main()` (or whatever you want to call it) function in your package.
I cannot remember exactly but there was something annoying about setting up a new project; I think by default Xcode assumes that you want a swift package to be treated as a dependency that is checked out from git, but there was a way to drag and drop a local dependency in the same project directory and then it worked.
What this buys you is that files are no longer tracked by Xcode, so renames/merges no longer trigger project.pbxproj surgery. Instead you specify the swift package directories and are done.
I used this for some large personal projects and was very happy with it.
At one point I also wrote my own build tool that imitated all of the steps that Xcode took so that I could truly be Xcode-free. That also worked (and was not even that hard) but I went back to Xcode because the debugger UI was better than what I could get out of VS Code at the time, and especially because I was playing around with the Metal debugger.
Using godot iOS export.
For regular old native ios dev replacing Xcode i don’t think i would do it.
Interestingly, since about Opus 4.6, Claude has been able to reason its way into this process on its own. It was clunky until 4.7, and in 4.8 it's managed to find its way around every reason I had to open xcode myself.
Claude was great at figuring out what was broken when and either fixing it, or clicking as far as it could until it needed me.
You could say I'm mostly just IRL hands for the AI now.
https://github.com/software-mansion/argent
or
https://github.com/callstack/agent-device
both callstack and swmansion are mostly react native shops but those should work even in native ios/android as well
React Native and Flutter seem to be much more predictable for the bots (and more fun for humans, since they have actual hot reload).
Areas they could improve - what ever the heck is going on in their diffing ui for source control.
honestly would not even know what to do / click in xcode
The fact that xAI uploaded someone's home directory, including their SSH keys, is giving me serious pause at my choices here.
Generally, I don't worry about my machine being "blown up". I don't have a TON of unreproducible stuff on my machine. Everything is backed up, committed to git, and the like. I can restore most of it in a couple hours.
That said, I really, really don't want my .SSH directory sent to an AI agent and it's silly to prompt your way around that. You need to block it at the system level. I'm considering a separate user and then 700 permissions on my home directory.
I feel like we're back to 1990's security here. The double-edged sword is that it's helping us get things done at a pace like never before.
I'm not throwing shade here, I'm among the guilty.
Surprisingly, it's very easy. This works like a charm: https://github.com/xtool-org/xtool
You do not need to upload to TestFlight or the App Store; you can just install the app locally to your iPhone via usb -- even from Linux!
When in doubt, just ask your coding agent of choice to help you create and upload a Hello World iOS app. It's really easy.
In addition to a deep roster of skills and agencts, Axiom includes several for-LLM tools². xclog, xcprof, xcsym, and xcui are designed to be used by LLMs, and expose capabilities in a token-efficient way. These tools are equally helpful for non-Axiom skills/agents.
¹ Axiom: https://charleswiltgen.github.io/Axiom/ ² Axiom CLI tools: https://charleswiltgen.github.io/Axiom/tools/
> I had Claude Code create mine: I told Claude, more or less: I want to archive, Developer ID-sign, notarize, staple, and install this app to /Applications without ever opening Xcode. Write me a script that does the whole chain and fails loudly if any step breaks.
Even though the text we're reading is Claude talking to us as well :)
Also it was weird to see the mention of "ask your LLM" at almost every stage in the blog post:
> point Claude Code or your LLM coding tool of choice to this blog post, and let it figure it out
> When in doubt, ask your LLM of choice about them and have it help you get set up. It’s the one that’s going to be using Xcode for you anyway.
> The whole point of using the LLM in the first place is to avoid doing things manually that you don’t want to do.
> Again, if in doubt, ask Claude Code or your LLM of choice to create this for you.
> Again, this is why you talk to your LLM, tell it what you want, and have it help build your workflow.
Xcode is a [buggy as hell] GUI wrapper for a lot of system-level UNIX utilities and apps [which are generally, not so buggy].
Using CLI to release apps is a pretty old practice; at least as long as I've been doing it (I released my first Xcode app in 2012).
I should blog about more this, but I also went to some effort to add support free iOS provisioning with just an Apple ID (using internal APIs); and creating a nice DMGs for macOS app distribution (reverse engineered .DS_store files for this). And there's also a built-in command to install skills for coding agents, which was fun.
This [1] is an example app I built with it, a simple utility to manage macOS file extension handling.
Xcode MCP is not perfect, by any stretch (stupidly, it issues a permission prompt on every single agent launch—something I defeated with a Keyboard Maestro hack to auto-accept it), but Xcode 27 is such a huge leap forward in the ergonomics of vibe coding with Xcode that I've actually stopped dealing with the MCP and (after an afternoon of futzing) have started using the app's own agent UI for driving Codex because that harness better steers the model to take advantage of the tools available.
All of this redounds to two things: faster feedback and more robust verification. The two things that matter most.
As for the builds, your agent probably already knows how to do a lot of this from the command line, although explicit suggestions can help it build faster for different situations.
As for XcodeGen, you may find it unnecessary overhead if you're already using Xcode file system synced groups.
For iOS my biggest suggestion would be to enable App Store Connect skills for your agent (https://github.com/rorkai/App-Store-Connect-CLI).
With this not only do you not need Xcode all the time, you also don't even need to be near a MacBook.
Just make changes via Codex on your iPhone, the tell asc to build and upload to TestFlight, download and run the new version, iterate.
Its basis is React, so the code output quality is much higher than Swift because there is much more React code in LLM training data.
Everything is in the command line, and debugging is a breeze because it's a web view. But once it's compiled to native iOS, it feels like any other native app.
Expo + Fastlane = fully automated iOS submission and deployment. I issue one command and see a new version in the App Store.
Ignite is a great starter using RN and Expo. Some great people maintaining it. https://github.com/infinitered/ignite
I did end up somehow installing Xcode via some shady download and was on my way. But the whole ordeal left a very sour taste.
“From bundle install to your phone in minutes. To the App Store and Google Play without a line of native code.“
That said, it might be well enough for simpler apps.
The most useful one is a little weather sparkline to show local temperature forecasts. Useful every day.
In terms of risk, I see it as halfway between stock claude with the sandbox and full-blown container or machine isolation.
I was recently thinking that as Claude's own sandbox gets better I'm doubting the ROI on my belt-and-suspenders project, but your comment reminds me why I'm doing it.
It is not currently published open source but I'm happy to talk about it with strangers.
You can develop for Linux and BSD and Haiku and all sorts of other platforms for free. If the cost is what matters to you, then just do that. If you’re doing it as a business, though, consider that you’re extremely lucky to be able to just buy a low-end device and a low-end system and that’s all the capital investment needed (plus the US$100/year membership as recurring opex).
You can also use native Mac VMs.
Many people have created Mac VM projects to do exactly this, I was working on one but was stalled too often because before I started using claude I bought a new laptop with what I KNEW was enough disk space.
The 100 GB or so I need to comfortably do the VM stuff just isn't available on my mac.
npx expo run:ios --device #runs a build on my iphone and shows logs in the terminal
eas build --platform ios --profile preview #builds in the cloud and gives me a QR code to install a build on my device with a custom certificate
eas build --platform ios --profile production --auto-submit #auto increments and submits a production build to the app store.
* A Markdown viewer (obviously pretty simple)
* A menu bar Apple TV remote (surprisingly complicated)
* A menu bar and desktop temperature tracker for the silly Tapo temperature trackers I have all over my house (a little complicated because multiple user interfaces, a backing database, scheduling, and multiple local/remote protocol interfaces)
* A graphical CAS calculator frontend to Sage Math with LaTeX math rendering
* An LLMwiki implementation that uses macOS filesystem extensions to reflect a SQLite database into a local filesystem for agents to traverse (basically the new macOS way to do FUSE, fussy enough that the app has to run out of /Applications to work)
* An agent-driven Music.app replacement with AirPlay streaming support managing Apple Music catalogs and tracks
All of these have worked without me ever once touching the xcode UI, except one time to generate Apple Developer certificates, which I then drove exclusively from CLI tools.
These are all pretty modern SwiftUI applications.
You don't have to! All recent Macs come with nearly zero perf cost virtualization. You can easily run Mac or Linux VMs assuming same architecture. Use it all the time for development and whatever.
Use Tart [1] or VirtualBuddy [2], both open-source, for a packaged solution. Or in the spirit of this post, vibe code your own wrapper around the OS API [3]
More recently, there are also Apple containers [4]
[2] https://github.com/insidegui/VirtualBuddy
[3] https://developer.apple.com/documentation/virtualization
Our code broke
It looks like there was an error we need to look at. Sorry about the inconvenience.At first, I just used simulator builds to test (and debug) on the macOS VM, in the simulator, but that was slow and painful. Finally, I bought a used iPhone, all bruised, just so I could test it on a real device. The main issue for me was signing, one cannot register a macOS VM (maybe with some kext hacks?), I managed to enable developer mode on the phone (crazy process, but it worked with Xcode in a VM), I registered the phone on the website, and paid those damn 99$.
I used that for my cross-platform UI library with native controls, based on IUP. Now, future users of the UI toolkit do not need to care about Mac, iOS, or tooling; it just works. You can check the toolkit (WIP) here https://github.com/gen2brain/iup-go .
My only familiarity with it is because it's needed by brew. I honestly never looked into exactly what is in the package, but I assumed things like what is installed with -devel packages of yum/apt-get/dnf/etc. Lots of repos have common list of things to install like gcc/make/etc. Again, just guessing, but it's one of the first things I've always run on a new Mac to get it usable for CLI usage.
Forcing people onto Macs is such a pathetic way of extracting rent from developers and makes workflows worse. No you can't just build and debug your app from your Windows or Linux workstation because we want you to buy hardware you don't need. Sorry we only support Xcode on the latest version of MacOS, how else will your build machine roll out of the OS support window and make you replace a functioning machine?
It's especially nice when you work on projects that aren't iOS/Apple native and you don't get Mac workstations. You end up with this wonderful mess of shared machines and half assed tools because you just need the bare minimum to push a build to a phone.
Linux VMs are not relevant here. For downloading Xcode and building apps, I believe one needs to sign in with their Apple account.
Does this work well with a macOS VM?
> vibe code your own wrapper around the OS API
Maybe stick with one of the existing projects? Throwing out all of the edge cases and hardening that went into a security-related project just to burn a bunch of resources to generate a worse one you have to maintain yourself is 90s-level security with 2026-level inefficiency, IMO.
I love how people say this like Apple finally created a native solution...
They didn't. It's just a linux vm running normal docker engine.
Kind of fun, you can develop iOS and Android both without a build step and without a Mac even.
You can have something like Claude's auto approvals. You can poke specific holes in the sandbox (E.G. my Codex one can write to ~/go, ~/.cache and ~/.cargo). You can have explicit deny rules that hard-deny and bypass the sandbox and auto-approval. You can allow certain commands to bypass sandbox execution entirely.
While those aren't secure in theory (if you let a third party type prompts into the agent, they will likely be able to exfiltrate your data), they're secure against agent carelessness and AI stupidity in practice.
If I had to guess, I would say this is the human summarised conversation(s) with a bot.
Hope you like long argument lists. xcodebuild has a crazy long parameter list.
https://developer.apple.com/documentation/xcode/xcode-comman...
> It's especially nice when you work on projects that aren't iOS/Apple native and you don't get Mac workstations.
If you develop natively for macOS, then you probably need the OS for it. It's the same for Linux and Windows. But building is not the biggest hurdle because you can build in your usage-based-billed/free CI as well the same way you do for other platforms.
For complete native development (system APIs, OS interactions, native UI) against macOS/iOS on PC, Apple needs to open those OSes for PCs/Pixels/Samsungs/etc. This is a software issue, not hardware. Otherwise Apple silicon Macs are less restrictive than PC hardware in general!
If you can produce a properly structured submission to the App Store ingestion pipeline without using a Mac, then I doubt Apple will care. But that doesn’t mean they’re obligated to help you, and especially not to spend millions of dollars to do so.
This is the perfect use case for a skill: one person takes the hit to create the skill and then anyone else who wants the tool can use the skill.
I'm not getting that vibe from this one; I'd bet money on it at least being substantially human edited. What are the tells you're seeing?
I am getting a little tired of every single HN comment being about how the linked article is written by an LLM.
So you can develop ios apps, run it on a real iphone that is activated without going through app store as a native app? But, you have to reinstall it every week?
Most of the problems that I saw people have with Xcode over the two decades that I worked on it were the result of one fundamental problem: They wanted to work a different way than Xcode is designed to work, and jumped through a ton of hoops to do so, and then blamed Xcode for not accommodating their intentional working at cross-purposes.
A good example: An app that considered itself “large” because it had a thousand tiny static library targets each with only one to three or so ObjC classes. Far smaller than Xcode itself, but because they insisted on working that way—for no real articulable benefit besides “that’s how we want to work”—their project took hours to build from scratch instead of minutes like much larger projects that have many fewer targets.
So, the actions that I really don't want the agent to take (establishing an SSH connection, pushing to a git remote) always require my manual intervention.
[1] https://gist.github.com/arianvp/5f59f1783e3eaf1a2d4cd8e952bb...
OCI compatible, but not docker
https://github.com/apple/container/blob/main/docs/technical-...
Actually makes use of Virtualization.framework mentioned above.
I have nothing against the hardware. I despise how artificial the problem is. It's not even like Apple just refuse to provide tools and leave it up to the community. They force you to use a Mac in the terms of service.
For context I work on the mobile team for a large AAA game engine. This is going to colour my opinion because we're very Windows centric, but my experience wouldn't exist if it weren't for Apple. I need to sync code on two different machines and build half the project on Windows and the other half on the Mac. If we could just build and debug the iPhone on Windows none of this mess would be needed.
We don't develop for MacOS, we develop for iOS. Just debugging the app requires several hoops across multiple machines because of Apple's policy here. Just because a base spec Mac Mini is cheap doesn't make this whole problem any less stupid. I don't need a vendor's special-sauce computer to build for consoles. Cross compilation and remote debugging has existed for decades, this problem should not exist.
Calling it more affordable because Mac Minis aren't junk anymore is really just accepting Apple's ridiculous requirement because "I guess it could be worse right?".
Apple could've chosen to offer no tools and leave it to the community to build their own. I think that's reasonable. I don't expect Microsoft to bring MSVC to Linux.
But Apple take the explicit position that you must purchase a Mac in order to ship apps for iOS.
See docs here: https://developer.apple.com/documentation/virtualization/vir...
And projects like Tart, VirtualBuddy, etc. provide convenient wrappers for these APIs.
LLMs will often use CLI tools. In my experience, they loves their CLI.
This is ironic. Of course you do. You're already using it. You just happen to like the Microsoft's walled garden and not Apples. That's fine, but let's not pretend that "Windows centric" is anything but another monopolistic and exclusionary ecosystem. If you want to use Nvidia drivers, or develop against DirectX, can you do it on a macOS? I get the criticism of Apple's walled garden and I'm against it too, but not from your position.
Or better yet it needs dead-end privileges with a system and data that doesn't matter.
In my experience, it’s quite “crashy.” It often locks up so bad, that I need to force-quit (or force-restart the Mac). I end up doing that once a day, or so. I suspect that many of the performance and stability issues, have to do with resource usage. It’s a damn big app.
It can also end up in strange states, where portions of the UI fall “out of sync,” and the app needs to be restarted. Some operations (like opening a storyboard, or stopping at a breakpoint) introduce massive delays.
It’s not a denigration, calling something a UI shell. I’m working on one, right now. The engine and backend were done in April (started in February), and all the work since, was on the UI shell. I probably could have shipped something that “ticked all the boxes,” in May. It’s almost done, but there’s still a couple of months of testing, ahead.
It’s also a hell of an indictment of this community. FreeBSD is a community project. Qubes is a community project. SELinux is a community project. And people wonder why developers don’t take vibe coders seriously.
Akamai akr should be able to do something similar, but would store the private key on your phone. It's the successor of krypton, which was bought by Akamai, and I liked that even more as you really need a second device.
I'm treating it like...you can hold me accountable for that commit and the commit came from my computer. That feels like the right spirit of things.
How do you do commit signing? I've never worked anywhere it was common, nor worked on a team where anyone else signed commits. I don't know how it's really used in the wild, so to speak.
Commit signing primarily exists because linux kernel development happens via email, which isn't a secure channel.
In that context, signing is used to prove provenance; a commit signed by someone with merge rights gets included, and if you repeatedly sign bad code you'll have your right to merge revoked.
> you can hold me accountable for that commit
I wouldn't personally want to be held accountable for a commit I'd never so much as read, so I would never sign (even locally) a commit until I'd looked at it.
The problem is that nowadays, people roll their own solution, don’t take the time to learn (somewhat understandably, given the complexity of today’s stuff. You simply cannot learn all details about your hardware, OS, and third-party libraries anymore) but immediately deploy things in production.
Eating your own dog food can be good, but for security related stuff, it can easily be disastrous.
> I wouldn't personally want to be held accountable for a commit I'd never so much as read, so I would never sign (even locally) a commit until I'd looked at it.
I think that's reasonable. I'd say that if I push the commit, then I should be accountable for it whether I read it or not (and no one is going to see it unless I push it).
https://developer.apple.com/documentation/virtualization/run...
If the below sounds too complex to work with, can review more basics first. Can start with a Swift tutorial
virtualMachine.start(completionHandler: { (result) in
if case let .failure(error) = result {
fatalError("Virtual machine failed to start with \(error)")
}
})Not to decent mistakes or amateur brain surgery, but there has to be some level of risk tolerance among tinkerers.
Lately, I’ve heard several Apple related podcasters talk about how bad Xcode is, and how Apple needs to make vibe-coding Mac and iOS apps better by making Xcode less inscrutable. They’re not wrong, but also I don’t understand why they’re even opening Xcode in the first place. With a little bit of pre-work, you can vibe code Mac and iOS apps to your heart’s content without looking at Xcode anymore.
And if you’re ever in doubt about how to make any of the following work, point Claude Code or your LLM coding tool of choice to this blog post, and let it figure it out. That’s literally its job, figuring out things you don’t want to have to.
xcodebuild, notarytool, stapler, and devicectl all live inside Xcode and run fine from a shell.scripts/release.sh — which you write once. It runs the whole chain: archive → Developer ID sign → notarize → staple → install to /Applications.xcodebuild finds it automatically. No secrets in the repo.The one-time setup is the only part with any friction, so let’s get it out of the way first.
You do have to have Xcode installed, there’s no getting around that, because build depends on tools that live inside Xcode.app.
Once Xcode is installed, make sure it’s the selected command line toolchain, and not /Library/Developer/CommandLineTools. If the output of the check is /Applications/Xcode.app/Contents/Developer, you’re in good shape:
❯ xcode-select -p/Applications/Xcode.app/Contents/Developer
If it DOES return the path for the standalone CommandLineTools instead, point it to Xcode.
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
NOTE: The name “Command Line Tools” can be confusing.
This is because there’s a standalone Command Line Tools package, available with xcode-select --install, which is the /Library/Developer/CommandLineTools version. This contains clang and git, but not the iOS SDK, notarytool, devicectl, and other items needed for full app development.
The complete toolchain is inside Xcode.app, at /Applications/Xcode.app/Developer, and it has everything you need. If you have Xcode installed, you don’t need the standalone Command Line Tools.
Xcode and its command line tools aren’t enough to generate and manage Xcode projects automatically. For that, you’re going to need XcodeGen. You can download it from Github or install it using homebrew:
brew install xcodegen
Long story short, Xcode projects are actually folders that macOS makes appear as files, and they contain everything about your project needed to create and compile your app. Xcode constantly modifies the files and file references constantly, and it creates issues for git repositories.
Xcodegen creates a project.yml (YAML) file with all your project settings, and then on every build, it recreates the entire .xcodeproj folder using that project.yml file. Only the YAML file has to be committed to git, and the whole .xcodeproj can be ignored from git’s perspective.
You do need to setup Xcode initially in order to never have to look at it again.
First, either accept its license and install its additional components, or do it through the command line:
sudo xcodebuild -license acceptsudo xcodebuild -runFirstLaunch
Next, open Xcode, click on Settings → Accounts and click on + to add your account.
NOTE: You have to have a paid Apple Developer account in order to distribute and notarize your apps.
And you will want them notarized in order to install them on you Mac and iOS devices and not have the OSes decided they’re malware and delete them.
Once that’s done, create a Developer ID Application certificate (Settings → Accounts → your Apple ID → Manage Certificates… → + → Developer ID Application), which creates a cert for signing the shipped .app bundle.
Please note that a Developer ID Application certificate and your Apple Development certificate are two separate things.
The Apple Development identity is for building and running on your own devices — pushing to your iPhone, local debugging. The Developer ID Application identity is for the notarized .app that survives Gatekeeper and runs on someone else’s Mac. The release script wants that second one.
Creating the certificate in Xcode installs both the certificate and its private key into your login keychain. That private key is what actually does the signing, and it cannot be re-downloaded — so don’t delete it, and back up your keychain.
When in doubt, ask your LLM of choice about them and have it help you get set up. It’s the one that’s going to be using Xcode for you anyway.
And finally,
Notarization uploads your signed app to Apple for a malware scan. notarytool authenticates using a stored keychain profile that you create once, interactively — it prompts for an app-specific password, and there’s no way around the prompt:
xcrun notarytool store-credentials App-Name \ --apple-id "[email protected]" --team-id YOUR-TEAM-ID# paste an app-specific password when prompted
A few things worth knowing here:
401 invalid credentials out of notarization almost always means “go make a fresh app-specific password,” not “your setup is broken.”Confirm it’s stored:
xcrun notarytool history --keychain-profile App-Name
Side topic here, I store my app-specific passwords in a 1Password vault that Claude Code has access to. That way whenever I’m creating a new app, I can tell IT to create the notarization credential for me, and it knows to check its 1Password vault for the password. The whole point of using the LLM in the first place is to avoid doing things manually that you don’t want to do.
Real signing needs your team ID and bundle prefix, and I put those in a Local.xconfig file:
cp Local.xcconfig.example Local.xcconfig# then edit Local.xcconfig to set:# BUNDLE_PREFIX = your.real.prefix# DEVELOPMENT_TEAM = YOUR-TEAM-ID
Again, if in doubt, ask Claude Code or your LLM of choice to create this for you.
Deployment on my apps is handled via a script called release.sh that lives in a scripts folder inside the repo. Without it, I don’t have an automated build pipeline.
I had Claude Code create mine: I told Claude, more or less: I want to archive, Developer ID-sign, notarize, staple, and install this app to /Applications without ever opening Xcode. Write me a script that does the whole chain and fails loudly if any step breaks.
It didn’t need me to explain the pipeline, because the pipeline isn’t a secret — archive with xcodebuild, export with -exportArchive and an ExportOptions.plist, submit with notarytool --wait, attach the ticket with stapler, check with spctl. That’s the documented, conventional way to ship a Developer ID Mac app, and the model knows it. What it needed from me was the project-specific stuff: the scheme name, the team ID, what to call the notary profile, where to install the result.
Then it wrote a first draft, we ran it, it broke, and we fixed it. That loop is not a failure mode, it’s just the process. I always look at AI workflows as works in progress, but it doesn’t take long before you can stop tweaking things and just start working.
This is the actual script from one of my app repos:
#!/usr/bin/env bash# scripts/release.sh — produce a Developer ID-signed, notarized MY-APP-NAME.app and# install it to /Applications.## Requires (one-time): Xcode signed into your Apple ID, the paid Developer# Program, and a notarytool credential profile. Defaults to the "MY-APP-NAME"# profile; override with MY-APP-NAME_NOTARY_PROFILE=<name>.## Usage: ./scripts/release.shset -euo pipefailPROJECT="MY-APP-NAME.xcodeproj"SCHEME="MY-APP-NAME-macOS"APP_NAME="MY-APP-NAME"TEAM_ID="YOURTEAMID"NOTARY_PROFILE="${MY-APP-NAME_NOTARY_PROFILE:-MY-APP-NAME}"BUILD_DIR="build"ARCHIVE_PATH="$BUILD_DIR/$APP_NAME.xcarchive"EXPORT_PATH="$BUILD_DIR/Export"APP_PATH="$EXPORT_PATH/$APP_NAME.app"INSTALL_DIR="/Applications"LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"cd "$(dirname "$0")/.."step() { printf "\n\033[1;36m▸ %s\033[0m\n" "$*"; }fail() { printf "\n\033[1;31m✗ %s\033[0m\n" "$*" >&2; exit 1; }step "Pre-flight"command -v xcodegen >/dev/null || fail "xcodegen not installed (brew install xcodegen)."if ! xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null 2>&1; then fail "notarytool profile '$NOTARY_PROFILE' missing. Create it with xcrun notarytool store-credentials."fistep "Regenerating project"xcodegen generaterm -rf "$BUILD_DIR"; mkdir -p "$BUILD_DIR"step "Archiving (Release)"xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \ -derivedDataPath "$BUILD_DIR/derived" -archivePath "$ARCHIVE_PATH" \ -allowProvisioningUpdates archivestep "Exporting Developer ID-signed app"cat > "$BUILD_DIR/ExportOptions.plist" <<EOF<?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>developer-id</string> <key>teamID</key><string>$TEAM_ID</string> <key>signingStyle</key><string>automatic</string></dict></plist>EOFxcodebuild -exportArchive -archivePath "$ARCHIVE_PATH" -exportPath "$EXPORT_PATH" \ -exportOptionsPlist "$BUILD_DIR/ExportOptions.plist" -allowProvisioningUpdates[ -d "$APP_PATH" ] || fail "Exported app not found at $APP_PATH."step "Notarizing (submitting to Apple, may take a few minutes)"ditto -c -k --keepParent "$APP_PATH" "$BUILD_DIR/notarize.zip"xcrun notarytool submit "$BUILD_DIR/notarize.zip" --keychain-profile "$NOTARY_PROFILE" --waitstep "Stapling ticket"xcrun stapler staple "$APP_PATH"step "Verifying Gatekeeper acceptance"spctl -a -vvv -t exec "$APP_PATH"step "Installing to $INSTALL_DIR/$APP_NAME.app"pkill -x "$APP_NAME" 2>/dev/null || truerm -rf "$INSTALL_DIR/$APP_NAME.app"cp -R "$APP_PATH" "$INSTALL_DIR/"[ -x "$LSREGISTER" ] && "$LSREGISTER" -f "$INSTALL_DIR/$APP_NAME.app" >/dev/null 2>&1 || truestep "Verifying installed bundle"xcrun stapler validate "$INSTALL_DIR/$APP_NAME.app"spctl -a -vvv -t exec "$INSTALL_DIR/$APP_NAME.app"printf "\n\033[1;32m✓ %s notarized, stapled, installed.\033[0m\n" "$APP_NAME"
It looks more complicated than it is, but it is a series of steps that you’d have to know need performed. Again, this is why you talk to your LLM, tell it what you want, and have it help build your workflow.
Some things to note:
set -euo pipefail halts the script on any failing command immediately instead of blundering forward. There’s no half-finished state that looks like success.
cd "$(dirname "$0")/.." means the script hops to the repo root regardless of where you invoked it from, so ./scripts/release.sh works whether you’re in the repo root or three directories down.
The pre-flight block checks that xcodegen exists and that the notary profile is actually stored before spending five minutes on an archive that’s doomed to fail at step five.
And the last two steps re-verify the installed bundle, not just the exported one. Belt and suspenders, but I’ve had a copy step silently mangle a bundle before and I’d rather find out from the script than from Gatekeeper three days later when it deletes my app for me.
release.sh gives you a one-command deploy. CLAUDE.md (or AGENTS.md for basically every other model under the sun) is what makes the agent actually use it without being told every single time.
I had Claude create my CLAUDE.md itself after going back and forth about the build process. Now whenever I create a new app, I tell it to reference the repo for one of my other apps and use the same methodology.
## Build commands```bash# Regenerate the Xcode project after changing project.yml or adding source filesxcodegen generate# Unit tests (YOUR-APP-NAMEKit only; fast, no Xcode build required)swift test# macOS appxcodebuild -project YOUR-APP-NAME.xcodeproj -scheme YOUR-APP-NAME-macOS \ -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build```## Release (Developer ID + notarization)```bash./scripts/release.sh # archive → Developer ID export → notarize → staple → install```The `xcodebuild` commands above use `CODE_SIGNING_ALLOWED=NO`, which produces an**ad-hoc** build: fine for CI and quick local checks, but Gatekeeper rejects itand the iCloud KVS / App Group entitlements don't bind (no team prefix). For areal menu-bar build that survives quarantine and lets iCloud sync work, use`scripts/release.sh`. It signs with Developer ID, notarizes via the `YOUR-APP-NAME`notarytool keychain profile, staples the ticket, and installs to`/Applications/YOUR-APP-NAME.app`.
And that’s the whole one-time setup. From here on, nothing needs a mouse.
Everything below is plain command-line invocation. Xcode.app never launches; these tools live inside it but run standalone. This is exactly what Claude Code executes through its shell.
For “does it compile / do the tests pass,” you don’t need signing at all:
# Unit tests — pure SPM, no Xcode build at allswift test# Compile the macOS app (ad-hoc, unsigned — fine for CI/local sanity)xcodebuild -project TZed.xcodeproj -scheme TZed-macOS \ -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build# Compile the iOS app + widget extension for the simulatorxcodebuild -project TZed.xcodeproj -scheme TZed-iOS \ -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
CODE_SIGNING_ALLOWED=NO gives you an ad-hoc build: it compiles and runs in a simulator, but Gatekeeper rejects it and entitlements like iCloud KVS and App Group don’t bind. That’s the fast inner loop.
One command does the entire shippable chain — the script from Part two:
./scripts/release.sh
Archive, Developer ID export, notarize, staple, verify, install. If any step fails it stops and tells you which one broke. Need a different notary profile? Override it: TZED_NOTARY_PROFILE=<name> ./scripts/release.sh.
iOS has no notarization step — that’s a Mac-distribution concept. Getting a build onto a connected iPhone is xcodebuild plus devicectl, both inside Xcode’s toolchain:
# Build & sign for a real device (uses the Apple Development cert + provisioning)xcodebuild -project TZed.xcodeproj -scheme TZed-iOS \ -destination 'generic/platform=iOS' \ -allowProvisioningUpdates \ -derivedDataPath build/ios archive -archivePath build/TZed-iOS.xcarchive# Install the built .app onto the connected device by its UDIDxcrun devicectl device install app \ --device <DEVICE-UDID> build/ios/…/TZed.app
devicectl list devices lists connected and paired devices with their UDIDs. Device builds sign with the Apple Development identity (not Developer ID) plus a development provisioning profile, which -allowProvisioningUpdates fetches for you.
If you’ve only ever signed apps by ticking a box in Xcode, it’s worth understanding what’s actually going on under there, because none of it needs the GUI at build time.
The private key does the signing. When you created that Developer ID Application certificate, Apple issued a certificate and your Mac generated a matching private key, both landing in your login keychain. codesign (which xcodebuild calls) uses the private key to sign the binary; the certificate — which chains up to Apple’s root — gets embedded so anyone can verify it.
Automatic signing picks the identity for you. The release script uses signingStyle: automatic, so xcodebuild selects the right identity by team ID and pulls any needed provisioning profile from Apple on the fly. No profiles checked into the repo.
Entitlements bind at sign time. Each target has a .entitlements file (sandbox, network client, iCloud KVS, App Group). These only take effect when the app is signed with a real team identity, which is the other reason ad-hoc builds can’t ship: the iCloud and App-Group entitlements quietly don’t bind without the team prefix, and you get to spend an hour wondering why your key-value store is empty.
Notarization isn’t signing. Signing proves who built the app. Notarization is a separate step where Apple scans the signed app for malware and issues a ticket; stapling attaches that ticket so Gatekeeper trusts the app offline. For a hidden-UI menu bar app (LSUIElement), notarization is what keeps XProtect from flagging it.
The secrets never touch git. The signing private key lives in the login keychain. The notarization app-specific password lives in the notarytool keychain profile. Neither ever gets written into the repo.
You can verify any signed build by hand:
codesign -dv --verbose=4 /Applications/TZed.app # who signed it, with what certspctl -a -vvv -t exec /Applications/TZed.app # would Gatekeeper allow it?stapler validate /Applications/TZed.app # is the notarization ticket attached?
There’s no magic here. Claude Code drives all of this through a plain, non-interactive shell — there’s no special “build” MCP server or plugin doing something clever. It’s xcodebuild, xcrun notarytool, xcrun stapler, spctl, codesign, devicectl, xcodegen, and swift. Standard CLI tools, the same ones we’d use ourselves.
The glue is the CLAUDE.md from Part two. It tells the agent the notary-profile convention, the two build paths, and that shipping means release.sh. The result is Claude just runs the thing, without me re-explaining it every session.
The one step that stays interactive is notarytool store-credentials, and that’s a choice rather than a limitation: you could pass --password and script it, but that means putting an app-specific password in your shell history. Type it once by hand, let the keychain or 1Password hold it, and everything downstream is automated.
Put the GUI workflow next to the headless one and the whole thing just lines up:
| JOB | GUI WAY | HEADLESS WAY |
|---|---|---|
| Generate project | Xcode manages .xcodeproj |
xcodegen generate from project.yml |
| Build | ⌘B / Run button | xcodebuild … build |
| Archive | Product → Archive | xcodebuild … archive |
| Export signed app | Organizer → Distribute | xcodebuild -exportArchive |
| Notarize | Organizer upload | xcrun notarytool submit --wait |
| Staple | (automatic in Organizer) | xcrun stapler staple |
| Install to /Applications | drag-and-drop | cp -R + lsregister |
| Deploy to iPhone | Run on device | xcodebuild archive + devicectl device install |
The GUI is only ever needed for that one-time credential setup. After that, the entire lifecycle is scriptable, which is exactly what release.sh finalizes, and exactly why the agent can own it end to end while I go do something more interesting than watching a progress bar.
If you want to try it yourself, the order is: install Xcode and xcodegen, do the credential setup, then sit down with Claude and build your own release.sh and CLAUDE.md. That last part is the actual work, and it’s an hour or two at most. After that, “ship a new build” becomes one sentence. After the first app setup, Claude Code can copy the same setup to apply to future apps.