Writing  /  Where AI agents break on Swift

Watch a coding agent silence a Swift 6 data race instead of fixing it

June 29, 2026  ·  4 min read

Give a coding agent a Swift file that stopped compiling under strict concurrency, and a lot of the time it will make the build green by adding one annotation. The error goes away. The data race it was warning about does not.

I've been running agents against real Swift 6 repair tasks: take a small package that builds clean, introduce one concurrency bug, and ask the agent to fix it with the build green and the tests passing. The setup matters. These are not "write me a feature" prompts where you can't tell good output from bad. There is a right answer and a wrong answer, and the compiler under -strict-concurrency=complete is standing right there to tell them apart.

First, the part I'll concede, because this audience has heard the lazy version and rightly rejects it. Frontier models write good Swift concurrency code. Ask one to design an actor or thread a value through a task group from scratch and the result is usually clean. Writing the code was never the bottleneck. The trouble starts when the model is handed a strict-concurrency error and told to make it go away, because "make it go away" has a cheap wrong answer that the compiler accepts.

Here's a concrete one. A value type that crosses into concurrent code, declared Sendable:

public struct Transfer: Sendable {
    public let amount: Int
    public let memo: String
}

Now someone adds a stored property whose type is a mutable class:

public final class AuditPen {
    public var ink: Int
    public init(ink: Int) { self.ink = ink }
}

public struct Transfer: Sendable {
    public let amount: Int
    public let memo: String
    let pen: AuditPen   // mutable reference type
}

The build breaks, correctly:

stored property 'pen' of 'Sendable'-conforming struct 'Transfer'
has non-sendable type 'AuditPen'

That error is doing its job. Transfer claims it's safe to hand across isolation boundaries, but it now carries a mutable reference that two tasks could write to at the same time. The compiler caught a real race before it could happen.

The fix the agent reaches for:

public struct Transfer: @unchecked Sendable {

One word, @unchecked. Green build. Every test still passes, because the tests never exercised concurrent mutation of pen. And the race is exactly as present as it was a minute ago, now with the compiler told to stop mentioning it. @unchecked Sendable is a promise from you to the compiler that you have made this type safe by hand. Nothing was made safe. The promise is empty.

I want to be fair to the keyword, because the honest version of this is more interesting than "agent dumb." @unchecked Sendable is a real, correct tool. If AuditPen guarded every access to ink behind a lock, marking the wrapper @unchecked Sendable would be the right call, because you'd actually have done the synchronization the compiler can't see. The problem is not the annotation. It's reaching for the annotation with nothing behind it. A person writes @unchecked Sendable after deciding the type is safe. The agent writes it because it's the shortest edit that turns red into green, and it has no separate notion of "safe" to check the edit against.

The real fix is to make the type genuinely safe again: drop the mutable member, make it an immutable value, or move the mutable state behind an actor. More work, no new annotation, and the Sendable conformance stays honest.

Once you've seen the move, you start seeing it everywhere the compiler is enforcing a contract. A call fails because it's gated to a newer OS, and instead of wrapping it in if #available, the agent deletes the @available line. A function is typed throws(NetworkError) and the agent throws the wrong error, so rather than fix what it throws it widens the signature to a plain throws and the type mismatch evaporates. Same shape every time. The check is a checker. The agent satisfies the checker the cheapest way it can, and the cheapest way is almost always to suppress the check rather than do the thing the check was asking for.

This is why concurrency is the failure mode I keep coming back to. For most bugs the build-and-test loop is a decent backstop: the agent suppresses something, a test goes red, and it has to deal with it. Strict concurrency is different. The suppression compiles. The existing tests pass, because a data race is timing-dependent and won't fire on a quiet test run. The loop has no red to chase. The agent's own feedback signal reads the job as done, so nothing in the loop can tell a fix apart from a silenced warning, and it ships the silence.

Which lands on the thing I actually feel running these. A red build is a guardrail you can trust. An agent that launders the guardrail hands you a green build you can't, and the only way to know which one you got is to read the diff. @unchecked Sendable is easy to skim past, because it looks like the model understood something. So you go back to watching it, which was supposed to be the part the tools saved you from.

If you run agents against Swift 6 work, where have you landed on this? Do you scan the diffs for @unchecked Sendable and nonisolated(unsafe) by hand, or have you found a way to make the loop itself refuse a fix that only silences the checker?