Building an agentic loop for Xcode in Cursor
Your agent can write Swift, but it can't see if it compiled. I wired Cursor and Xcode into a loop so builds run in the background and errors go straight to the agent. Free starter kit inside.

The current problem of iOS development
I've been developing my own iOS app, Beam, a project and task manager, in Cursor, my editor of choice. The agent writes Swift fine; verifying it meant constantly switching between Cursor and Xcode: Run, copy errors, paste, repeat. Tedious, and the context-switching made my dev workflow more time consuming.
As a web developer, I'm used to closing that loop with runtime feedback: test output, a dev server log, a failing API call. iOS is different. The gate is compile time: Swift has to build before anything runs, and that feedback lives in Xcode (builds, simulators, compiler errors). Cursor can write the Swift; it cannot see whether it compiled.
I couldn't believe there still wasn't a built-in setting for this. Recent Xcode betas actually have one (giving external agents access to Xcode): Intelligence → allow external agents via MCP. But that's the agent calling Xcode tools when you ask—not a closed loop that builds and feeds errors back on its own. So I wired up my own agentic loop instead. Here's how it works, plus a starter kit for your iOS project.
What is an agentic loop?
An agentic loop is when the agent and the environment take turns automatically: the agent changes something, the environment runs and produces output, and the agent reads that output on the next turn. You wire it up once. After that, you're not the one copying errors or switching apps in between.
The phrase is still fairly new. In June 2026 Peter Steinberger posted on X: "You shouldn't be prompting coding agents anymore. You should be designing loops that prompt your agents." Anthropic's Claude Code team wrote it up in
getting started with loops.
The pattern is always the same: act → observe → repeat. Without that feedback channel, you are the loop.
TL;DR: I wired a loop: when Swift files are updated in Cursor, hooks trigger shell scripts that tell Xcode to build. When the build finishes, the agent reads build status and error logs, edits again, and builds again. Rinse and repeat. Skip to the prompt →
Deep dive
Three layers: hooks decide when to build, shell + AppleScript drive Xcode, and plain-text output gives the agent something reliable to read. Below is a look at what each file does and how they work together.
Starter kit layout
After each run, the agent also reads generated files in .build/ at your repo root:
| File | What the agent sees |
|---|---|
xcodebuild-status.txt | succeeded YourScheme or failed YourScheme |
xcodebuild-issues.txt | Errors like ContentView.swift:42:0: error: … |
xcode-build-hook.log | Hook activity when something misfires |
Cursor hooks
Cursor calls these when Swift files are edited and when the agent stops. They decide whether to kick off a build.
hooks.json wires those events to the shell scripts. afterFileEdit fires after a Swift edit; stop fires when the agent ends a turn.
hooks.json{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": "bash .cursor/automations/xcode-ios-loop/hooks/on-save.sh",
"timeout": 20
}
],
"stop": [
{
"command": "bash .cursor/automations/xcode-ios-loop/hooks/on-stop.sh",
"timeout": 15
}
]
}
}
If you already have a .cursor/hooks.json, merge these two entries. Do not overwrite unrelated hooks.
on-save.sh (wired to afterFileEdit) reads the edited file path from stdin via jq, ignores non-Swift files and build artifacts, debounces for six seconds, then calls run.sh. Only the last edit in a burst wins, so ten quick agent edits become one build, not ten.
on-save.shinput=$(cat)
file_path="$(echo "$input" | jq -r '.file_path // empty')"
[[ -n "$file_path" && "$file_path" == *.swift ]] || exit 0
case "$file_path" in */.build/*|*/DerivedData/*) exit 0 ;; esac
token="$$-$(date +%s%N)"
echo "$token" > "$STAMP"
nohup bash -c "
sleep \"${DEBOUNCE_SEC}\"
[[ \"\$(cat \"$STAMP\" 2>/dev/null)\" == \"$token\" ]] || exit 0
xcode_run_build \"afterFileEdit\"
" >> "$HOOK_LOG" 2>&1 &
on-stop.sh is the safety net: always triggers a build when the agent finishes, bypassing debounce and cooldown. Even if saves were skipped or debounced away, you still get a run after each turn.
on-stop.shxcode_log_hook "stop hook"
xcode_run_build "stop"
Build pipeline
Once a hook fires, these scripts talk to Xcode and write results back for Cursor.
shared.sh is imported by every hook and script. It reads .xcode-loop and .scheme, finds your .xcworkspace or .xcodeproj (up to four levels deep), auto-detects a scheme if .scheme is empty, enforces a cooldown between hook builds, skips duplicate watchers, and decides whether to call run.sh:
shared.shxcode_run_build() {
[[ "$reason" == "stop" ]] && XCODE_FORCE_BUILD=1 # bypass cooldown
if xcode_within_cooldown; then return 0; fi
if xcode_action_running && [[ "${XCODE_FORCE_BUILD:-}" != "1" ]]; then return 0; fi
XCODE_BUILD_TRIGGER="$reason" "$AUTOMATION_DIR/run.sh" >> "$log" 2>&1 &
}
run.sh is the orchestrator. Everything eventually flows through here. It opens Xcode if needed, kicks off the build, watches for completion, then calls finish.sh:
run.shosascript "$TMP_START" "$SCHEME" "$XCODE_ACTION" "$XCODE_ACTIVATE"
echo "running $SCHEME" > "$STATUS_FILE"
# poll Xcode for completed / failed / errors; finalize on device Run
# when status stays "running" with 0 errors (~45s stable)
(
while (( SECONDS < deadline )); do
snapshot="$(xcode_action_snapshot)" # completed, status, err_count
[[ "$completed" == "true" ]] && break
[[ "$build_status" == "failed" || "$err_count" != "0" ]] && break
# device Run: break when status=running, 0 errors, stable ~10s (after 45s min)
[[ "$device_run_stable" == "1" ]] && break
sleep 2
done
"$FINISH_SCRIPT" "$ISSUES_FILE" "$STATUS_FILE" "$SCHEME"
) &
start.applescript tells Xcode to switch to your scheme and hit Run. Not headless xcodebuild, so signing, simulators, and provisioning behave like normal development.
start.applescripttell application "Xcode"
tell active workspace document
-- switch scheme, stop any in-flight Run, then build or run
if completed of last scheme action result is false then stop
run
end tell
end tell
By default Xcode stays in the background (XCODE_ACTIVATE=0) so Cursor keeps keyboard focus.
finish.sh queries Xcode's last scheme action result and writes compiler errors in a format agents already understand, plus a one-line status file.
finish.sh# write errors as file:line: error: message
while IFS=$'\t' read -r fp ln msg; do
echo "$fp:$ln:0: error: $msg" >> "$ISSUES_FILE"
done < <(osascript ...) # reads build errors from Xcode
xcode_build_succeeded() {
[[ "$build_status" == "succeeded" ]] && return 0
# device Run: status stays "running" while the app is open
[[ "${XCODE_ACTION:-run}" == "run" && "$build_status" == "running" && "${err_count:-1}" == "0" ]]
}
if xcode_build_succeeded; then
echo "succeeded $SCHEME" > "$STATUS_FILE"
else
echo "failed $SCHEME" > "$STATUS_FILE"
fi
The agent workflow becomes: fix Swift → wait for hook build → read status and issues → repeat. No screenshots, no copy-paste from Xcode.
Agent instructions
SKILL.md teaches the agent how to participate. Hooks handle when to build; the skill handles how: read .build/xcodebuild-status.txt and .build/xcodebuild-issues.txt before claiming success, fix compile errors, let hooks rerun, and do not spam ./run.sh in a tight loop.
Swift errors often cascade; the loop lets the agent chew through them turn by turn instead of you pasting each batch manually.
Safeguards
The loop is aggressive by design. These guardrails keep it from melting your machine:
| Guard | What it does |
|---|---|
.xcode-loop | Master on/off toggle |
| Debounce | 6s quiet after last Swift save |
| Cooldown | 30s between hook builds (stop bypasses) |
| In-progress | Skips if a build watcher is already running |
| Path filter | Ignores .build/ and DerivedData/ |
| Device Run | Finalizes when Xcode stays running with 0 errors (~55s) |
Toggle the loop anytime by editing .cursor/automations/xcode-ios-loop/.xcode-loop (one line: on or off).
Troubleshooting
- Status stuck on
running? On a physical device, Run can take ~60s to finalize. Wait, then re-read.build/xcodebuild-status.txt. - Already have
hooks.json? Merge theafterFileEditandstopentries. Do not overwrite unrelated hooks. - Wrong scheme? Run
doctor.shand write the correct name to.cursor/automations/xcode-ios-loop/.scheme.
Two settings I keep on while this runs: Xcode build notifications (so you know when a build finishes without switching apps) and Cursor's prompt finished sound (so you know when the agent is done and Xcode is about to build again). Small things, but they make the loop feel much less blind.
Try it on your project
You need Xcode open, Cursor with hooks, and an iOS project. The starter zip is ~10 files.
Step 1: Copy the files
Unzip the download and copy the .cursor folder into the root of your Xcode project. If you already have a .cursor/hooks.json, merge the afterFileEdit and stop entries. Do not overwrite unrelated hooks.
Step 2: Prompt Cursor
Open your .xcodeproj or .xcworkspace in Xcode and leave it running. Then open the same project in Cursor and paste:
I've copied the xcode-ios-loop .cursor folder into this repo. Set up the Xcode build loop:
- Make all .sh files executable.
- If jq is missing, install it (brew install jq).
- Run .cursor/automations/xcode-ios-loop/doctor.sh and report the output.
- Run .cursor/automations/xcode-ios-loop/run.sh once to verify Xcode builds.
- If doctor lists multiple schemes and picks the wrong one, write the correct scheme name to .cursor/automations/xcode-ios-loop/.scheme.
When you edit Swift:
- Hooks build and run in Xcode after Swift edits (debounced) and when your turn ends.
- Read .build/xcodebuild-status.txt and .build/xcodebuild-issues.txt before claiming success.
- Fix compile errors and let the hook rerun. Do not spam ./run.sh.The agent wires everything up and runs a sanity check. Scheme detection is automatic — doctor lists what's available if you need to override. You write in Cursor, Xcode builds, the agent reads errors and tries again.
Going further
The zip is deliberately minimal: one scheme at a time, no path inference, no runtime log capture. For my app I extended the same pattern for macOS + iOS: scheme selection from file paths, focus pins for shared code, and simulator console output after a successful run.
If you only ship iOS, the starter is enough. If you add a macOS target later, the architecture scales; you mainly need scheme selection logic for shared folders.
This was a fun one to build, and I use it daily on Beam. Hope you find it helpful!