apple-dev
Skill0 starsChanged 41 days agoMITView on GitHub
Build, run, test, verify and install iOS and macOS apps as an agent - the just verbs of the Apple templates, the verification ladder (unit tests, SwiftUI preview render, simulator drive with taps and the accessibility tree, physical-device install, macOS app on screen), Apple's Xcode MCP call order, raw xcodebuild/simctl/devicectl fallbacks, and the gotchas. Use for ANY task touching an Xcode project, a simulator, a physical iPhone or Mac app build, SwiftUI previews, .xcresult bundles, or the Xcode MCP server.
In this repository34 other files it gives its agents.
- afkSkill
- apple-contactsSkill
- apple-notesSkill
- apple-photosSkill
- bug-huntSkill
- catch-upSkill
- cherriSkill
- chrome-controlSkill
- claude-sessionsSkill
- coordinatorSkill
- discountsSkill
- forge-skillSkill
- groupmeSkill
- herdr-crewSkill
- iconsSkill
- imsgSkill
- language-selectionSkill
- laterSkill
- mac-controlSkill
- nix-mechanicsSkill
- notion-cliSkill
- posthogSkill
- quotaSkill
- redditSkill
- rustSkill
- scoutSkill
- semverSkill
- social-contentSkill
- spotifySkill
- travelSkill
- validate-ideaSkill
- verdictSkill
- web-reconSkill
- whatsappSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
13 rules. About 0 are common, 10 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈10
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 17
Every Apple template (ios-app, appstore-app, macos-app) ships the same just interface.
- Very specific: line 57
Before a long local build: at least 15 GB free, one build at a time on a shared host (its build lock), COMPILATION_CACHE_ENABLE_CACHING=NO, …
- Distinctive: line 60
Ask the owner whether iteration is done.
- Distinctive: line 64
Use CI earlier only when the build host is short on disk or overloaded or the signing material cannot be reached.
- Distinctive: line 95
No persistent encryption key or recipient repository variable is needed.
- Distinctive: line 101
Verify it yourself first (the ladder below: tests, preview, simulator or the Mac).
- Very specific: line 107
Only once they confirm, suggest a release per the semver skill.
- Distinctive: line 136
Keep business logic independent of transport, clocks and recording devices.
- Distinctive: line 137
Use the project's existing injection seams.
- Distinctive: line 152
Never disable host networking, revoke shared permissions or disturb another agent's simulator to simulate failure.
- Distinctive: line 167
Call order for a project:
- Distinctive: line 235
After publication, three phone install paths remain available:
- Distinctive: line 260
When the local-network install fails because the phone is unreachable, ask the owner which of 2 and 3 they prefer - neither is the default, …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Apple Dev
How an agent builds, verifies, and delivers an Xcode project. Local
development uses the build host named in the machine sheet (Xcode,
simulators, the xcode MCP server). The sheet also identifies the phone
installer, the Mac paired to the owner's iPhone. While iterating, builds
are signed and installed from the build host; CI signs once the owner
confirms iteration is done.
Verbs
Every Apple template (ios-app, appstore-app, macos-app) ships the same
just interface; a project's justfile is the truth for its own extras.
| Verb | Does | Needs |
just gen | XcodeGen regenerates the .xcodeproj from project.yml (every other verb runs it first) | xcodegen |
just check | Unsigned build - the CI gate | nothing |
just test | Unit tests on a simulator (iOS) or the Mac (macOS) | a simulator runtime |
just run | Debug build, boot the simulator if needed, install, launch; prints bundle=<id> simulator=<udid> (macOS: opens the app from build/) | a simulator runtime |
just build | Debug build + install on the phone (automatic signing, readable logs) | IOS_DEVELOPMENT_TEAM, IOS_DEVICE_ID, Xcode signed into the team |
just deploy | Local Release Ad Hoc export + phone install (ios-app; a host without the identity in its keychain uses scripts/sign-ios.py with the signing material in the environment) | + IOS_PROFILE, the Apple Distribution identity in the keychain |
just ota | Add build/<App>.ipa to the machine's shared tailnet install page, https://<tailnet name>/install/ (ios-app only; returns at once) | a verified, decrypted CI IPA or local export; Tailscale with HTTPS certificates |
just logs | Five minutes of device logs into logs/ (Debug installs only) | IOS_DEVICE_ID |
just install | macos-app: Release build copied to ~/Applications/<App>.app and opened, beside a released copy in /Applications | nothing; MAC_INSTALL_HOST for another Mac |
Environment interface: IOS_TEST_DESTINATION (simulator, default in the
justfile), IOS_DERIVED_DATA (keep it outside any cloud-synced folder),
IOS_INSTALL_HOST (ssh host of the phone installer; unset = this Mac
installs), MAC_INSTALL_HOST (ssh host that gets the Mac pre-release build;
unset = this Mac). Raw xcodebuild output is long: pipe it through xcbeautify
when you read it yourself.
Iterate locally, CI after the owner confirms
Measured on the build host (Apple silicon, 2026-10) against GitHub-hosted runners for the same apps:
| Build | Local cold / warm | CI, push to artifact (median, worst) |
Small iOS app, signed Ad Hoc .ipa | 13 s (12 s unsigned archive, 8 s warm) | 2.2 min, 4.8 min |
| Large multi-target iOS app | 48 s / 3 s | 6 min, 44 min (queued behind other runs) |
| Large macOS app, Release | 91 s / 2 s | 30 min, 2 h (signing + notarization) |
Local wins by one to two orders of magnitude while iterating. So:
- While iterating, build, sign and install from the build host:
just build,just deploy, orscripts/sign-ios.pyintobuild/plus thejust otaand the install; macOSjust install. Never push, tag or dispatch CI only to get an installable build. Request a secret-manager unlock up front when local signing needs one. - Before a long local build: at least 15 GB free, one build at a time on a
shared host (its build lock),
COMPILATION_CACHE_ENABLE_CACHING=NO, and delete your own products afterwards. - Ask the owner whether iteration is done. Only then dispatch the Ad Hoc
workflow or push the release tag, and install from the CI artifact. Wait
on the run with a waker if the environment has one (one API call every
few minutes), never with parallel
gh run watchloops. - Use CI earlier only when the build host is short on disk or overloaded or the signing material cannot be reached. Developer ID notarization and App Store/TestFlight uploads are CI-only.
Personal Ad Hoc CI
ios-app uses a manual workflow_dispatch signing workflow; ordinary
validation runs do not need signing secrets. CI archives and exports for
registered devices, with installation as a separate step. TestFlight and
App Store submission belong to appstore-app.
The signing helper accepts --project, --scheme, and --output, with
IOS_CERTIFICATE_P12_BASE64, IOS_CERTIFICATE_PASSWORD, IOS_PROFILE_BASE64,
and optional IOS_DEVICE_ID. Credentials come from the project's configured
secret manager. CI imports into a temporary keychain, restores the previous
keychain search list, and removes temporary signing material even on failure.
A runner does not need a desktop login or the phone paired to it.
Generate a temporary age identity in private local scratch for each dispatch.
Supply only its public key as artifact_recipient; keep the private
identity locally until download/decryption is complete. Watch the workflow,
then download its encrypted .ipa.age artifact within the one-day retention
period. Decrypt locally, verify the exported app's signature, bundle/team and
device profile, and place the IPA at the path expected by the project's
install/OTA helper. Remove the temporary identity after decryption and the
decrypted transfer copy when installation no longer needs it.
Public-repository Actions artifacts are not private. Upload only the encrypted IPA, never standalone profiles, P12 files, private keys, keychains, or plaintext IPAs. The IPA necessarily contains an embedded provisioning profile with device IDs; retain it for installation and keep it out of logs. No persistent encryption key or recipient repository variable is needed.
Pre-release builds
A change reaches the owner as a pre-release build before it becomes a release:
- Verify it yourself first (the ladder below: tests, preview, simulator or the Mac).
- Install a pre-release build where the owner uses the app: iPhone
just build(Debug, throughIOS_INSTALL_HOST); macOSjust install(throughMAC_INSTALL_HOST). - Tell the owner exactly what to check, and finish testing with them.
- Only once they confirm, suggest a release per the
semverskill. Several verified changes ship as one release. A pre-release install is not a release: no version bump, no tag.
Verification ladder
Choose the cheapest sufficient evidence for each affected behavior. A passing mock does not discharge a separate hardware or interaction requirement.
- Logic:
just test. Write the test first; mutation-test it. - A view:
RenderPreviewthrough the Xcode MCP on the#Previewof the changed view - an image without booting anything. - The running app on a simulator:
just run, then a device-interaction session through the Xcode MCP (below). Every capture returns a PNG and the accessibility hierarchy (label, frame, a precomputed point to tap); find controls by label, never by guessed coordinates. ReadGetConsoleOutputfor OSLog and crashes. - Hardware-only behavior (camera, microphone, ShazamKit, push, HealthKit,
real network conditions):
just build(Debug, logs readable) or install the verified CI Ad Hoc IPA, then ask the owner for the one check only a human can do. - A macOS app:
just runopens it frombuild/; look at it and drive it with themac-controlskill (screenshot, accessibility tree, clicks).
Regression-lock a flow only when the project has one worth locking: add an XCUITest target then, not by default.
Deterministic tests and hardware boundaries
Keep business logic independent of transport, clocks and recording devices. Use the project's existing injection seams; add a small protocol or closure only where a concrete test needs it. Test fixtures contain synthetic data or licensed sample audio, never a user's recordings, credentials or identifiers.
| Behavior | Deterministic evidence | Separate acceptance |
| Connectivity and retry | Inject timeout, offline and server rejection into the real request path; assert pending/error state, cancellation, retry after recovery and no duplicate submission. Restart and verify recovery when persistence is promised. | Exercise the running app's offline/reconnect UI; real service access and OS background delivery need their own check. |
| Audio processing | Feed a prerecorded fixture through the production decoding/signature/processing path; assert useful output and malformed/empty-input handling. | A fixture proves processing, not microphone access, route selection, capture quality or recognition by a live service. |
| Recording | Inject permission outcomes, interrupted capture and input failure; assert state, duration and cleanup using a controlled clock. | Verify actual permission denied/granted behavior, microphone capture and relevant interruptions on the device that provides them. |
| Taps, swipes and drags | Use current screenshot and accessibility hierarchy to target the running view; perform the gesture and assert its visible result, boundary behavior and cancellation. | A preview or a model method call does not prove hit targets, scrolling or gesture conflicts; capture the resulting UI. |
For a bug, reproduce the user-visible failure before the fix. Test the expected behavior, then temporarily remove the fix or break the relevant transition to prove the test detects it. Keep fake services confined to previews/tests or an explicit development entry point; normal app launches use production services. Never disable host networking, revoke shared permissions or disturb another agent's simulator to simulate failure. Use an isolated test instance.
Record which evidence came from a unit test, fixture, simulator, Mac or physical device. Finish all available programmatic checks before asking for one precise human-only check. Unavailable microphone, phone, push or signing acceptance stays unverified with its required action; do not replace it with a mock pass.
The Xcode MCP server
Declared as xcode (xcrun mcpbridge), backed by Xcode's headless
mcp-server daemon, so no Xcode window is needed. The tool list is the same
(54 tools) whatever the state; a short list means the server is broken, not
that a workspace is missing.
Call order for a project:
XcodeOpenWorkspaceon the.xcodeproj(afterjust gen) and keep itsworkspaceIdentifier: headless calls take it explicitly, every time.XcodeListSchemes/XcodeListRunDestinationsonce, thenBuildProject; errors come fromGetBuildLog(filter to errors) rather than the raw log.RunAllTests/RunSomeTests;GetTestListto pick.RenderPreviewfor a view;RunCodeSnippetto evaluate an expression in a file's context. File arguments are project-organization paths (<Project>/App/ContentView.swift, found withXcodeGlob), never filesystem paths. "Not built with -Onone" means the Debug configuration is optimized: setSWIFT_OPTIMIZATION_LEVEL: "-Onone"for Debug inproject.yml(XcodeGen does not) andjust gen.DeviceInteractionStartWorkspaceSession→DeviceInteractionInstallAndRun→DeviceInteractionSynthesize(tap, swipe, type, press, capture) →DeviceInteractionEndSession. The command grammar forSynthesize(t x yto tap, capture, type, swipe) is in Apple'sdevice-interactionskill; read it before the first call. Physical devices work here too when the session's Mac has the device paired.GetConsoleOutput(regex filter) while the app runs;InvokeDebuggerCommandfor LLDB against the running process.DocumentationSearchbefore guessing an API;XcodeGrep/XcodeReadfor project-aware search.
Rules:
- "Waiting for the user to approve this request" on the first
workspace call: the agent's code signature is not approved yet.
xcrun mcp-server statuslists it under Pending approvals with an id. Where the machine sheet names an approve helper, run it and retry once; otherwise the owner runssudo xcrun mcp-server approve --always <id>. Report the id and stop; never retry in a loop. Workspaces outside the permitted folders need the same kind of grant (allow-folder). - Every call hangs,
statussays "the service is running but did not answer": a windowed Xcode is running on the host and shadows the headless server. Quit it (osascript -e 'tell application "Xcode" to quit'),xcrun mcp-server stop, call again. Anything that launches the Xcode app (xcrun agent skills export,open *.xcodeproj,just dev) recreates the problem on a build host. Apple's skills come headless fromxcrun agent plugin path --plugin-format claudeinstead. xcrun mcp-server show-logsprints the path of the agent activity log: every connection, refusal and tool call, with the reason.- The MCP closes the build and runtime loop; it does not see a 10-pixel layout mistake for you - capture and look.
Raw fallbacks (no MCP, CI, scripts)
xcodebuild -project App.xcodeproj -scheme App -destination "platform=iOS Simulator,name=iPhone 17" test 2>&1 | xcbeautify
xcrun xcresulttool get test-results summary --path build/App.xcresult # or: get --format json --path
xcrun simctl list devices available --json | jq '.devices[][] | select(.name=="iPhone 17") | .udid'
xcrun simctl boot <udid>; xcrun simctl install <udid> path/to/App.app; xcrun simctl launch <udid> <bundle-id>
xcrun simctl io <udid> screenshot shot.png
xcrun simctl spawn <udid> log stream --predicate 'subsystem == "<bundle-id>"' --style compact
xcrun devicectl list devices
xcrun devicectl device install app --device <udid> build/App.ipa
xcrun devicectl device process launch --device <udid> --console <bundle-id>Phone install
A new installable iOS build always replaces its entry on the shared install
page with just ota, including builds also installed over Wi-Fi or cable.
Download, decrypt and verify a CI IPA before publishing it. Verify the served
manifest and IPA match the intended version/build; give the owner the page URL.
Publishing is a required delivery step, not a fallback for an unreachable phone.
A delivered Debug .app also needs a verified Ad Hoc .ipa on the page; simulator
builds do not. Local Debug builds remain useful for readable device logs.
After publication, three phone install paths remain available:
- Local network (default, try it first).
IOS_INSTALL_HOSTmakesjust build/ localjust deploydo the hop. For a CI IPA, use the project's install helper on the downloaded artifact without rebuilding or signing it again. Copy the artifact to the paired host and rundevicectl device install appthere. The phone must be on the same network as the installer and visible to it (pairing happened once by cable).devicectlfinds the phone by local discovery, so this fails on networks that isolate clients (train, hotel, guest Wi-Fi):devicectl list devicesthen shows the phoneunavailable, itstunnelStateunavailable. It cannot be aimed at a VPN address. - Tailnet install link.
just ota(scripts/ota-install.py) adds the verified Ad Hoc.ipato the serving Mac's one install page,https://<tailnet name>/install/, and prints that URL; the owner opens it in Safari on the phone and taps Install. Any network, one tap. Every agent's apps share the page: a new app is appended, a newer build of the same app replaces its entry, entries expire after 48 h (OTA_TTL_HOURS), and the page server removes itself once the list is empty. Hand over the URL and confirm with the owner that the app updated. A project still carryingscripts/ota-install.shpredates the shared page: copy the template'sota-install.pyin rather than running the old script. - Cable. The owner plugs the phone into the installer; the install command the failed recipe printed then works.
When the local-network install fails because the phone is unreachable, ask the owner which of 2 and 3 they prefer - neither is the default, and both need them. When the installer itself is asleep or unreachable, report the artifact path and the install command in one line and continue with what does not need the phone. No retries, no polling.
Installing an already signed IPA does not require a signing identity on the
installer. Local signing needs an accessible signing keychain: a locked login
keychain can cause errSecInternalComponent over SSH. Use the configured
signing setup; CI uses its temporary keychain instead of a desktop session.
Gotchas
- Derived data outside cloud-synced folders (
IOS_DERIVED_DATAor the justfile default under~/Library/Developer): resource forks from a sync client break code signing with opaquecodesignerrors. - Simulator Keychain needs an identity:
CODE_SIGN_IDENTITY=-with signing allowed (theios-appjust testdoes this); a fully unsigned build reads and writes no Keychain items. xcodegenbehind a package-manager symlink cannot find its bundled settings presets; resolve the real executable (realpath "$(command -v xcodegen)") - the templates'just genalready does.- Cloud-evicted inputs: a project under a synced folder can have dataless files; empty reads and "no such module" in a clean tree mean eviction, not a code problem. Materialize the tree first.
- Only Debug installs have readable logs: Release strips
get-task-allow;just logson a Release install returns nothing. - MCP grants are per code signature: the approved binary is the agent
itself; a wrapper (
timeout, a shell script) gets approved instead and the agent stays blocked. Unsigned binaries get 24-hour grants only. - Signing from a shell without a desktop login (ssh, a headless build
host): the login keychain refuses with "User interaction is not allowed"
on import and
errSecInternalComponenton codesign. Signing identities for such a host live in a dedicated keychain that the shell unlocks itself, as CI does. - App Store installs need root:
mas installcannot run inside a non-interactive switch; the first install of Xcode comes from the App Store app, and an installed Xcode with an unaccepted license stops Homebrew entirely untilxcodebuild -license acceptruns as root. - "Profile doesn't match" / "no identity found" on
just deploy: the Apple Distribution identity or the Ad Hoc profile is not installed on this Mac; both are machine setup, not a project bug. - Full rebuilds per MCP call: the server does not track incremental state between invocations; batch edits before rebuilding.
- XCUITest cannot read the app's clipboard from the runner: reading
UIPasteboard.generalthere hits the cross-app paste permission and never returns the copied text. Write a sentinel from the runner (writes are allowed), then paste through the OS edit menu into a field of the app under test and read that field's value. - Read-only SwiftUI rows expose one combined label:
LabeledContentappears as a single static text"Label, Value". ExactstaticTexts["Value"]lookups never match, so negative checks on them pass vacuously; match withlabel CONTAINS. - iOS 27
confirmationDialogis a popover with no cancel button: only the destructive action is a button. Dismiss by tapping a point outside the popover's frame, then assert the sheet is gone.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

