This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Bushel is a CloudKit demonstration project that syncs macOS restore images, Xcode versions, and Swift compiler versions to CloudKit using MistKit. This is an example application designed to showcase MistKit's CloudKit Web Services capabilities, particularly Server-to-Server authentication and batch record operations.
Key Purpose: Tutorial-friendly demo for developers learning CloudKit and MistKit integration patterns.
# Build the project
swift build
# Run the CLI tool
.build/debug/bushel-cloud --help# Sync data to CloudKit (verbose mode recommended)
.build/debug/bushel-cloud sync --verbose
# Dry run (fetch data without uploading)
.build/debug/bushel-cloud sync --dry-run --verbose
# Export CloudKit data to JSON
.build/debug/bushel-cloud export --output data.json --verbose
# Clear all CloudKit data
.build/debug/bushel-cloud clear
# List records in CloudKit
.build/debug/bushel-cloud list
# Check CloudKit status
.build/debug/bushel-cloud statusRequired for CloudKit operations:
export CLOUDKIT_KEY_ID="your-key-id"
export CLOUDKIT_PRIVATE_KEY_PATH="$HOME/.cloudkit/bushel-private-key.pem"
export CLOUDKIT_CONTAINER_ID="iCloud.com.yourcompany.Bushel" # Optional, has defaultOptional for VirtualBuddy TSS signing status:
export VIRTUALBUDDY_API_KEY="your-virtualbuddy-api-key" # Get from https://tss.virtualbuddy.app/Manually trigger the scheduled CloudKit sync workflow:
# Trigger sync workflow on 8-scheduled-job branch
gh workflow run cloudkit-sync.yml --ref 8-scheduled-job
# Or using the API directly
gh api repos/brightdigit/BushelCloud/actions/workflows/cloudkit-sync.yml/dispatches -f ref=8-scheduled-job
# Check status of recent workflow runs
gh run list --workflow=cloudkit-sync.yml --limit 5
# View details of a specific run
gh run view <run-id>
# Watch logs of a running workflow
gh run watch <run-id>Starting with v0.0.1, BushelCloud uses BushelKit as a modular foundation:
BushelKit (Packages/BushelKit/):
BushelFoundation- Core domain models (RestoreImageRecord, XcodeVersionRecord, SwiftVersionRecord)BushelUtilities- Shared utilities (FormattingHelpers, JSONDecoder extensions)BushelLogging- Logging abstractions
BushelCloudKit (Sources/BushelCloudKit/):
- Extends BushelKit models with CloudKit integration
Extensions/*+CloudKit.swift- CloudKitRecord protocol conformanceDataSources/- API fetchers (IPSW, AppleDB, MESU, etc.)CloudKit/- SyncEngine and service layer
Dependency Flow:
BushelCloudCLI → BushelCloudKit → BushelFoundation → BushelUtilities → MistKit
Why This Architecture:
- Domain models reusable across projects (BushelKit can evolve independently)
- CloudKit logic isolated to extensions (clean separation)
- Shared utilities promote consistency
- Testing each layer independently
The application follows a pipeline architecture:
External Data Sources → DataSourcePipeline → CloudKitService → CloudKit
↓
(deduplication & merging)
Three-Phase Sync Process:
- Fetch:
DataSourcePipelinefetches from multiple external APIs in parallel - Transform: Records are deduplicated and references resolved
- Upload:
BushelCloudKitServicebatches operations and uploads to CloudKit
CloudKit Integration (Sources/BushelCloud/CloudKit/):
BushelCloudKitService.swift- Server-to-Server auth setup and batch operations wrapperSyncEngine.swift- Orchestrates the full sync process from fetch to uploadCloudKitFieldMapping.swift- Type conversion helpers between Swift and CloudKit FieldValueRecordManaging+Query.swift- Query extensions for fetching records
Data Sources (Sources/BushelCloud/DataSources/):
DataSourcePipeline.swift- Coordinates fetching from all sources with metadata tracking- Individual fetchers:
IPSWFetcher,AppleDBFetcher,MESUFetcher,XcodeReleasesFetcher, etc.
Models (Packages/BushelKit/Sources/BushelFoundation/):
RestoreImageRecord- macOS restore images (usesURLandInttypes)XcodeVersionRecord- Xcode releases with CKReference relationshipsSwiftVersionRecord- Swift compiler versionsDataSourceMetadata- Fetch metadata with timestamp tracking
CloudKit Extensions (Sources/BushelCloudKit/Extensions/):
RestoreImageRecord+CloudKit.swift- Implements CloudKitRecord protocolXcodeVersionRecord+CloudKit.swift- Handles CKReference serializationSwiftVersionRecord+CloudKit.swift- Basic CloudKit mappingDataSourceMetadata+CloudKit.swift- Metadata sync recordFieldValue+URL.swift- URL ↔ STRING conversion
Commands (Sources/BushelCloud/Commands/):
- CLI commands using swift-argument-parser
- Each command (sync, export, clear, list, status) is a separate file
Records have dependencies that must be uploaded in order:
SwiftVersion (no dependencies)
RestoreImage (no dependencies)
↓
| CKReference (minimumMacOS, swiftVersion)
↓
XcodeVersion
Upload Order: SwiftVersion & RestoreImage → XcodeVersion (see SyncEngine.swift:100)
Multiple sources provide overlapping data. The pipeline deduplicates using:
- Build Number as unique key for Restore Images and Xcode Versions
- Version String as unique key for Swift Versions
- Merge Priority: MESU for signing status, AppleDB for hashes, most recent
sourceUpdatedAtwins
See .claude/implementation-patterns.md for detailed deduplication logic and code examples.
Purpose: VirtualBuddy provides real-time TSS (Tatsu Signing Status) verification for macOS restore images for virtual machines.
API Endpoint:
GET https://tss.virtualbuddy.app/v1/status?apiKey=<key>&ipsw=<IPSW URL>
Board Config: Checks VMA2MACOSAP (macOS virtual machines)
Key Response Fields:
isSigned(boolean) - true if Apple is signing the build, false otherwiseuuid- Request tracking ID for debuggingversion- macOS version (e.g., "15.0")build- Build number (e.g., "24A5327a")code- Status code (0 = SUCCESS, 94 = not eligible)message- Human-readable status message
HTTP Status Codes:
200- Success (returned regardless of signing status)400- Bad request (invalid IPSW URL)429- Rate limit exceeded500- Internal server error
Rate Limits & Caching:
- Rate limit: 2 requests per 5 seconds
- Server-side CDN cache: 12 hours (to avoid Apple TSS rate limiting)
- Client-side implementation: Random delays of 2.5-3.5 seconds with 1-second tolerance between requests
Implementation Details:
- File:
Sources/BushelCloudKit/DataSources/VirtualBuddyFetcher.swift - Integration: Enriches RestoreImageRecord with real-time signing status after other data sources
- Error handling: HTTP 429 errors are logged; original record preserved on any error
- Progress tracking: Shows "X/Y images checked" during sync
- Performance: ~2.5-4 minutes for 50 images (acceptable for 8-16 hour sync schedules)
Deduplication Priority:
- VirtualBuddy is an authoritative source for
isSignedstatus (along with MESU) - Takes precedence over other sources when merging duplicate records
- See
DataSourcePipeline.swift:429-447for merge logic
Example Response (Signed):
{
"uuid": "67919BEC-F793-4544-A5E6-152EE435DCA6",
"version": "15.0",
"build": "24A5327a",
"code": 0,
"message": "SUCCESS",
"isSigned": true
}Example Response (Unsigned):
{
"uuid": "02A12F2F-CE0E-4FBF-8155-884B8D9FD5CB",
"version": "15.1",
"build": "24B5024e",
"code": 94,
"message": "This device isn't eligible for the requested build.",
"isSigned": false
}The project uses structured logging with BushelLogger (wrapping os.Logger):
- Standard logs: Progress and results
- Verbose logs (
--verbose): MistKit API calls, batch details - Subsystems:
.sync,.cloudKit,.dataSource
BushelKit is embedded as a git subrepo for rapid development during the migration phase.
- Remote:
git@github.com:brightdigit/BushelKit.git - Branch:
bushelcloud - Location:
Packages/BushelKit/
# 1. Edit files in Packages/BushelKit/
vim Packages/BushelKit/Sources/BushelFoundation/RestoreImageRecord.swift
# 2. Commit changes in BushelCloud repository
git add Packages/BushelKit/
git commit -m "Update BushelKit: add field documentation"
# 3. Push changes to BushelKit repository
git subrepo push Packages/BushelKitgit subrepo pull Packages/BushelKit| Development Stage | Approach | Package.swift |
|---|---|---|
| Now (active migration) | Git subrepo (local path) | .package(path: "Packages/BushelKit") |
| After BushelKit v2.0 stable | Remote dependency | .package(url: "https://github.com/brightdigit/BushelKit.git", from: "2.0.0") |
Benefits of Subrepo Now:
- Edit BushelKit and BushelCloud together
- Test integration immediately
- No version coordination overhead
Migration to Remote:
- Tag stable BushelKit version
- Update
Package.swiftto use URL dependency - Remove
Packages/BushelKit/directory - Use standard SPM workflow
- Commit BushelKit changes separately from BushelCloud changes
- Push to subrepo after each logical change
- Pull before starting new work
- Test both repositories after changes
The project uses strict Swift 6 concurrency checking (see Package.swift:10-78):
- Full typed throws
- Complete strict concurrency checking
- Noncopyable generics, variadic generics
- Actor data race checks
- All types are
Sendable
When adding code: Ensure all new types conform to Sendable and use async/await patterns consistently.
Decision: All models use Int for byte counts (fileSize fields)
Rationale:
- All supported platforms are 64-bit (macOS 14+, iOS 17+, watchOS 10+)
- On 64-bit systems:
Int==Int64(same size and range) - Swift convention: use
Intfor counts, sizes, and indices - CloudKit automatically converts via
.int64(fileSize)
Safety Analysis:
- Largest image file: ~15 GB
Int.maxon 64-bit: 9,223,372,036,854,775,807 bytes (~9 exabytes)- No overflow risk for any realistic file size
CloudKit Integration:
// Write to CloudKit
fields["fileSize"] = .int64(record.fileSize) // Auto-converts Int → Int64
// Read from CloudKit
let size: Int? = recordInfo.fields["fileSize"]?.intValue // Returns IntDecision: Models use URL (not String) for download links
Benefits:
- Type safety at compile time
- URL component access (scheme, host, path, query)
- Automatic validation on creation
- Custom
FieldValue(url:)extension handles CloudKit STRING conversion
CloudKit Integration:
// Extension: Sources/BushelCloudKit/Extensions/FieldValue+URL.swift
public extension FieldValue {
init(url: URL) {
self = .string(url.absoluteString)
}
var urlValue: URL? {
if case .string(let value) = self {
return URL(string: value)
}
return nil
}
}Tests: See Tests/BushelCloudTests/Extensions/FieldValueURLTests.swift (13 test methods)
All domain models conform to CloudKitRecord:
protocol CloudKitRecord {
static var cloudKitRecordType: String { get }
func toCloudKitFields() -> [String: FieldValue]
static func from(recordInfo: RecordInfo) -> Self?
static func formatForDisplay(_ recordInfo: RecordInfo) -> String
}When adding a new record type:
- Create model struct in
Models/ - Implement
CloudKitRecordconformance - Add to
BushelCloudKitService.recordTypes(line 19) - Update CloudKit schema in Dashboard or via
cktool
CloudKit dates use milliseconds since epoch (not seconds). MistKit's FieldValue.date() handles conversion automatically:
fields["releaseDate"] = .date(Date()) // ✅ Auto-converted to millisecondsCloudKit has no native boolean type. Use INT64 with 0/1:
// In schema
"isSigned" INT64 QUERYABLE, // 0 = false, 1 = true
// In Swift
fields["isSigned"] = .int64(record.isSigned ? 1 : 0)
// Reading back
if case .int64(let value) = fields["isSigned"] {
let isSigned = value == 1
}CloudKit references use record names (not IDs):
// Creating a reference
fields["minimumMacOS"] = .reference(
Reference(recordName: "RestoreImage-23C71")
)
// Reading a reference
if case .reference(let ref) = fieldValue {
let recordName = ref.recordName
}Upload order matters: Upload referenced records before records that reference them, or you'll get VALIDATING_REFERENCE_ERROR.
CloudKit enforces a 200 operations per request limit. Operations are automatically chunked:
let batchSize = 200
let batches = operations.chunked(into: batchSize)
for batch in batches {
let results = try await service.modifyRecords(
batch,
database: .public(.prefers(.serverToServer))
)
}See .claude/s2s-auth-details.md for detailed batch operation examples and error handling.
BushelCloudKitErrorenum defines project-specific errors- MistKit operations throw
CloudKitErrorfor API failures modifyRecordsreturns[RecordResult]; switch over each (.success/.failure) to detect partial batch failures- Verbose mode logs error details (serverErrorCode, reason)
Common error codes:
ACCESS_DENIED- Check schema permissions (_creator + _icloud)AUTHENTICATION_FAILED- Invalid key ID or signatureCONFLICT- Use.forceReplaceinstead of.createVALIDATING_REFERENCE_ERROR- Referenced record doesn't exist
Individual fetchers can be tested directly:
let fetcher = IPSWFetcher()
let images = try await fetcher.fetch()No formal test suite exists (this is a demo project).
S2S authentication allows CLI tools to access CloudKit without a signed-in iCloud user. BushelCloud uses ECDSA P-256 private keys:
// Initialize with private key from .pem file
let tokenManager = try ServerToServerAuthManager(
keyID: "your-key-id",
pemString: pemFileContents
)
let service = CloudKitService(
containerIdentifier: "iCloud.com.company.App",
tokenManager: tokenManager,
environment: .development
)The database scope is picked per call now (e.g. database: .public(.prefers(.serverToServer))); it's no longer fixed at init time.
Key setup:
- Generate key pair in CloudKit Dashboard
- Download .pem file to
~/.cloudkit/bushel-private-key.pem - Set permissions:
chmod 600 ~/.cloudkit/bushel-private-key.pem - Set environment variables (see Quick Start section)
See .claude/s2s-auth-details.md for implementation details, security best practices, and troubleshooting.
The project requires three record types in the public database:
RestoreImage: version, buildNumber, releaseDate, downloadURL, fileSize, sha256Hash, sha1Hash, isSigned (INT64), isPrerelease (INT64), source, notes, sourceUpdatedAt
XcodeVersion: version, buildNumber, releaseDate, downloadURL, isPrerelease (INT64), source, minimumMacOS (REFERENCE), swiftVersion (REFERENCE), notes
SwiftVersion: version, releaseDate, downloadURL, source, notes
DataSourceMetadata: sourceName, recordTypeName, lastFetchedAt, sourceUpdatedAt, recordCount, fetchDurationSeconds, lastError
Critical Schema Rules:
- Always start schema files with
DEFINE SCHEMA - System fields (
___recordID,___createdTimestamp, etc.) can be included in.ckdbschema files but are auto-generated when creating records via API - Grant permissions to both
_creatorAND_icloudfor S2S auth - Use
INT64for booleans (0 = false, 1 = true)
# Save management token (for schema operations)
xcrun cktool save-token
# Validate schema
xcrun cktool validate-schema --team-id TEAM_ID --container-id CONTAINER_ID --environment development --file schema.ckdb
# Import schema
xcrun cktool import-schema --team-id TEAM_ID --container-id CONTAINER_ID --environment development --file schema.ckdb
# Export current schema
xcrun cktool export-schema --team-id TEAM_ID --container-id CONTAINER_ID --environment development > backup.ckdbSee .claude/schema-management.md for complete cktool reference, schema versioning, CI/CD deployment, and troubleshooting.
| Error | Cause | Solution |
|---|---|---|
ACCESS_DENIED |
Missing permissions | Grant to both _creator and _icloud |
AUTHENTICATION_FAILED |
Invalid key/PEM | Check CLOUDKIT_KEY_ID and .pem file |
VALIDATING_REFERENCE_ERROR |
Referenced record missing | Upload referenced records first |
QUOTA_EXCEEDED |
Too many operations | Reduce batch size or wait |
# From Terminal
cd /Users/leo/Documents/Projects/BushelCloud
open Package.swift
# Or: File > Open in Xcode, select Package.swift- Edit Scheme (Cmd+Shift+,)
- Run → Arguments tab:
- Add environment variables:
CLOUDKIT_CONTAINER_ID:iCloud.com.brightdigit.BushelCLOUDKIT_KEY_ID: Your key IDCLOUDKIT_PRIVATE_KEY_PATH:$HOME/.cloudkit/bushel-private-key.pem
- Add arguments for testing:
sync --verboseorexport --output ./export.json --verbose
- Add environment variables:
- Run → Options tab:
- Set custom working directory to project root
Test data fetching without CloudKit:
- Use
--dry-runflag:.build/debug/bushel-cloud sync --dry-run --verbose - Or set breakpoint in
SyncEngine.swiftafterfetchAllData()to inspect fetched records
Debug CloudKit upload:
- Set breakpoint in
BushelCloudKitService.modifyRecords()before upload - Inspect
operationsarray andresultsfor errors
Useful breakpoint locations:
SyncEngine.swift-sync()method startDataSourcePipeline.swift-fetchAllData()to inspect fetched recordsBushelCloudKitService.swift-modifyRecords()before CloudKit upload- Individual fetchers -
fetch()methods for data source issues
| Issue | Solution |
|---|---|
| "Cannot find container" | Verify CLOUDKIT_CONTAINER_ID is correct |
| "Authentication failed" | Check CLOUDKIT_KEY_ID and CLOUDKIT_PRIVATE_KEY_PATH |
| "Module 'MistKit' not found" | Reset package cache or rebuild |
| "Cannot find type 'RecordOperation'" | Clean build folder (Cmd+Shift+K) and rebuild |
Generate Xcode project with pre-configured settings:
brew install xcodegen
xcodegen generate
open BushelCloud.xcodeprojNote: The .xcodeproj is in .gitignore - always regenerate rather than committing.
- MistKit (local path:
../MistKit) - CloudKit Web Services client with S2S auth - IPSWDownloads - ipsw.me API wrapper for restore images
- SwiftSoup - HTML parsing for web scraping
- ArgumentParser - CLI framework
- swift-log - Logging infrastructure
MistKit is the parent package; BushelCloud is an example in Examples/Bushel/.
./Scripts/lint.shTools (managed via Mint):
swift-format@602.0.0- Code formattingSwiftLint@0.62.2- Style and convention linting (90+ opt-in rules)periphery@3.2.0- Unused code detection
Configuration: .swiftlint.yml, .swift-format, Mintfile
swift testNote: Currently only placeholder tests exist (demo project focused on CloudKit patterns).
- BushelCloud.yml - Main CI with multi-platform testing (Ubuntu, Windows, macOS)
- codeql.yml - Security analysis
- claude.yml - Claude Code integration for issues/PRs
- claude-code-review.yml - Automated PR reviews
The main branch requires:
- All 14 status checks passing (multi-platform builds + lint + CodeQL)
- Pull request reviews
- Conversation resolution
As noted in README.md, this is a demonstration project with known limitations:
- No incremental sync (always fetches all data from external sources)
- No conflict resolution for concurrent updates
- Limited error recovery in batch operations
- Export pagination: Export only retrieves first 200 records per type (see Issue #8)
These are intentional to keep the demo focused on MistKit patterns rather than production robustness.
Note on Duplicates: The sync properly uses .forceReplace operations with deterministic record names (based on build numbers), so repeated syncs update existing records rather than creating duplicates.
For detailed guides on advanced topics, see:
-
.claude/schema-management.md - Complete CloudKit schema management guide
- Schema file format and rules
- Complete cktool command reference
- Schema validation errors and solutions
- Versioning best practices
- CI/CD deployment automation
- Database scope considerations
-
.claude/s2s-auth-details.md - Server-to-Server authentication implementation
- How S2S authentication works internally
- BushelCloudKitService implementation pattern
- Security best practices and key management
- Common authentication errors and solutions
- Operation types and permissions
- Batch operations with detailed examples
- Testing S2S authentication
- Development vs Production environments
-
.claude/implementation-patterns.md - Implementation patterns and history
- Data source integration pattern with code examples
- Deduplication strategy and merge logic
- AppleDB integration details
- S2S authentication migration history
- Critical issues solved and lessons learned
- Common pitfalls to avoid
- Lessons for building future CloudKit demos