Swift Concurrency skill for Codex

Swift 6 strict concurrency without unchecked escapes. Codex migrates Swift code to strict concurrency one target at a time, choosing MainActor, actors, Sendable values or lock-protected types by rule rather than sprinkling @unchecked Sendable. It handles Swift 6.2 default isolation and @concurrent, accounts for actor reentrancy, gives every Task an owner that cancels it, bridges callbacks with continuations resumed exactly once, and confirms with Thread Sanitizer.

  • Languages
  • $swift-concurrency
  • 187 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- swift-concurrency

Installs to ~/.agents/skills/swift-concurrency/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $swift-concurrency, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and migrates Swift concurrency code - async/await, structured task groups, actors and MainActor isolation, Sendable checking, cancellation, AsyncStream and continuations for callback APIs - and fixes Swift 6 strict concurrency errors such as data races on non-Sendable values and main actor-isolated access from nonisolated contexts, including Swift 6.2 default MainActor isolation and @concurrent. Use when enabling the Swift 6 language mode, when Xcode shows Sendable or actor isolation errors, when bridging completion handlers to async, or when a Task leaks or ignores cancellation.

Use it when

  • Enabling strict concurrency checking or the Swift 6 language mode
  • Xcode reports Sendable, data race or actor isolation diagnostics
  • Converting completion handlers or delegates to async/await
  • Tasks keep running after a screen closes or ignore cancellation

Use something else when

How to check the result

The target builds in Swift 6 mode, swift test --sanitize=thread is clean across repeated runs, and every @unchecked Sendable or nonisolated(unsafe) in the diff has a written justification.

The mistake it prevents

Silencing Sendable errors with @unchecked Sendable or nonisolated(unsafe) on types that have no lock, which hides exactly the data races Swift 6 was reporting.

~/.agents/skills/swift-concurrency/SKILL.md

--- name: swift-concurrency description: Writes and migrates Swift concurrency code - async/await, structured task groups, actors and MainActor isolation, Sendable checking, cancellation,… license: MIT ---

Done well means: the module builds in the Swift 6 language mode (or with complete strict checking) without @unchecked Sendable or nonisolated(unsafe) added just to silence errors, UI state is isolated to @MainActor, shared mutable state lives in an actor or a lock-protected type with a stated reason, and every unstructured Task has an owner that cancels it.

Find the current settings first

grep -n 'swift-tools-version\|swiftLanguageMode\|swiftLanguageModes\|enableUpcomingFeature\|defaultIsolation' Package.swift
grep -n 'SWIFT_VERSION\|SWIFT_STRICT_CONCURRENCY\|SWIFT_DEFAULT_ACTOR_ISOLATION\|SWIFT_APPROACHABLE_CONCURRENCY' *.xcodeproj/project.pbxproj
swift --version

Read AGENTS.md for the minimum deployment target: Mutex (Synchronization module) needs iOS 18 / macOS 15; actors and async/await back-deploy to iOS 13.

Migration path

- [ ] 1. Swift 5 mode + strict concurrency "targeted", fix warnings module by module
- [ ] 2. Strict concurrency "complete"; fix warnings starting from leaf modules
- [ ] 3. Decide default isolation: app/UI targets -> MainActor default (Swift 6.2+); libraries -> nonisolated
- [ ] 4. Switch the module to Swift 6 language mode; warnings become errors
- [ ] 5. Run tests with Thread Sanitizer; remove any remaining @unchecked that lacks a proof

SwiftPM settings:

// swift-tools-version: 6.2
.target(
    name: "App",
    swiftSettings: [
        .defaultIsolation(MainActor.self),                     // app/UI code only
        .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
    ]
)
// Swift 5 mode during migration: .swiftLanguageMode(.v5) plus
// .enableUpcomingFeature("StrictConcurrency")

Xcode 26: Build Settings “Default Actor Isolation” = MainActor, “Approachable Concurrency” = Yes, “Swift Language Version” = 6. Migrate one target at a time.

Isolation decisions

Kind of codeIsolation
Views, view models, anything touching UIKit/AppKit/SwiftUI state@MainActor (or the target’s default)
Shared mutable state accessed from many tasks (caches, session store)actor
Hot, tiny critical sections, or synchronous API requiredfinal class + Mutex (iOS 18+) or OSAllocatedUnfairLock, marked Sendable
Pure data passed between tasksstruct/enum with Sendable members (implicitly Sendable when internal)
CPU-heavy work that must leave the main actor@concurrent async function (Swift 6.2+) or nonisolated async on older compilers
Immutable reference typefinal class with only let Sendable properties, declared Sendable
actor ImageCache {
    private var store: [URL: Image] = [:]
    private var inFlight: [URL: Task<Image, Error>] = [:]

    func image(for url: URL, loader: @Sendable @escaping (URL) async throws -> Image) async throws -> Image {
        if let cached = store[url] { return cached }
        if let task = inFlight[url] { return try await task.value }   // dedupe across reentrancy
        let task = Task { try await loader(url) }
        inFlight[url] = task
        defer { inFlight[url] = nil }
        let image = try await task.value
        store[url] = image
        return image
    }
}

