"Six common 'fixes' AI coding agents make to get a Swift build or test passing, what the real fix looks like, and a pre-commit hook that catches them."
AI coding agents are very good at making a red build go green. They're less reliable at making it go green for the right reason.
We build Phoenix.vu, an AI coding agent for Xcode, so we read a lot of agent-written diffs. The same handful of shortcuts comes up again and again, from every model and every tool, including the agents now built into Xcode. None of them are malicious. The agent was asked to fix an error, and these are the fastest ways to make the error disappear.
Here are six to watch for, with the real fix for each.
1. @unchecked Sendable to silence a concurrency error
The error (Swift 6 strict concurrency):
Capture of 'cache' with non-sendable type 'ImageCache' in a '@Sendable' closure
The shortcut:
final class ImageCache: @unchecked Sendable {
private var storage: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? { storage[url] }
func insert(_ image: UIImage, for url: URL) { storage[url] = image }
}
It compiles. It also tells the compiler "trust me, this is thread-safe" when it isn't. You still have a data race on storage. The compiler has just stopped warning you about it.
The real fix: make the shared state safe. An actor is the simplest option:
actor ImageCache {
private var storage: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? { storage[url] }
func insert(_ image: UIImage, for url: URL) { storage[url] = image }
}
// Callers now await:
let cached = await cache.image(for: url)
@unchecked Sendable has legitimate uses, for example a type protected by a lock. But if the diff adds it without adding a lock, ask why.
2. nonisolated(unsafe) on global state
The error:
Static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state
The shortcut:
final class Settings {
nonisolated(unsafe) static var shared = Settings()
var theme: Theme = .system
}
The real fix: decide who owns this state. If it never needs to change, make it a let of a Sendable type. If the UI owns it, isolate it to the main actor:
@MainActor
final class Settings {
static let shared = Settings()
var theme: Theme = .system
}
3. Force unwraps to get past optionals
The error:
Value of optional type 'URL?' must be unwrapped to a value of type 'URL'
The shortcut:
let url = URL(string: endpoint)!
The build passes, and the app crashes the first time endpoint is malformed.
The real fix:
guard let url = URL(string: endpoint) else {
throw APIError.invalidURL(endpoint)
}
4. try? that throws away the error
The error:
Call can throw but is not marked with 'try'
The shortcut:
let user = try? decoder.decode(User.self, from: data)
Now a decoding failure silently becomes nil, and your UI shows an empty profile with no clue why.
The real fix: let it throw, or handle it on purpose:
do {
let user = try decoder.decode(User.self, from: data)
show(user)
} catch {
logger.error("Failed to decode User: \(error.localizedDescription)")
showError(.couldNotLoadProfile)
}
try? is fine when nil really is an acceptable result. It's a problem when it was added only to make the compiler stop complaining.
5. Disabling the failing test
The failure:
Expectation failed: (order.total → 108.0) == 110.0
The shortcut:
@Test(.disabled("Flaky"))
func totalIncludesTax() {
let order = Order(items: [.init(price: 100)], taxRate: 0.10)
#expect(order.total == 110)
}
Or the test is deleted, or the expected value is changed to match the wrong output. The suite is green, and the bug is now invisible.
The real fix: fix the logic the test is checking. Then, if the test really is flaky, fix the flakiness, for example by removing the dependency on time, the network or ordering.
6. Making a property optional to "fix" decoding
The failure:
keyNotFound(CodingKeys(stringValue: "price", intValue: nil), ...)
The shortcut:
struct Product: Decodable {
let id: String
let name: String
let price: Decimal? // was: let price: Decimal
}
It decodes, and now every screen that uses price has to deal with nil, even though a product without a price is probably a bug in the API response.
The real fix: find out why the key is missing. If the API really does omit it sometimes, decode it explicitly with a documented default:
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(String.self, forKey: .id)
name = try c.decode(String.self, forKey: .name)
price = try c.decodeIfPresent(Decimal.self, forKey: .price) ?? 0 // API omits price for free items
}
A 30-second review checklist
Before you accept an agent's fix, search the diff for:
-
@unchecked Sendableornonisolated(unsafe) - New
!force unwraps,try!oras! - New
try? -
.disabled(,XCTSkip, or removed test functions - Properties that became optional
- Large deletions in a "small" fix
Bonus: a pre-commit hook that flags them
Save this as .git/hooks/pre-commit and make it executable (chmod +x .git/hooks/pre-commit):
#!/bin/sh
# Flags common "make it compile" shortcuts in staged Swift changes.
PATTERNS='@unchecked Sendable|nonisolated\(unsafe\)|try!|as!|\.disabled\(|XCTSkip'
if git diff --cached -U0 -- '*.swift' | grep '^+' | grep -E "$PATTERNS"; then
echo ""
echo "These added lines often hide a real problem. Review them,"
echo "or commit with --no-verify if they're intentional."
exit 1
fi
It won't catch everything, but it catches the most common cases, whoever wrote the code.
Where Phoenix fits
We built Phoenix around this review step. It runs the build-fix loop alongside Xcode, but every change is shown as a diff that you approve or reject before it's applied, and it snapshots each file before writing so you can undo without Git. It's pay-per-task with no subscription, and your code stays on your Mac.
If you already pay for Claude or ChatGPT, Xcode's built-in agents are a good place to start, and everything above applies to them too.
What's the worst "fix" you've seen an AI agent make to get a build passing? Share it in the comments. We'll add the best ones to the checklist.
Spotted something that behaves differently in your Xcode version? Let us know in the comments and we'll update the post.
Top comments (0)