# Swift Verify

> Run the right local verification gate for a Swift project - detect XcodeGen vs plain .xcodeproj vs SwiftPM vs Tuist, then regenerate (if applicable), swiftformat --lint, swiftlint --strict, and xcodebuild test on the cheapest valid destination (macOS for pure-Swift) or swift build/test for SwiftPM. Reports each stage; --fix lets SwiftFormat rewrite.

- Skill: `charlesjones-dev/swift-verify` (Agent Skill)
- Install (CLI): `npx skillmds@latest add charlesjones-dev/swift-verify`
- Raw SKILL.md: https://api.skillmd.com/api/skills/charlesjones-dev/swift-verify/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: charlesjones-dev (https://skillmd.com/u/charlesjones-dev)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/charlesjones-dev/swift-verify

---


# Swift Verify

You run a Swift project's local quality gate, adapting the pipeline to how the project is built. This is the portable analog of a hand-written `scripts/verify.sh`: it discovers the build system and tools, runs them in the order that does not waste work, and reports each stage honestly.

This is the Swift-native specialization of a generic preflight; it does not duplicate ai-workflow's `/workflow-preflight` (Node/Python/.NET/Go/Rust). For release-blocker auditing (entitlements, Xcode Cloud, build numbers) use `/swift-preflight`; for general bug-finding use `/code-review`.

## Arguments

`$ARGUMENTS`:

- `path` - repo root (default cwd).
- `--fix` - allow `swiftformat` to rewrite in place (default is lint-only / non-destructive).
- `--no-test` - skip the build/test stage (format + lint only; good for doc-only changes).
- `--destination=<dest>` - override the `xcodebuild` destination (e.g. `platform=iOS Simulator,name=iPhone 16`).

## Phase 0: Detect build system + tools

- **XcodeGen**: `project.yml` + `command -v xcodegen`.
- **Tuist**: `Project.swift` + `command -v tuist`.
- **Plain Xcode**: `*.xcodeproj` / `*.xcworkspace`, no generator.
- **SwiftPM**: `Package.swift` (library or executable).

Tool availability: `command -v swiftformat swiftlint xcodegen xcodebuild swift`. If a required tool is missing, report it and the install hint (`brew install swiftformat swiftlint xcodegen`) and continue with the stages you can run - do not abort the whole gate for one missing optional tool.

Config presence: `.swiftformat`, `.swiftlint.yml`. If absent, run the tool with defaults but note that the project has no pinned config (so local and CI can diverge).

## Phase 1: Regenerate (generator projects only)

If XcodeGen: `xcodegen generate` (quiet). If Tuist: `tuist generate`.

Why first: newly added `.swift` files are not in the `.xcodeproj` until regen, and SourceKit diagnostics for them are stale. Regenerating makes `xcodebuild` authoritative. On a plain `.xcodeproj` or SwiftPM, skip this stage.

**Caution:** regeneration rewrites the pbxproj from `project.yml`. If the working tree has uncommitted pbxproj hand-edits, warn the user that regen will revert them (this is exactly the drift trap - see `/swift-preflight` Check 1). Do not regen over a dirty pbxproj without noting it.

## Phase 2: Format

- Default / lint mode: `swiftformat --lint .` (reports, exits non-zero on diffs, writes nothing).
- `--fix` mode: `swiftformat .` (rewrites in place).

Run format **before** lint so SwiftLint does not flag issues the formatter would have fixed. Note the known SwiftFormat↔SwiftLint `modifier_order` standoff (`nonisolated(unsafe) private static`): if both fire on the same line, that is the documented conflict, not a real defect - prefer moving the helper to file scope, or a scoped per-line disable pair.

## Phase 3: Lint (strict)

`swiftlint lint --strict --quiet`. Strict = warnings fail the run, matching CI. Report each violation with `file:line`. Do not suggest `// swiftlint:disable` as a fix unless the rule genuinely cannot be satisfied (e.g. an `override class func` that cannot be `static`); even then require a one-line justification comment. Never suggest `--no-verify` or blanket disables.

## Phase 4: Build / test

Pick the **cheapest valid destination**:

- **SwiftPM**: `swift build` then `swift test` (no simulator needed).
- **App / framework with macOS support**: `xcodebuild -scheme <scheme> -destination "platform=macOS,arch=arm64" test`. macOS is cheapest (no simulator boot) and is the right default for pure-Swift logic (models, services, persistence, networking).
- **iOS-only target**: requires a simulator runtime. Use `--destination` if given, else pick an available iOS simulator from `xcrun simctl list devices available`. If no iOS runtime is installed, report it and the non-interactive install path `xcodebuild -downloadPlatform iOS` rather than failing silently; fall back to building (not testing) if no destination is bootable.

For multiplatform unit-test targets generated by XcodeGen, remember the macOS `TEST_HOST` override is required (`$(BUILT_PRODUCTS_DIR)/App.app/Contents/MacOS/App`); if `xcodebuild test` fails to launch the host on macOS, surface that as the likely cause.

Skip this phase entirely if `--no-test`.

## Phase 5: Report

```
Swift Verify — <repo>  (XcodeGen · macOS destination)

xcodegen generate   ok
swiftformat --lint   3 files would change   (run with --fix to rewrite)
swiftlint --strict   2 violations
   App/Services/AuthService.swift:42  force_unwrapping
   App/Views/ReaderView.swift:88      modifier_order
xcodebuild test      52 passed, 0 failed

Result: FAIL (format + lint)
Next: /swift-verify --fix to auto-format, then fix the 2 lint violations.
```

Report the true outcome of every stage, including the exact failing command output (trim to the relevant lines). If a stage was skipped (missing tool, `--no-test`, no bootable destination), say so explicitly - never imply a skipped stage passed. Exit-code semantics: clean = all stages green; any failing stage = overall FAIL.

