AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Swift Concurrency Review

skill-stuartshields-claude-setup-swift-concurrency-review · by stuartshields

Use when reviewing Swift 5.5+ code containing `await`, `actor`, `Task { }`, `@MainActor`, `@unchecked Sendable`, cancellable timer/deadline handles, or long-lived `for await` AsyncStream consumers — especially diffs extending existing actors with new methods. Catches post-`await` state-overwrite races, TOCTOU around Task spawn, cancellation-not-honoured, terminal-state clobber, and consumer-Task-…

No reviews yet
0 installs
9 views
0.0% view→install

Install

$ agentstack add skill-stuartshields-claude-setup-swift-concurrency-review

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-stuartshields-claude-setup-swift-concurrency-review)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Swift Concurrency Review? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Swift Concurrency Review

Overview

Swift 6 strict concurrency catches data races at compile time but lets a class of semantic bugs through: state-overwrite races, TOCTOU around Task { }, and cancellation that doesn't actually prevent firing. This is a reviewer's checklist for those bug classes. Apply on any diff touching actor, @MainActor, await, Task {, lock-protected state, or deadline timers.

The Five Questions

1. After every await, is state re-checked — not just the index?

When an actor method suspends at await, any other method on the actor can run to completion before it resumes. Re-finding a record by id ≠ verifying its state is still what you expected.

// BAD
try await transport.send(message)
guard let idx = outbound.firstIndex(where: { $0.id == id }) else { return }
outbound[idx].direction = .delivered   // overwrites .cancelled / .deadlineMissed

// GOOD
try await transport.send(message)
guard let idx = outbound.firstIndex(where: { $0.id == id }),
      outbound[idx].direction == .sending else { return }
outbound[idx].direction = .delivered

Invariant: terminal states are sticky. Once a record is .cancelled / .expired / .deadlineMissed, no later success path may overwrite it. The guard encodes this invariant.

2. Is Task { } body reading mutable state, or is it snapshotted at spawn?

A Task { } body runs later, on some executor. Reading clock.now, self.count, or any mutable state inside the closure is a TOCTOU — the value is read at an arbitrary later moment, not at spawn.

// BAD — clock.now read inside the Task
task = Task { [weak self] in
    let seconds = max(0, deadline.timeIntervalSince(clock.now))
    try? await clock.sleep(for: seconds)
}

// GOOD — clock.now read before Task exists, captured as constant
let seconds = max(0, deadline.timeIntervalSince(clock.now))
task = Task { [weak self] in
    try? await clock.sleep(for: seconds)
}

3. If cancellable, does cancel() actually prevent firing?

Task.cancel() signals cancellation; it does not stop a running task. try? await clock.sleep(for:) swallows CancellationError and falls through. A handle that looks cancellable can still fire after cancel().

Fix options (at least one must exist and have a test):

  • Flip a fired: Bool / cancelled: Bool under the lock in cancel(), check it after sleep
  • Check Task.isCancelled explicitly after the sleep
  • Use try await (not try?) and let CancellationError propagate

4. Does fire-and-forget Task { } manage the lifetime of what it captures?

A detached Task { } that captures self strongly or any SomeProtocol and runs after the owner tears down is a leak or a crash. Use [weak self] + early return, or await on a parent task that's guaranteed to complete.

5. Is a long-lived for await consumer Task owned by something whose deinit finishes the source?

A Task { for await event in stream { ... } } stored on an actor does NOT die deterministically when the actor goes out of scope. [weak self] only nils the reference inside the loop body — not while the task is suspended at for await, which is most of the task's life. AsyncStream's auto-finish-on-continuation-dealloc is non-deterministic, and Swift Testing waits for all background Tasks before exiting — symptom: swift test prints all "passed" lines, then hangs at exit before the run summary.

The fix is structural: park the Task on a non-actor class that the actor strong-references. When the actor deinits, the class deinits, and the class's deinit runs synchronously on the deallocating thread — finishing the stream (so the for await returns nil) and cancelling the Task (belt-and-suspenders for a body that's mid-event).

