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.

Building an agentic loop for Xcode in Cursor

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

hooks.json
.scheme# optional — auto-detected from project if empty
.xcode-loop# on | off
run.sh# orchestrates build + finish
start.applescript
finish.sh
shared.sh# helpers (debounce, cooldown, toggles)
on-save.sh# afterFileEdit hook
on-stop.sh
SKILL.md

After each run, the agent also reads generated files in .build/ at your repo root:

FileWhat the agent sees
xcodebuild-status.txtsucceeded YourScheme or failed YourScheme
xcodebuild-issues.txtErrors like ContentView.swift:42:0: error: …
xcode-build-hook.logHook 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:

GuardWhat it does
.xcode-loopMaster on/off toggle
Debounce6s quiet after last Swift save
Cooldown30s between hook builds (stop bypasses)
In-progressSkips if a build watcher is already running
Path filterIgnores .build/ and DerivedData/
Device RunFinalizes 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 the afterFileEdit and stop entries. Do not overwrite unrelated hooks.
  • Wrong scheme? Run doctor.sh and 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!