The course I took to get up to speed on agentic coding was TypeScript-based. The pattern it taught was the one most demos show: the agent writes some code, runs the tests, reads the output, fixes whatever broke, runs the tests again. The loop closes in seconds. You can sit and watch it work, or walk away and come back to something done.
Then I pointed the same setup at an iOS project, and the loop didn’t close in seconds. It closed in minutes, sometimes a lot of minutes. The first thing I’d budgeted for, paid plans, turned out to be the easy half of the bill. The expensive half was the two walls the standard playbook walks straight into on iOS: feedback loops slow enough that the agent’s economics shift, and a sandboxing story that doesn’t have a Docker-shaped answer because Docker can’t run macOS.
The simulator tax
On iOS, the simulator tax gets paid twice per change. Once by the agent, on its inner build-and-test loop. Once by you, on the outer review loop where you actually open the app and click around. Both halves are slower than their web equivalents, and the slowness is doing different work in each.
The agent’s inner loop
The validation script the agent runs at the end of every slice is straightforward enough. Two xcodebuild invocations, one to build, one to test. A -quiet flag so the output doesn’t blow the agent’s context budget on Swift compiler chatter. An OTHER_SWIFT_FLAGS='$(inherited) -disable-sandbox' value I’ll come back to in the sandboxing section, because it’s there for a non-obvious reason. And an optional -only-testing:SententisSnapshots scoping flag for runs that only need to re-validate the snapshot suite.
#!/bin/bash
set -euo pipefail
DESTINATION='platform=iOS Simulator,name=iPhone 17 Pro'
TEST_FLAGS=""
if [ "${1:-}" = "snapshots" ]; then
TEST_FLAGS="-only-testing:SententisSnapshots"
fi
steps=(
"xcodebuild build -scheme Sententis -destination '$DESTINATION' OTHER_SWIFT_FLAGS='\$(inherited) -disable-sandbox' -quiet"
"xcodebuild test -scheme Sententis -destination '$DESTINATION' OTHER_SWIFT_FLAGS='\$(inherited) -disable-sandbox' $TEST_FLAGS -quiet"
)
As of mid-2026, on this project, a clean run takes about six minutes. Build is roughly 24 seconds warm. The tests are the rest. Unit tests run subsecond each, the way you’d expect. The snapshot tests run 42 to 48 seconds each, and there are about sixty of them. Snapshot tests are roughly 95% of the wall-clock.
That percentage isn’t a build-system fault. It’s the irreducible cost of asking the simulator to render a SwiftUI view, take a screenshot of it, and compare. Unit tests are fine. Anything that needs the sim to render is expensive, and on a real iOS project, rendering is most of what you actually need to validate.
That number is what reshapes the agent’s economics. On the TypeScript course, “run the tests” was a punctuation mark inside a single train of thought. On iOS, “run the tests” is a coffee break. The agent’s choices change, and so do mine. Four concessions I’ve made on this project, each one a deliberate dodge of the render cost:
-quieton the build, because at six minutes a run, you can’t also afford to flush the agent’s context window every iteration.-only-testing:SententisSnapshotsscoping, so iteration on a snapshot can target just the snapshot suite instead of waking the whole machinery.- SwiftUI explicitly out of TDD. The AFK prompt the agent follows says, in so many words, implement view code directly without TDD and include
#Previewblocks instead. SwiftUI is the most pointed concession on the list because TDD is supposed to be table stakes. It isn’t, here. Writing a snapshot test first means paying 45 seconds per red-green cycle on a view that hasn’t even compiled in the agent’s head yet. The economics aren’t there. - Tracer-bullet slices kept narrow. A TypeScript dev can write a fat failing test and let the agent fan out fixes against it because each run is cheap. An iOS agent that fans out the same way pays the snapshot cost every time. One slice at a time is, partly, just iOS forcing patience the TS workflow doesn’t.
None of those are best practices. They’re concessions the platform extracts because the render is slow and the run isn’t free.
Your outer review loop
The agent’s loop is the half that gets talked about. The half I’ve found harder to deal with is the one I run myself after the agent’s done.
For anything visual, automated tests on iOS are necessary but not sufficient. Snapshots tell me the pixels didn’t change unexpectedly. They don’t tell me the interaction feels right, that the animation lands, that the empty state I never wrote a snapshot for looks like an empty state. So I pull the branch, build, launch the simulator, and click around. That last step is near-instant on a web project. On iOS it’s a wait, then a launch, then a navigation back to wherever the change actually lives, then the look.
I don’t have a workaround. Hot reload tools exist, previews help for components, but neither closes the loop the way a browser refresh does. I’m naming it as a finding, not as a problem I’ve solved.
This is also where post 2’s smudge bites hardest. The cost of not skimming is structurally higher on iOS than on web, which makes the temptation to skim worse exactly where the consequences of skimming are worst.
Sandboxing
The other wall is safety. The standard agentic-coding advice is “run it in a Docker container so it can’t touch anything you care about.” Docker can’t run macOS, so it can’t run Xcode. That advice doesn’t survive contact with iOS development. You need a different answer, and the answer isn’t pre-built.
The threat model
Before any tooling, the question I had to answer for myself was what I was actually defending against. The answer is out-of-repo damage.
Git makes in-repo damage cheap to recover from. A bad commit is a git reset away. A bad file is a checkout away. The blast radius of an agent loose inside a single repo, with git, is small and bounded. The blast radius of an agent loose on the rest of the filesystem is not. SSH keys, cloud credentials, unrelated client repos, personal files, a thousand things git has no opinion about. Sandboxing extends git’s safety net to the parts of the disk git doesn’t cover.
I haven’t had an incident. Nothing I’m describing here is a reaction to losing files or leaking secrets. It’s defense in depth. The reason to set it up before something happens is that after something happens, the question isn’t “should I have sandboxed” but “which SSH key do I now need to rotate.”
Why Docker doesn’t work, and what does
Docker doesn’t run macOS, so it can’t run Xcode, so it can’t validate iOS code. End of that path.
The tool that does work, as a starting point, is agent-safehouse.dev. It’s a macOS-native wrapper around Apple’s own sandbox-exec, with a composable profile system and a deny-by-default posture. If you want to hand an agent restricted filesystem and network access on a Mac, it’s the right starting point and I’d recommend setting it up that way before doing anything more ambitious.
I should be honest, though: I’m not running it out of the box. I read the profiles, used them as a structural reference, and rolled my own. The file is about five hundred lines of sandbox-exec policy, tuned to my projects. That’s more representative of what a working dev actually ends up doing than “I installed the thing and it worked.” The off-the-shelf profile gets you most of the way; the last mile is project-specific.
The nested-sandbox gotcha
The one excerpt from my profile worth showing is the rule that took the longest to get right. SwiftPM, when it compiles package manifests, invokes sandbox-exec itself. An agent running inside a sandbox-exec policy that tries to invoke a nested sandbox-exec fails in a way that doesn’t obviously look like a sandboxing problem. The fix is two halves: allow the nested process to start un-sandboxed, and tell the Swift driver not to add its own sandboxing layer on top.
;; ---------------------------------------------------------------------------
;; Nested sandbox support: allow sandbox-exec to escape the outer sandbox.
;;
;; xcodebuild's SPM integration spawns sandbox-exec to sandbox manifest
;; compilation. macOS forbids nested sandbox_apply(), so the child must
;; start un-sandboxed. sandbox-exec is a trusted system binary that
;; immediately applies its own restrictive profile to the actual command.
;; ---------------------------------------------------------------------------
(allow process-exec (with no-sandbox)
(literal "/usr/bin/sandbox-exec"))
The companion fix lives back in the validation script, in the OTHER_SWIFT_FLAGS='$(inherited) -disable-sandbox' value on both xcodebuild invocations. That flag isn’t disabling my sandbox. It’s telling the Swift driver not to wrap its build subprocesses in their own sandbox, which would otherwise stack a second layer on top of the outer one and trip the same nested-sandbox_apply problem from a different direction. The two changes only make sense together, which is why I flagged the flag earlier and waited until here to explain it.
Death by a thousand cuts
Beyond the nested-sandbox rule, most of the profile is Xcode-specific opt-ins. I’m not going to publish the full file, but the shape is worth describing because it conveys what “make Xcode work under a deny-all sandbox” actually costs.
There’s a filesystem subpath allow for CoreSimulator, because the sim writes there. There are mach-service regexes (^com\.apple\.CoreSimulator\. and ^com\.apple\.dt\.) instead of literal names, because Apple renames mach services between Xcode versions and a literal-match policy turns into whack-a-mole. There are IOKit user-client allows for the simulator’s GPU path, IOSurfaceRootUserClient and AGXDeviceUserClient among them, without which views render but snapshot tests fail in ways that look like Metal bugs. Distributed notifications. Job creation. Signal delivery. CFPrefsd preference domains for half a dozen Apple bundle identifiers. About a hundred lines of this, accreted over weeks, each one earned by an opaque failure under the previous policy.
The point of listing them isn’t to teach the rules. The point is that an honest iOS sandboxing setup is not a weekend project. It’s a long tail of “Xcode also needs this,” discovered one failure at a time.
What it isn’t
Agent-safehouse, and the custom profile on top of it, is not a VM boundary. It restricts filesystem and mach and IOKit access. It doesn’t stop the network exfiltration of files the policy has been told the agent is allowed to read. It doesn’t stop a sandbox escape, if there is one. The threat model it covers is accidental damage from an agent operating in good faith on the wrong file. It is not the threat model where an attacker controls the agent.
That’s worth saying out loud because the alternative is a post that oversells, and the reader who acts on an oversold post is the one who gets hurt.
Where this leaves the iOS playbook
Right now, the iOS-flavored version of agentic coding costs you the inner loop, costs you the outer loop, and costs you a sandboxing setup the rest of the field doesn’t have to build. None of those are fatal. They reshape what’s worth automating and what isn’t. SwiftUI ends up out of TDD. Slices end up narrower. The agent’s run is something you batch and walk away from rather than something you watch.
What’s still unsolved on my end is the outer review loop. The inner loop has concessions that work. The sandbox has a long file that works. The outer loop, where I open the simulator and click around, is still slow, and that’s the loop post 2’s skimming smudge sits on top of, because skimming a PR on iOS saves you more time per PR than skimming a PR on web, which is exactly the situation in which holding the line is hardest and matters most.
I’ll keep watching that one.