This file provides guidance to AI coding agents (Claude Code, etc.) when working with code in this repository. CLAUDE.md is a symlink to this file.
Less is more. The simplest solution is the best solution. The action hierarchy for every change: Delete > Replace > Add.
- Solve at the owner: Put behavior in the code path that owns or observes it. For fixes, never guard a symptom with a staleness check, initialization flag, skip-first-call branch, or
try/exceptaround broken logic; relocate the trigger and delete the wrong path. For features, extend the existing owner rather than creating a parallel abstraction. - Search and reuse first: Search the whole repository before creating a feature, component, helper, workflow, or utility. Reuse or adapt what exists, consolidate in-scope duplication in the shared owner, and delete duplicate paths. Three similar lines beat a helper nobody else calls.
- Delete and modify existing code before creating new code: Bugfixes are net-negative by default unless deletion and relocation are demonstrably impossible. A new file must first prove it cannot fit cleanly in an existing owner.
- Keep scope minimal: Implement only the simplest complete solution. Avoid impossible-state handling, speculative flags, compatibility shims, policy scaffolding, and unrelated cleanup. Tests are out of scope by default — rely on existing coverage and focused validation; only an uncovered, high-risk regression path justifies minimal new test code.
- Ship zero-regression, production-ready changes: Understand what you remove instead of retaining broken code as insurance. Remove unused imports, functions, types, files, and comments; run relevant cleanup checks; and thoroughly debug and validate the changed owner. Do not break existing features or workflows unless the PR intentionally removes them with evidence.
Review gate: for every addition, the reviewer decides whether deleting or changing existing code would have fixed the problem instead — if it would, that is a blocking finding. A missing or thin PR description is never itself a finding.
NEVER push to main. NEVER force push. Always start work in a new git worktree (git worktree add) on a feature branch and open a PR — never edit the primary checkout directly, it may hold in-flight work.
After opening a PR:
- Wait for the automated PR review and auto-format commit from Ultralytics Actions (
format.yml), then pull and address every finding. - Review the full diff in-session against the Core Principles, performance, and the review gate above, then batch the fixes into one commit and push. After each round of bot or human commits, pull and resume the same reviewer on
<last-reviewed-sha>..HEADplus anything that delta could have invalidated. Repeat until the local head matches the live head. - Hand off or merge only on a clean final pass: one cold full-diff review returning LGTM with no findings, on a head that is still live at merge time.
- Never fight other commits: Ultralytics Actions pushes auto-format and header commits, and multiple users may work on the same PR.
git pull --rebasebefore pushing; never reset or revert commits you did not author. - After the PR merges, clean up: remove local worktrees and branches for it, then
git checkout main && git pull.
# One-time: download the seven nano Core ML models (required by model-backed tests;
# also copies them into YOLOiOSApp/Models/ for the app bundle)
bash scripts/download-models.sh
# Run all package tests (mirrors .github/workflows/ci.yml; get a simulator UDID
# from `xcrun simctl list devices available` — use id=, name= resolves unreliably)
xcodebuild -scheme UltralyticsYOLO -sdk iphonesimulator -derivedDataPath Build/ \
-destination "platform=iOS Simulator,id=<SIMULATOR_UDID>,arch=arm64" \
IPHONEOS_DEPLOYMENT_TARGET=16.0 build test
# Run a single test class or method: append e.g.
# -only-testing:YOLOTests/PlotTests
# -only-testing:YOLOTests/PlotTests/testUltralyticsColorsExist
# Coverage as CI runs it: add `-enableCodeCoverage YES clean` to the command above;
# ci.yml then exports lcov with llvm-cov and filters out camera/UI files before Codecov upload
# Format (what format.yml auto-applies to PRs; no .swift-format config file = defaults)
swift-format --in-place --recursive . # brew install swift-format
npx prettier --write "**/*.{md,yml,json}" # YAML/JSON/Markdown
# Dead-code check (CI `periphery` job, strict; brew install periphery)
periphery scan --project YOLOiOSApp/YOLOiOSApp.xcodeproj --schemes YOLOiOSApp \
--exclude-tests --retain-public --report-include 'Sources/UltralyticsYOLO/**/*.swift' \
--strict -- -destination "platform=iOS Simulator,id=<SIMULATOR_UDID>,arch=arm64"
# Model export env (scripts/export-models.py; needs a sibling ultralytics checkout)
uv venv --python 3.13 .venv && uv pip install -e "../ultralytics[export]"CI (ci.yml) runs two jobs on macos-26: test (build + test + a non-blocking Codecov upload) and periphery (dead-code scan, --strict fails on any unused declaration). Package.swift is pinned to swift-tools-version: 5.10 for CI compatibility — do not raise it.
- Single SPM library target
UltralyticsYOLO(Sources/UltralyticsYOLO/), also published as theUltralyticsYOLOCocoaPod; theultralytics/yolo-flutter-appplugin depends on the pod (pinned< 9.0), so public API breaks there too. Package floor is iOS 13 (with@availablefallbacks) while the main appYOLOiOSApp/targets iOS 16. - Zero third-party dependencies: ZIP extraction of downloaded models is the in-repo
MiniZip.swift(Foundation + Compression only). - Inference flow:
YOLO.swiftfacade (callAsFunctionoverloads for URL/String/UIImage/CIImage/CGImage) →BasePredictorsubclasses (ObjectDetector,Segmenter,SemanticSegmenter,DepthEstimator,Classifier,PoseEstimator,ObbDetector) → VisionVNCoreMLRequest.YOLOView(UIKit, wrapsAVCaptureSession+ overlays) andYOLOCamera(SwiftUI) provide real-time camera UI. - YOLO26 vs YOLO11: model metadata key
nms == "false"marks NMS-free YOLO26 end2end models (detect output[1, 300, 6]xyxy pixel coords, decoded in Swift); defaultrequiresNMS = truekeeps the Core ML NMS path for YOLO11 ([1, 4+nc, 8400]xywh). Always indexMLMultiArrayviastrides. .mlpackagemodels are never committed (gitignored); tests and the app get them from thev8.3.0release assets viascripts/download-models.sh(an Xcode "Download YOLO Models" build phase runs it locally and is skipped on GitHub Actions, where CI runs the script as its own step).- Publishing (
publish.yml, push tomain, runs only when the pushing actor isglenn-jocher): a newMARKETING_VERSIONinYOLOiOSApp/YOLOiOSApp.xcodeproj/project.pbxprojtriggers tagv{version}+ GitHub release +pod trunk push+ a squashedtestflightbranch force-pushed for Xcode Cloud; an unchanged version still ships a TestFlight build.
- License header
// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/licenseon every source file — Ultralytics Actions adds it automatically; don't add or revert it manually. - Formatting is enforced by
format.ymlpushing commits onto PRs (swift-format, Prettier, codespell, Ruff/docformatter for Python) — pull its commits instead of re-formatting locally. - Tests are XCTest in
Tests/YOLOTests; model-backed tests load.mlpackagebundles from test resources (run the download script first) and none hit the live network. - Releases: bump
MARKETING_VERSION(two build configurations inproject.pbxproj) ands.versioninUltralyticsYOLO.podspectogether in the release PR; merging tomainthen auto-tags, releases, and publishes the pod. - Archive app builds auto-bump
CFBundleVersioninYOLOiOSApp/YOLOiOSApp/Info.plist— never commit a stray build-number bump. README.mdandREADME.zh-CN.mdare translations of each other — apply any README change to both.