Actor reentrancy: state can change across every await inside an actor method. Re-check assumptions after each suspension and record in-flight work (as above) instead of assuming “nothing else ran”.

Fixing common diagnostics

Diagnostic (abridged)Fix
Sending ‘x’ risks causing data racesMake the type Sendable, pass a copy, or mark the parameter sending when ownership transfers
Main actor-isolated property ‘x’ can not be referenced from a nonisolated contextMark the caller @MainActor, await MainActor.run { }, or make the property nonisolated if it is immutable
Type ‘X’ does not conform to the ‘Sendable’ protocolValue type with Sendable fields, final class with lets, or an actor; @unchecked Sendable only with an internal lock and a comment
Static property ‘shared’ is not concurrency-safestatic let of a Sendable type, isolate it to @MainActor, or make the type an actor
Capture of ‘self’ with non-sendable type in a @Sendable closureIsolate the class to an actor, or capture the specific Sendable values instead of self
Call to main actor-isolated initializer in a synchronous nonisolated contextMake the calling function @MainActor or async
Warnings from a dependency that is not yet audited@preconcurrency import ThatModule (temporary; note it in the report)

nonisolated(unsafe) and @unchecked Sendable move race detection from the compiler to you. Each use needs a comment naming the lock or invariant that makes it safe.

Tasks and cancellation

NeedUse
Fixed parallel work with resultsasync let a = f(); async let b = g(); try await (a, b)
Dynamic number of childrenwithThrowingTaskGroup(of:); withDiscardingTaskGroup for fire-and-forget children
Start async work from sync UI codeTask { } stored in a property and cancelled in deinit/onDisappear; SwiftUI .task { } cancels automatically
Work that must not inherit actor or priorityTask.detached (rarely needed)
func thumbnails(for urls: [URL]) async throws -> [URL: Image] {
    try await withThrowingTaskGroup(of: (URL, Image).self) { group in
        for url in urls.prefix(100) {
            group.addTask { (url, try await render(url)) }
        }
        var result: [URL: Image] = [:]
        for try await (url, image) in group { result[url] = image }
        return result
    }
}
  • Cancellation is cooperative: loops call try Task.checkCancellation() or check Task.isCancelled. URLSession async APIs already respect it.
  • withTaskCancellationHandler cancels underlying non-async work (a callback request).
  • Limit group width for large inputs: add tasks up to N, then add one more each time one finishes.

Bridging callback APIs

func currentLocation() async throws -> CLLocation {
    try await withCheckedThrowingContinuation { continuation in
        locator.requestOnce { result in continuation.resume(with: result) }   // exactly once
    }
}

func events() -> AsyncStream<Event> {
    let (stream, continuation) = AsyncStream.makeStream(of: Event.self, bufferingPolicy: .bufferingNewest(100))
    let token = bus.subscribe { continuation.yield($0) }
    continuation.onTermination = { _ in bus.unsubscribe(token) }
    return stream
}

Resume a continuation exactly once on every path, including errors and cancellation; the checked variant traps on double resume and logs a leak if never resumed.

Gotchas

  • Task { } inherits the current actor; inside a @MainActor view model it runs on the main actor. Heavy synchronous work inside it still blocks the UI; move it to an @concurrent or nonisolated function.
  • With NonisolatedNonsendingByDefault (Swift 6.2 approachable concurrency), nonisolated async functions run on the caller’s actor instead of the global executor. Code that relied on hopping off the main actor needs @concurrent.
  • With default MainActor isolation, every unannotated type and global in the target is main-actor isolated. Types used from @Sendable closures (onTermination, callbacks, background services) need an explicit nonisolated on the declaration.
  • DispatchQueue.main.async and DispatchSemaphore.wait() inside async code mix models and can deadlock; never block a cooperative thread waiting on async work.
  • MainActor.assumeIsolated crashes if the assumption is wrong; use it only in callbacks documented to arrive on the main thread.
  • Global and static mutable vars are errors in Swift 6; most should become let, actor-isolated, or injected.

Verification

swift build -Xswiftc -strict-concurrency=complete     # during migration
swift test --sanitize=thread
xcodebuild test -scheme App -destination 'platform=iOS Simulator,name=iPhone 17' -enableThreadSanitizer YES

Grep the diff for @unchecked Sendable, nonisolated(unsafe) and @preconcurrency; each must have a justification. Re-run tests several times; TSan reports are real bugs.

Report template

**Modules migrated:** Networking (Swift 6), Models (Swift 6), App (Swift 6, default MainActor)
**Diagnostics fixed:** 64 (41 Sendable, 17 isolation, 6 global state)
**Escape hatches:** 1 @unchecked Sendable (TokenStore, guarded by Mutex), 1 @preconcurrency import LegacySDK
**Tasks:** 3 unowned Task {} now stored and cancelled on disappear
**Checks:** swift test --sanitize=thread clean x3; app smoke test on simulator

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Swift Concurrency"
  short_description: "Swift 6 strict concurrency without unchecked escapes"
  default_prompt: "$swift-concurrency Turn on Swift 6 language mode for our networking module and fix the errors properly"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem