DEV Community

Vishnu JP for iBoson Innovations Private Limited

Posted on AI-assisted

The AI agent made my Xcode build green. Here are 6 ways it might have cheated

"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
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

@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
Enter fullscreen mode Exit fullscreen mode

The shortcut:

final class Settings {
    nonisolated(unsafe) static var shared = Settings()
    var theme: Theme = .system
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

3. Force unwraps to get past optionals

The error:

Value of optional type 'URL?' must be unwrapped to a value of type 'URL'
Enter fullscreen mode Exit fullscreen mode

The shortcut:

let url = URL(string: endpoint)!
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

4. try? that throws away the error

The error:

Call can throw but is not marked with 'try'
Enter fullscreen mode Exit fullscreen mode

The shortcut:

let user = try? decoder.decode(User.self, from: data)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The shortcut:

@Test(.disabled("Flaky"))
func totalIncludesTax() {
    let order = Order(items: [.init(price: 100)], taxRate: 0.10)
    #expect(order.total == 110)
}
Enter fullscreen mode Exit fullscreen mode

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), ...)
Enter fullscreen mode Exit fullscreen mode

The shortcut:

struct Product: Decodable {
    let id: String
    let name: String
    let price: Decimal?   // was: let price: Decimal
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

A 30-second review checklist

Before you accept an agent's fix, search the diff for:

  • @unchecked Sendable or nonisolated(unsafe)
  • New ! force unwraps, try! or as!
  • 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
Enter fullscreen mode Exit fullscreen mode

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)