name: mac-builder
description: Use when building, testing, running, or deploying the Silo Apple clients (iOS, tvOS, macOS) from the Linux workstation, which has no Xcode — all Apple toolchain work runs on the remote mac-builder Mac over SSH and XcodeBuildMCP. Covers syncing the working tree to the Mac, XcodeGen, simulator and device builds, code signing and the keychain trap, reading real build errors, and on-device log capture. Not for editing Swift source (do that locally) and not for server or Android work.
Building the Apple clients on mac-builder
This repo is developed on a Linux host with no Xcode. Every xcodebuild, simctl, devicectl,
and signing operation runs on a remote Mac reached through the SSH alias mac-builder.
Read this before the first build in a session. Full background, one-time setup, and the
rationale behind each rule: docs/mac-builder.md.
Two channels, and why the difference matters
- XcodeBuildMCP (
mcp__xcodebuildmcp__*) — a long-lived stdio MCP server running on the Mac over SSH. Structured build/test/device/simulator/UI-automation/LLDB tools. Prefer these. - Ad-hoc
ssh mac-builder '<command>'— for what MCP doesn't cover:xcodegen, keychain unlocking, reading raw build logs,devicectl --console.
These are separate SSH sessions. State set in one does not apply to the other. That single fact causes the worst failure in this workflow — see Signing.
If MCP tools are missing entirely, the server isn't connected; fall back to ad-hoc SSH and tell
the user. If only some tools exist (simulator but no device/macOS/debug), the wrapper's
XCODEBUILDMCP_ENABLED_WORKFLOWS is wrong — see docs/mac-builder.md.
Step 1 — Sync the working tree, and verify it
The Mac has its own checkout at ~/silo-apple-deploy. It is build scratch. Never edit source
there, never commit from it. Local Linux checkout is the source of truth.
A build result from a stale tree is worse than no build. Always sync, always verify.
Full sync — first sync of a session, after a branch switch, or after large changes:
rsync -az --delete --exclude 'DerivedData' --exclude 'build' \
/srv/dev/github/SiloServer/silo-apple/ mac-builder:~/silo-apple-deploy/
Incremental sync — the fast path while iterating (seconds, not tens of seconds). Diff against a fixed base commit and reset first, so each push is the full cumulative delta:
git diff <base-commit> > /tmp/silo.patch
ssh mac-builder 'cd ~/silo-apple-deploy && git checkout -- .'
ssh mac-builder 'cd ~/silo-apple-deploy && git apply - && git status --short' < /tmp/silo.patch
Then verify — do not skip this:
ssh mac-builder 'cd ~/silo-apple-deploy && git log --oneline -1 && git status --short | wc -l'
The commit must match local HEAD and the count must match local git status --short | wc -l.
If either differs, fix the sync before building. Do not report a build result from an unverified
tree.
Step 2 — Regenerate the project
Silo.xcodeproj is generated by XcodeGen and is not committed. A stale one silently omits new
files, producing confusing "cannot find type" errors.
ssh mac-builder 'cd ~/silo-apple-deploy/iosApp && /opt/homebrew/bin/xcodegen generate'
Required after any sync touching project.yml, adding/removing files, or changing signing
xcconfigs. Cheap — just run it after every sync. Use the absolute path: a non-interactive SSH
session has no Homebrew on PATH.
Step 3 — Session defaults
session_show_defaults # always call before the first build in a session
If unset, set them once so later calls take no arguments:
// session_set_defaults
{
"projectPath": "/Users/<mac-user>/silo-apple-deploy/iosApp/Silo.xcodeproj",
"scheme": "Silo",
"simulatorId": "<uuid from list_sims>",
"configuration": "Debug"
}
Get <mac-user> from ssh mac-builder pwd. Discover IDs with list_sims / list_devices —
never hardcode simulator or device UUIDs; they change on runtime reinstall and re-pairing,
and device IDs are personal hardware identifiers that must not be committed to this public repo.
Schemes: Silo (iOS), SiloTV (tvOS), SiloMac (macOS).
Building and testing
Simulator — the default
Needs no developer signing identity, so it never hits the login-keychain trap. Normal simulator
build/run tools still apply Xcode's local ad-hoc signature and simulated entitlements. Keep that
signature for any flow that signs in: Silo stores its session in the Keychain, and an installed
CODE_SIGNING_ALLOWED=NO product has no Keychain entitlement, so authentication exists only in
memory and disappears on the next launch.
| Intent | Tool |
|---|---|
| Build | build_sim |
| Build and run in one call | build_run_sim (prefer over build + launch — fewer SSH round trips) |
| Unit tests | test_sim |
| tvOS / macOS | same tools with {"scheme": "SiloTV"} / build_macos, test_macos |
| Screenshot, UI tree | screenshot, snapshot_ui |
| Interact | tap, type_text, swipe, button |
| Debug | debug_attach_sim, debug_breakpoint_add, debug_stack, debug_variables |
Fast compile check, no signing
Quickest confirmation that a platform still builds:
ssh mac-builder 'cd ~/silo-apple-deploy/iosApp && \
xcodebuild build -project Silo.xcodeproj -scheme SiloTV \
-destination "generic/platform=tvOS" CODE_SIGNING_ALLOWED=NO 2>&1 \
| grep -E "error:|BUILD (SUCCEEDED|FAILED)"'
-scheme SiloMac -destination "platform=macOS" for the Mac app.
This is compile-only. Never install or launch this unsigned product for authenticated UI
validation. Use build_run_sim or a normal simulator xcodebuild without
CODE_SIGNING_ALLOWED=NO, reuse the same simulator, and verify the built app contains simulated
application-identifier / keychain-access-groups entitlements before asking the user to sign
in. Once authenticated, prefer stop/launch over reinstalling when the binary has not changed.
Physical device
Requires real signing. Discover the device first:
ssh mac-builder 'xcrun devicectl list devices'
An Apple TV reads unavailable while asleep — ask the user to wake it rather than retrying.
Build with the keychain unlock in the same SSH command (see below), then:
ssh mac-builder 'xcrun devicectl device install app --device <DEVICE_ID> \
~/silo-build-ios/Build/Products/Debug-iphoneos/Silo.app'
ssh mac-builder 'xcrun devicectl device process launch --device <DEVICE_ID> org.siloserver.silo'
Use the stable derived-data paths ~/silo-build-ios / ~/silo-build-tvos so incremental builds
stay warm and product paths are predictable. tvOS products land in Debug-appletvos — installing
a Debug-appletvsimulator bundle fails confusingly, so never mix destinations.
Signing and the keychain trap
security unlock-keychain does not persist across SSH sessions. Each connection is its own
security session. Unlocking in one ssh call has no effect on the next one, and none on the
already-running MCP server — a different, long-lived session.
Symptom: the build dies in a CodeSign step, often on an embedded extension rather than the app.
MCP output shows only "Command CodeSign failed"; the real cause (User interaction is not allowed) is in the raw log.
Rules:
- Normally signed simulator builds are unaffected. Xcode uses "Sign to Run Locally" and does not need the developer login keychain.
CODE_SIGNING_ALLOWED=NOis compile-only. It is safe for build checks, but an installed Silo product cannot persist its Keychain-backed login across relaunches.- Device builds: unlock inside the same
sshcommand asxcodebuild:
ssh mac-builder 'security unlock-keychain -p "$KEYCHAIN_PASSWORD" ~/Library/Keychains/login.keychain-db && \
cd ~/silo-apple-deploy/iosApp && \
xcodebuild build -project Silo.xcodeproj -scheme Silo \
-destination "id=<DEVICE_ID>" -derivedDataPath ~/silo-build-ios \
DEVELOPMENT_TEAM=<TEAM_ID> -allowProvisioningUpdates 2>&1 | tail -30'
- If codesign still fails right after a successful unlock, the partition list is blocking non-interactive access:
ssh mac-builder 'security unlock-keychain -p "$KEYCHAIN_PASSWORD" ~/Library/Keychains/login.keychain-db && \
security set-key-partition-list -S apple-tool:,apple:,codesign: -s \
-k "$KEYCHAIN_PASSWORD" ~/Library/Keychains/login.keychain-db'
- Cheap isolation test instead of a five-minute build:
ssh mac-builder 'security find-identity -v -p codesigning'
ssh mac-builder 'cp /bin/echo /tmp/cs-probe; codesign --force --sign <IDENTITY_HASH> /tmp/cs-probe; rm -f /tmp/cs-probe'
Watch for CSSMERR_TP_CERT_REVOKED — revoked certs linger in the keychain and get picked ahead
of the good one.
Secret handling. The keychain password and team ID are not stored on the Linux host and must not be written into this repo. Ask the user, or have them unlock the keychain out-of-band. Never inline a password in a command — agent sessions write full transcripts to disk. If a device build isn't essential to the task, use a simulator or unsigned build and say so.
Signing configuration is in-repo: iosApp/Signing/Local.xcconfig.sample → Local.xcconfig
(gitignored) overrides bundle IDs, entitlements, and DEVELOPMENT_TEAM for Personal Teams. Run
xcodegen generate after editing it.
Reading the real build errors
MCP output is summarized and truncated. Full logs are on the Mac; the tool result gives the exact
path under ~/Library/Developer/XcodeBuildMCP/workspaces/<workspace>-<hash>/logs/.
ssh mac-builder 'grep -E "error:|CSSMERR|Command CodeSign" "<log-path>" | head -20'
ssh mac-builder 'grep -B5 -A25 "CodeSign /Users.*<Target>" "<log-path>"' # signing failures
For raw xcodebuild output, always filter with grep -E "error:" — real errors hide under
thousands of lines of toolchain noise. Do not diagnose from the tail alone.
Capturing device logs
OSLog does not reach devicectl --console; print() to stdout does.
ssh mac-builder 'nohup xcrun devicectl device process launch \
--device <DEVICE_ID> --console org.siloserver.silo \
>> /tmp/silo-run.log 2>&1 </dev/null & disown'
All three of nohup, </dev/null, and disown are required, or a SIGHUP / SIGTTIN stop / shell
exit kills the stream.
Tail with tail -n 0 -F (capital -F survives truncation) piped through grep --line-buffered
— without line buffering, events arrive in minute-long bursts.
Closing the --console stream terminates the app on the device. Never pkill that process
mid-test; if you do, you caused the App terminated due to signal 15 and must relaunch.
--console is launch-time only — you cannot attach to an app the user is already using.
Checklist before reporting a result
- [ ] Synced, and commit + dirty-file count verified against local
- [ ]
xcodegen generaterun after the sync - [ ] Errors read from the full log, not the truncated tool output
- [ ] Destination matches the claim (simulator build ≠ device verification)
- [ ] Authenticated simulator runs use a normally signed product, not
CODE_SIGNING_ALLOWED=NO - [ ] Nothing committed or edited on the Mac
When the simulator misbehaves, check the disk first
A near-full boot volume on the Mac does not announce itself. It presents as CoreSimulator corruption, and every symptom points somewhere else:
xcrun simctl installhangs forever on a ~100 MB app.- A booted device shuts itself down between two commands.
simctl bootreports "delete the device properly or erase contents and settings".ps/pgrepblock, because simulator processes are wedged in kernel wait.- A stray
simctl diagnosesits holding a device lock — a downstream symptom, not the cause.
ssh mac-builder 'df -h /System/Volumes/Data'
Below ~15 GiB free, expect trouble. ~/Library/Developer/CoreSimulator/Devices runs to tens
of GB on its own. Reclaim with xcrun simctl erase <udid> for devices you aren't using (this
wipes their app data and any signed-in session) and xcrun simctl delete unavailable.
~/silo-build-ios and ~/silo-build-tvos are symlinks to /Volumes/NVMe/..., which has
far more room than the boot volume. The documented paths above still work unchanged — don't
convert them back into real directories on /.
Gotchas
- SourceKit "No such module" errors are IDE index artifacts. If
xcodebuildsaysBUILD SUCCEEDED, the code is fine. mac-builder's Tailscale direct path sometimes drops and falls back to a lossy DERP relay. SSH then times out whiletailscale ping mac-builderstill answers (via DERP, ~300ms). The machine is not down; it recovers on its own. Connecting by raw Tailscale IP may work while the hostname does not.- Device install can fail on a Developer Disk Image mismatch when the device OS is newer than the Mac's Xcode. Updating Xcode is the fix; the build isn't wrong.
- Homebrew is not on the non-interactive SSH
PATH. Use absolute paths:/opt/homebrew/bin/xcodegen,/opt/homebrew/bin/xcodebuildmcp. - Commits, pushes, and PRs happen on Linux against the real working tree — never from
~/silo-apple-deploy. - Codex memory may reference a helper at
/home/dev/.local/bin/silo-apple-deploy. That script no longer exists. Use the sync steps above.
Not for this skill
- Editing Swift source — do that in the local checkout, then sync.
- Silo server or Android work.
- Client bugs reproducible without a build.
Next.js App Router Expert
Development
A skill that turns Claude into a Next.js App Router expert.
README Generator
Development
Creates professional and comprehensive README.md files for your projects.
API Documentation Writer
Development
Generates comprehensive API documentation in OpenAPI/Swagger format.