// BAD — Task stored on actor, not deterministically torn down
actor MyActor {
    private var consumerTask: Task?
    init(stream: AsyncStream) {
        consumerTask = Task { [weak self] in
            for await event in stream {
                guard let self else { return }
                await self.process(event)
            }
        }
    }
}

// GOOD — Task owned by a non-actor class with synchronous deinit
final class ConsumerBox: @unchecked Sendable {
    let stream: AsyncStream
    private let cont: AsyncStream.Continuation
    private var consumerTask: Task?

    init() {
        var captured: AsyncStream.Continuation!
        self.stream = AsyncStream { captured = $0 }
        self.cont = captured
    }

    deinit {
        cont.finish()             // 1. wake the consumer, return nil from `for await`
        consumerTask?.cancel()    // 2. belt-and-suspenders for mid-await body
    }

    func bind(to actor: MyActor) {
        let s = stream
        consumerTask = Task { [weak actor] in
            for await event in s {
                guard let actor else { return }
                await actor.process(event)
            }
        }
    }
}

actor MyActor {
    private let box = ConsumerBox()
    init() { box.bind(to: self) }
}

The non-actor class wrapper is load-bearing — actor deinit runs in a separate isolation context and doesn't synchronously tear down the Task chain.

Review Checklist

Flag any violation as at least a Major:

  • [ ] Every await is followed by a state re-check before mutating — not just a re-find by id
  • [ ] Terminal-state guards are mirrored across methods that can transition the same record (if send guards against .cancelled, cancel must also guard against .delivered)
  • [ ] Values used inside Task { } closures are captured at spawn time, not read inside
  • [ ] Every cancel() / teardown() has a test proving the handle does not fire after cancellation
  • [ ] try? around await is deliberate and justified — not a paper over a cancellation bug
  • [ ] [weak self] or explicit lifecycle management on every fire-and-forget Task { } that outlives the caller
  • [ ] Long-lived for await consumer Tasks are owned by a non-actor class whose deinit finishes the source stream and cancels the Task — NOT stored as a property on the actor itself
  • [ ] @unchecked Sendable has a code comment naming the synchronisation primitive that makes it safe

Common Mistakes

| Pattern | Why it's wrong | Fix | |---|---|---| | Re-find by id after await without checking state | Actor may have transitioned the record mid-await | Check state == expected before mutating | | clock.now / mutable state inside Task { } closure | Read happens at arbitrary future moment | Compute outside Task, capture as constant | | try? await sleep(...) with no post-sleep cancel check | Task continues to fire after cancel() | Task.isCancelled check or fired flag | | Task { foo() } without [weak self] | Strong self capture outlives owner | [weak self] + early return | | Task { for await event in stream { ... } } stored on an actor | Actor deinit doesn't synchronously tear down the Task; Swift Testing hangs at exit | Move the consumer to a non-actor class whose deinit finishes the source + cancels the Task | | @unchecked Sendable with no explanation | The escape hatch is load-bearing; nobody can verify | Add comment naming the lock / invariant |

Red Flags

These patterns in a diff mean stop and apply the four questions:

  • guard let idx = X.firstIndex(...) followed directly by a mutation, after an await
  • Task { [weak self] in or Task { in with a body that reads .now / .count / any stored prop
  • try? before await on a Clock/Sleep call
  • @unchecked Sendable with no comment
  • A cancel() that only calls task?.cancel() with no state flag
  • An actor with a stored Task? property that runs for await over an AsyncStream — especially if the surrounding test suite hangs at exit

Real-World Impact

Applied to bt-note's NoteStore.send: caught a terminal-state clobber where .deadlineMissed was overwritten by .delivered when the deadline fired during the transport's await. Applied to DeliveryTimerHandle.start: caught a clock.now TOCTOU that made the TestClock-based tests racy. Applied to BLETransport's consumer Task: caught the actor-stored for await Task that hung Swift Testing at exit — fix was to move it onto a non-actor DelegateBox class with a synchronous deinit.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.