NOASSERTIONupdated 1mo ago
Let be the absolute plugin directory two levels above this SKILL.md. Resolve that path before running commands or opening shared references. When these instructions say swiftui-ctx, invoke /scripts/swiftui-ctx; do not assume the command is on PATH.
What can you do with Audit Swiftui Concurrency Safety?
name: audit-swiftui-concurrency-safety description: Audit macOS SwiftUI concurrency safety for correctness, current APIs, and production conventions. Use for that domain or as part of a full audit.
Bundled resource root
Let <swiftui-plugin-root> be the absolute plugin directory two levels above this SKILL.md. Resolve that path before running commands or opening shared references. When these instructions say swiftui-ctx, invoke <swiftui-plugin-root>/scripts/swiftui-ctx; do not assume the command is on PATH.
Audit SwiftUI Concurrency Safety
AUDIT-ONLY Β· macOS-only Β· SwiftUI-only. Run this on a finished or in-progress macOS SwiftUI
project to detect β and where certain, fix β every way Swift concurrency goes wrong: non-Sendable
types crossing actor boundaries, @Sendable closures touching main-actor state, the
DispatchQueue.main.async cargo-cult, lifecycle-leaking Tasks, @MainActor spam, the Swift 6.2
opt-in misreads (@concurrent / nonisolated(nonsending) / NonisolatedNonsendingByDefault), and
non-Sendable Transferable payloads. Findings are written in the toolkit's unified schema; the one
mechanical defect (DispatchQueue.main.async) is fixed under the fix-safety protocol. This is never a
from-scratch concurrency generator.
Concurrency is the toolkit's most version-sensitive domain β the rules changed twice in twelve months and most training data predates both changes. This is a CROSS-CUTTING SWEEP: it owns the isolation verdict wherever concurrency hazards appear, and routes the lifecycle / model / bridge fix to the owning sibling.
Two eras β keep them apart or every fix is wrong
- Swift 6 language mode (Sept 2024) = strict DATA-RACE-SAFETY CHECKING by default. Old warnings
(non-
Sendablecrossing an actor, main-actor state in a@Sendableclosure) are now hard errors. The default is checking, not isolation. Opt-in per target (SWIFT_VERSION = 6/swiftLanguageMode(.v6)) β projects stay on their declared mode until bumped. - Swift 6.2 (Sept 15 2025) = an OPT-IN "main actor by default" build mode
(
-default-isolation MainActor, surfaced as Approachable Concurrency + Default Actor Isolation = Main Actor). A setting you turn on, NOT the unconditional default. AI conflates the two β@MainActorspam OR code that assumes isolation it never enabled.
Record which mode the target is in (ORIENT, step 1) β half the findings depend on it. Toolchain
facts (@concurrent, nonisolated(nonsending), -default-isolation MainActor, the
NonisolatedNonsendingByDefault flag) are Swift 6.2+ and verified against swift.org / Swift
Evolution, not swiftui-ctx.
Boundary / seam note (stay in lane)
This skill owns the isolation VERDICT; the fix shape often belongs to a sibling. Emit a cross_ref
on every shared-seam finding (targets + primary-owner verdicts derive from
<swiftui-plugin-root>/references/_shared/cross-ref-graph.md β do not restate them):
Taskin.onAppear/.onChange(conc-04, conc-10):async-dataowns the lifecycle fix (.task/.task(id:)). THIS skill owns the verdict only when an isolation hazard is present (a non-Sendablecapture, an off-actor mutation).cross_ref: audit-swiftui-async-data.- SwiftData
@Modelmutation off-context (conc-11):swiftdataprescribes@ModelActor; this skill flags the race and routes.cross_ref: audit-swiftui-swiftdata. loadTransferable/TransferableSendable race (conc-09): THIS skill owns Sendable correctness (primary);sandbox-filesowns consent/bookmark.cross_ref: audit-swiftui-sandbox-files.- AppKit
Coordinator/NSViewRepresentableboundary:appkit-overuseowns whether the bridge exists,appkit-interopowns how. This skill flags only the Sendable/isolation hazard at the boundary.cross_ref: audit-swiftui-appkit-interop. @MainActoron an@Observable(conc-06):state-observationowns model-correctness; this skill owns the isolation angle.cross_ref: audit-swiftui-state-observation.
Defect index (conc-01 β¦ conc-11)
id Β· tell Β· severity Β· fix Β· open reference. Severities: hard-fail (errors under the Swift 6
language mode / never-correct), warning (compiles but unsafe / non-native), advisory (judgment
/ toolchain-gated). auto = mechanical single-answer fix; flag = show the β
, dev applies.
| id | One-line tell | Sev | Fix | Reference |
|---|---|---|---|---|
| conc-01 | Task.detached { β¦ } carrying a non-Sendable class / ModelContext / NSView across the boundary |
warning | flag | strict-checking-and-sendable.md |
| conc-02 | @Sendable closure body reads self. / a @MainActor property ("can not be referenced from a Sendable closure") |
warning | flag | strict-checking-and-sendable.md |
| conc-03 | DispatchQueue.main.async inside async / SwiftUI code (GCD cargo-cult) |
warning | auto | main-actor-hops.md |
| conc-04 | bare Task { } in .onAppear / .onChange β not lifecycle-bound, not cancelled |
warning | flag | main-actor-hops.md |
| conc-05 | @MainActor on a pure-value struct/enum/free func (isolation spam) |
advisory | flag | isolation-modes-and-execution.md |
| conc-06 | @Observable UI type with no @MainActor while assuming "6.2 does it" |
advisory | flag | isolation-modes-and-execution.md |
| conc-07 | @concurrent present β confirm the target is Swift 6.2+ |
advisory | flag | isolation-modes-and-execution.md |
| conc-08 | plain nonisolated β¦ func β¦ async assumed to run in the caller's context "because 6.2" |
advisory | flag | isolation-modes-and-execution.md |
| conc-09 | Transferable / loadTransferable payload wraps a reference / non-Sendable type |
warning | flag | strict-checking-and-sendable.md |
| conc-10 | rapid post-await writes (selection/refresh) with no cancel + generation guard β stale overwrite |
advisory | flag | main-actor-hops.md |
| conc-11 | @Model / modelContext mutation inside Task.detached (off-context race) |
warning | flag | strict-checking-and-sendable.md |
UNVERIFIED / toolchain-gated β carry as advisory and never assert as Swift-6.0/6.1 fact (each
becomes source: verify against Xcode 26 SDK): everything that needs Swift 6.2+ β
@concurrent (conc-07), nonisolated(nonsending) and the NonisolatedNonsendingByDefault default
flip (conc-08), the -default-isolation MainActor mode (conc-05/06). Whether the target is in either
era is read in ORIENT, not assumed.
The real API, at a glance
Real & era-stable (back-deploy to macOS 10.15+): @MainActor, Sendable, MainActor.run,
Task.detached, sending, @preconcurrency import. .task / .task(id:) is macOS 12.0+; its closure
inherits the caller's isolation via @isolated(any). Swift 6.2+ only (verify the toolchain): @concurrent (ALWAYS the global
executor), nonisolated(nonsending) (ALWAYS the caller's context, SE-0461), -default-isolation MainActor, the NonisolatedNonsendingByDefault upcoming-feature flag, Task(name:).
The trap that is NOT a default: a plain nonisolated async function still hops to the global
executor (SE-0338) β it stays on the caller's context only when NonisolatedNonsendingByDefault
is enabled. @concurrent and nonisolated(nonsending) behave the same with or without the flag.
Floor values are the reconciled truth in <swiftui-plugin-root>/references/_shared/floors-master.md
(read, never restate). There is no hallucinated-API surface here β concurrency defects are isolation
mistakes on real symbols, not invented names.
β Correct β the grounded lifecycle shape (the anchor every fix imitates)
The most common concurrency defect (conc-04) is a bare Task { } in .onAppear β unbound to view
lifetime, never cancelled. The lifecycle-correct shape is .task / .task(id:) (macOS 12.0+,
caller-isolation-inheriting via @isolated(any), auto-cancelled on disappear). This is the real consensus shape, not a
hand-written snippet β verified live via swiftui-ctx lookup task (step 5): .task { } 70% Β·
.task(id:) 29% across the corpus.
// β leaks β never cancelled, re-runs on every re-appear, races a fast-toggling view
.onAppear { Task { activity = isActive ? begin(options, reason) : nil } }
// β
bound to view lifetime, auto-cancelled, re-keyed on input change
.task(id: Tuple3(isActive, options, reason)) {
activity = isActive ? SSApp.beginActivity(options, reason: reason) : nil
}
- Real example (
swiftui-ctx file ex_a1cff2419c --smart):sindresorhus/Gifskiβ https://github.com/sindresorhus/Gifski/blob/7f873856e2acd8b52e6681dee3aec31e6cab23e4/Gifski/Utilities.swift#L5590 - doc: https://sosumi.ai/documentation/swiftui/view/task(name:priority:file:line:_:) (
.taskfloormacOS 12.0+) - Note the seam: the lifecycle fix (
.task) isasync-data's to prescribe β this skill owns the isolation verdict only when the captured value is non-Sendableor main-actor state is mutated off-context. Every β in a written finding is reproduced this way: the consensus shape in## Correct, a permalinkedswiftui-ctx file β¦ --smartexample + thisdoc:in## Source(step 7 FIX).
The 8-step audit workflow (execute verbatim)
- ORIENT.
tree/findthe SwiftUI sources. Read (a) the Swift language mode (SWIFT_VERSIONinproject.pbxproj, orswiftLanguageMode/swift-tools-versioninPackage.swift) and (b) whether the opt-in mode is on (SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor/-default-isolation MainActor/Approachable Concurrency). Both are load-bearing: conc-01/02/09/11 are errors only under the Swift 6 language mode; conc-05/06 flip meaning under the opt-in mode; conc-07/08 require Swift 6.2+. Record theswift_era. - LOCATE. Run the shared hybrid lint runner:
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-concurrency-safety --dir <sources> --json /tmp/conc.json --sarif /tmp/conc.sarif. It runs this skill's tier-1 grep tells (lint/grep-tells.tsv) + tier-2 structural ast-grep rules (lint/ast-grep/*.ymlβ the Task-in-onAppearand off-context-@Modelcontainment rules grep can't express), plus a per-file parse probe, emitting unified JSON + SARIF. Read itsparse_warningsβ a flagged file did not fully parse, so a structural miss can't masquerade as clean; READ those by hand. The runner only LOCATES. Engine + rule-file format + degradation:<swiftui-plugin-root>/references/_shared/lint-architecture.md. - READ. Open every located file in full β actor isolation, closure capture lists, gate scope,
and the
@Sendable-vs-@MainActorinterplay are invisible to grep. Build a per-file inventory: each crossing site + what type crosses + the receiving actor + which era's rule applies. - DETECT. Apply the index. Assign each candidate a confidence; report a finding only at 100%
certainty (a non-
Sendableclass inTask.detached, aDispatchQueue.main.asyncin async code, an@ObservableUI type with no isolation under an OFF opt-in mode). Tagisolation_kind+swift_era. - VERIFY. For anything β€ ~70% confidence (a symbol's existence/floor, "does this run on the caller
or the pool", whether a type is
Sendable), run both evidence sources. (a) Practice βbash <swiftui-plugin-root>/scripts/swiftui-ctx lookup <api> --json(andswiftui-ctx deprecated <api>for a currency rule): read itsconsensus(the canonical shape),recommendedpermalink,introduced_macos, andco_occurs_with; an exit-3 corroborates an invented spelling. (b) Spec β for an API floor / signature confirm via Sosumi (curl -sSL https://sosumi.ai/<apple-path>usingreferences/source-directory.mdfor the path +<swiftui-plugin-root>/references/_shared/sosumi-reference.mdfor the protocol; neverWebFetch developer.apple.com); for a toolchain / isolation-semantics fact (@concurrent,nonisolated(nonsending), the flag, the language-mode default) the spec source is swift.org / Swift Evolution (SE-0338, SE-0461) inreferences/source-directory.md, NOT swiftui-ctx. Cross-checkintroduced_macosagainstfloors-master.md. Promote with the citation or discard; carry toolchain-gated items asadvisorywithsource: verify against Xcode 26 SDK. - REPORT. Write each confirmed finding (output contract below). One finding per file, zero-padded,
ordered. Write the run's
_index.md. - FIX. Apply corrections under the fix-safety protocol
(
<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md): clean-tree gate, findings-first, onlyfix_mode: auto(conc-03DispatchQueue.main.asyncβawait MainActor.run/@MainActor), one conventional commit per finding citing itsrule_id, never weaken a check. The β "Correct" is not a hand-written snippet β it is the swiftui-ctx consensus shape put in## Correct, backed by a real macOS-26 example fetched withbash <swiftui-plugin-root>/scripts/swiftui-ctx file <recommended.id> --smartwhose GitHub permalink (plus the swift.org / Sosumidoc:) goes in## Source. Leaveflag-onlyfindingsopenwith that β in## Correct. - DOUBLE-CHECK. Re-grep each fixed file to confirm the tell no longer matches; record the evidence
in
## Fix applied?. Re-confirm every citation still resolves. If a fix introduced a new tell (aMainActor.runyou added now needs anawait, or a removedTask.detachedleaves a non-Sendablecapture), loop that file back to DETECT.
Confidence gating (load-bearing)
Report a finding only at 100% certainty. Anything β€ ~70% goes to VERIFY (step 5). Whether the
target is in the Swift 6 language mode / opt-in mode is read in ORIENT, never assumed β an
era-dependent finding without a confirmed era is at best advisory. Auto-fix only conc-03; everything
else is fix_mode: flag-only.
Output contract
Inherits the toolkit's unified contract (full schema + body sections + frontmatter keys:
<swiftui-plugin-root>/references/_shared/finding-schema.md β do not restate it). Specialized for this
domain:
- Findings:
swiftui-audits/concurrency-safety/<context>/NN-slug.md(one finding per file, zero-padded, ordered). Per-run index:swiftui-audits/concurrency-safety/_index.md. domain: concurrency-safety.fix_modeisautoonly for conc-03, elseflag-only. Additive fields (perfinding-schema.mdΒ§4 β concurrency-safety owns these):swift_era:(swift6-checking|swift6.2-optin|era-independent) andisolation_kind:(boundary-crossing|sendable-closure|gcd-cargo-cult|lifecycle-task|mainactor-spam|nonisolated-misread|transferable-sendable|modelactor-race). MUST useswift_era, neverera(that is api-currency's).sourceis a swift.org / Swift-Evolution / Apple URL + access date, orverify against Xcode 26 SDK.
Starter <context> folders (file here whenβ¦):
<context> |
File a finding here when⦠|
|---|---|
sendable-crossing/ |
a non-Sendable type crosses an actor (conc-01), or a @Model mutates off-context (conc-11) |
sendable-closures/ |
a @Sendable closure touches main-actor state (conc-02) |
main-actor-hops/ |
a DispatchQueue.main.async cargo-cult or un-batched post-async write (conc-03) |
task-lifecycle/ |
a bare Task in .onAppear/.onChange, or a stale-result race (conc-04, conc-10) |
isolation-modes/ |
@MainActor spam, a missing-isolation @Observable, or an opt-in-mode misread (conc-05, conc-06) |
execution-semantics/ |
@concurrent / nonisolated(nonsending) / nonisolated async caller-context confusion (conc-07, conc-08) |
transferable-sendable/ |
a Transferable / loadTransferable payload isn't Sendable-correct (conc-09) |
New-folder rule: if a finding does not fit any existing context folder, create a new one under
swiftui-audits/concurrency-safety/ with a lowercase-hyphen slug, and note it in the run's _index.md.
Prefer an existing folder when the fit is reasonable; consistency across runs is a hard requirement.
Two runs over the same code produce structurally identical trees.
Reference routing
| File | Open when |
|---|---|
references/strict-checking-and-sendable.md |
a Sendable-crossing, @Sendable-closure, Transferable, or off-context-@Model question β the Swift 6 strict-checking era (conc-01/02/09/11) |
references/main-actor-hops.md |
a GCD cargo-cult, the .task-vs-Task lifecycle, MainActor.run batching, or a stale-result generation guard (conc-03/04/10) |
references/isolation-modes-and-execution.md |
the Swift 6.2 opt-in confusion, @MainActor spam, @concurrent, and the nonisolated(nonsending) / NonisolatedNonsendingByDefault execution-context trap (conc-05/06/07/08) |
references/source-directory.md |
step VERIFY β the swift.org / Swift-Evolution / Apple source map fetched via Sosumi |
lint/grep-tells.tsv + lint/ast-grep/*.yml |
step LOCATE β this skill's declarative lint rule set fed to the shared runner (tier-1 grep tells + tier-2 structural ast-grep); edit here to tune detection |
Shared toolkit references (point in, never restate):
| Shared file | For |
|---|---|
<swiftui-plugin-root>/references/_shared/floors-master.md |
every floor/availability value (the reconciled truth) |
<swiftui-plugin-root>/references/_shared/hallucination-blacklist.md |
the canonical invented-name list |
<swiftui-plugin-root>/references/_shared/finding-schema.md |
the unified finding schema + the swift_era/isolation_kind additive fields |
<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md |
the fix-safety protocol (step 7) |
<swiftui-plugin-root>/references/_shared/sosumi-reference.md |
the Apple-doc spec fetch protocol (step 5 VERIFY) |
<swiftui-plugin-root>/references/_shared/swiftui-ctx-reference.md |
the practice-corpus CLI contract β lookup/deprecated/file --smart for the consensus shape + permalinked example (steps 5 VERIFY Β· 7 FIX) |
<swiftui-plugin-root>/references/_shared/cross-ref-graph.md |
seam ownership + cross_ref targets (async-data Β· swiftdata Β· sandbox-files Β· appkit-interop Β· state-observation) |
<swiftui-plugin-root>/references/_shared/macos-arm-gating.md |
the macOS-arm gating rule (for any availability gate on a 6.2 API) |
Detection accelerator
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-concurrency-safety --dir <files-or-dir> [--json out.json] [--sarif out.sarif] β the toolkit's one shared hybrid lint engine, fed this
skill's declarative rules: tier-1 grep tells (lint/grep-tells.tsv,
conc-01/02/03/04/05/06/07/08/09) + tier-2 ast-grep structural rules (lint/ast-grep/*.yml β
conc-04 Task-in-onAppear containment, conc-11 @Model-mutation-in-Task.detached co-occurrence)
that grep cannot express. conc-10 (a missing generation guard) is intentionally read-only β no
lint tell can prove an absence; READ rapid-trigger sites by hand. It runs a per-file parse probe,
emits unified JSON + SARIF, and degrades to grep-only with a notice if ast-grep is unreachable
(npx --package @ast-grep/cli ast-grep; faster: brew install ast-grep). It only LOCATES β always
READ each hit in full before reporting (step 3). The legacy scripts/conc-lint.sh is a thin pointer to
this runner. Engine + rule-file format + JSON/SARIF shape:
<swiftui-plugin-root>/references/_shared/lint-architecture.md.
Install
Add Audit Swiftui Concurrency Safety to your client. Pick the one you use.
npx skills add yigitkonur/plugin-swiftuiInstalls every skill in the repository, then prompts for which to keep.
/plugin marketplace add yigitkonur/plugin-swiftuiAdds the repository as a plugin marketplace; install individual plugins with `/plugin install`.
git clone https://github.com/yigitkonur/plugin-swiftui
cp -r plugins/swiftui/skills/audit-swiftui-concurrency-safety ~/.claude/skills/A skill is a plain directory. Copy it into `.claude/skills/` in a project or in your home directory.
Score
75 / 100
Good