All skills
rshankras avatar

/release-review

@32566aa

Senior developer-level release review for macOS/iOS apps. Identifies security, privacy, UX, and distribution issues with actionable fixes. Use when preparing an app for release, want a critical review, or before App Store submission.

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/release-review

This session only. Nothing lands on disk.

api-design-checklist.md

≈2.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

API Design Checklist

Network and API patterns review for macOS and iOS applications.

User-Agent Headers

Why This Matters

User-Agent identifies your app to API servers. Spoofing (pretending to be a browser or another app) is:

  • Dishonest - Misrepresents your app's identity
  • Risky - Can get your app blocked when detected
  • Unprofessional - Shows lack of engineering maturity

✅ Good Pattern

// Honest identification
let userAgent = "MyApp/1.2.0 (macOS 14.0; com.company.myapp)"

var request = URLRequest(url: url)
request.setValue(userAgent, forHTTPHeaderField: "User-Agent")

❌ Anti-patterns

// Browser spoofing - NEVER do this
request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...", forHTTPHeaderField: "User-Agent")

// Pretending to be official app
request.setValue("OfficialAPIClient/1.0", forHTTPHeaderField: "User-Agent")

// Empty or missing - also problematic
// (no User-Agent set at all)

Checklist

  • User-Agent honestly identifies your app
  • Includes app name and version
  • No browser or other app spoofing
  • Consistent across all API calls

Search Pattern

Grep: "User-Agent|userAgent|Mozilla|Chrome|Safari"

Error Handling

HTTP Status Codes

Handle all relevant status codes gracefully:

func handleResponse(_ response: HTTPURLResponse, data: Data) throws -> Data {
    switch response.statusCode {
    case 200...299:
        return data
    case 401:
        throw APIError.unauthorized("Session expired. Please re-authenticate.")
    case 403:
        throw APIError.forbidden("Access denied. Check your permissions.")
    case 404:
        throw APIError.notFound("Resource not found.")
    case 429:
        throw APIError.rateLimited("Too many requests. Please wait and try again.")
    case 500...599:
        throw APIError.serverError("Server error. Please try again later.")
    default:
        throw APIError.unknown("Unexpected error (HTTP \(response.statusCode)).")
    }
}

User-Friendly Error Messages

✅ Good Pattern
enum APIError: LocalizedError {
    case networkUnavailable
    case unauthorized(String)
    case rateLimited(String)

    var errorDescription: String? {
        switch self {
        case .networkUnavailable:
            return "Unable to connect. Please check your internet connection."
        case .unauthorized(let message):
            return message
        case .rateLimited(let message):
            return message
        }
    }

    var recoverySuggestion: String? {
        switch self {
        case .networkUnavailable:
            return "Try again when you have a stable connection."
        case .unauthorized:
            return "Sign out and sign back in to refresh your session."
        case .rateLimited:
            return "Wait a few minutes before making more requests."
        }
    }
}
❌ Anti-patterns
// Technical jargon in user messages
throw NSError(domain: "HTTP", code: 401, userInfo: nil)

// Generic unhelpful messages
throw APIError.error("Something went wrong")

// Exposing internal details
throw APIError.error("JSON parsing failed at key 'data.user.id'")

Checklist

  • All HTTP status codes handled
  • Error messages are user-friendly
  • Error messages explain what to do
  • No technical jargon in user-facing errors
  • Errors logged for debugging (not shown to user)

Token Expiration

✅ Good Pattern

class APIClient {
    private var tokenExpiresAt: Date?

    func makeRequest() async throws -> Data {
        // Check expiration before request
        if let expiresAt = tokenExpiresAt, Date() >= expiresAt {
            throw APIError.tokenExpired("Your session has expired. Please restart the app to refresh.")
        }

        // Make request...
        let (data, response) = try await session.data(for: request)

        // Handle 401 from server (token revoked or expired early)
        if (response as? HTTPURLResponse)?.statusCode == 401 {
            throw APIError.tokenExpired("Your session has expired. Please restart the app to refresh.")
        }

        return data
    }

    func refreshTokenIfNeeded() async throws {
        guard let expiresAt = tokenExpiresAt else { return }

        // Refresh proactively when close to expiration
        let refreshThreshold: TimeInterval = 300 // 5 minutes
        if Date().addingTimeInterval(refreshThreshold) >= expiresAt {
            try await refreshToken()
        }
    }
}

Token Refresh Flow

// Option 1: Automatic refresh
func refreshToken() async throws {
    let newToken = try await authService.refreshToken()
    self.token = newToken.accessToken
    self.tokenExpiresAt = newToken.expiresAt
}

// Option 2: Notify user to re-authenticate
NotificationCenter.default.post(
    name: .tokenExpired,
    object: nil,
    userInfo: ["message": "Please sign in again to continue."]
)

Checklist

  • Token expiration time tracked
  • Expiration checked before requests
  • 401 responses handled as potential expiration
  • User notified with clear action when token expires
  • Proactive refresh implemented (if supported by API)

Rate Limiting

Handling 429 Responses (Backoff + Retry-After)

func makeRequestWithRetry(maxRetries: Int = 3) async throws -> Data {
    for attempt in 0..<maxRetries {
        do {
            return try await makeRequest()
        } catch APIError.rateLimited(let retryAfterHeader) {
            let delay = retryAfterHeader.map(Double.init) ?? pow(2.0, Double(attempt))
            try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
        }
    }
    throw APIError.unknown("Request failed after retries")
}

Checklist

  • 429 status code handled
  • Retry-After header respected
  • Exponential backoff implemented
  • User informed when rate limited
  • Request queuing for high-volume operations

Timeout Configuration

Guidelines

  • Short timeouts for user-initiated actions (10-30 seconds)
  • Longer timeouts for background operations (60-120 seconds)
  • Very short timeouts for connectivity checks (5 seconds)
// User-initiated request
var request = URLRequest(url: url)
request.timeoutInterval = 30

// Background sync
let config = URLSessionConfiguration.background(withIdentifier: "sync")
config.timeoutIntervalForRequest = 60
config.timeoutIntervalForResource = 300

// Connectivity check
var pingRequest = URLRequest(url: healthCheckURL)
pingRequest.timeoutInterval = 5

Checklist

  • Appropriate timeouts for each request type
  • Timeout errors show user-friendly message
  • Long operations use background session
  • No infinite timeouts

Offline Handling

Network Reachability

import Network

class NetworkMonitor: ObservableObject {
    private let monitor = NWPathMonitor()
    @Published var isConnected = true

    init() {
        monitor.pathUpdateHandler = { [weak self] path in
            DispatchQueue.main.async {
                self?.isConnected = path.status == .satisfied
            }
        }
        monitor.start(queue: DispatchQueue.global())
    }
}

Graceful Degradation

func fetchData() async throws -> [Item] {
    if !networkMonitor.isConnected {
        // Return cached data when offline
        if let cached = cache.loadItems() {
            return cached
        }
        throw APIError.networkUnavailable
    }

    // Fetch fresh data
    let items = try await api.fetchItems()
    cache.saveItems(items)
    return items
}

Checklist

  • Network connectivity monitored
  • Offline state shown to user
  • Cached data available offline (if applicable)
  • Auto-retry when connection restored
  • No silent failures when offline

Request/Response Logging

Debug Logging (Development Only)

#if DEBUG
func logRequest(_ request: URLRequest) {
    print("📤 \(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "")")
}

func logResponse(_ response: HTTPURLResponse, data: Data) {
    print("📥 \(response.statusCode) (\(data.count) bytes)")
}
#endif

❌ Anti-patterns

// NEVER log sensitive data
print("Token: \(apiToken)")  // Security risk!
print("Request body: \(String(data: body, encoding: .utf8))")  // May contain PII

// NEVER log in production
func makeRequest() {
    print("Making request...")  // Should be #if DEBUG
}

Checklist

  • Request/response logging only in DEBUG
  • No sensitive data in logs (tokens, passwords, PII)
  • No production logging of request bodies
  • Structured logging for debugging

Caching Strategy

HTTP Caching

// Respect cache headers
let config = URLSessionConfiguration.default
config.requestCachePolicy = .useProtocolCachePolicy

// Custom cache for specific needs
let cache = URLCache(
    memoryCapacity: 10 * 1024 * 1024,  // 10 MB
    diskCapacity: 50 * 1024 * 1024,     // 50 MB
    diskPath: "api_cache"
)
config.urlCache = cache

Application-Level Caching

actor APICache {
    private var cache: [String: (data: Data, timestamp: Date)] = [:]
    private let maxAge: TimeInterval = 300  // 5 minutes

    func get(_ key: String) -> Data? {
        guard let entry = cache[key] else { return nil }
        if Date().timeIntervalSince(entry.timestamp) > maxAge {
            cache.removeValue(forKey: key)
            return nil
        }
        return entry.data
    }

    func set(_ key: String, data: Data) {
        cache[key] = (data, Date())
    }
}

Checklist

  • Caching strategy defined
  • Cache invalidation implemented
  • Stale data handling defined
  • Cache size limits set

API Versioning

Handling API Changes

struct APIClient {
    static let apiVersion = "v1"
    static let baseURL = "https://api.example.com/\(apiVersion)"

    // Check minimum supported version
    func checkAPICompatibility() async throws {
        let serverVersion = try await fetchServerVersion()
        if serverVersion < minimumSupportedVersion {
            throw APIError.updateRequired("Please update the app to continue using this feature.")
        }
    }
}

Checklist

  • API version included in requests
  • Graceful handling of deprecated endpoints
  • User prompted to update when API incompatible
  • Fallback behavior for missing features

Search Patterns

// Find potential API issues
Grep: "URLRequest|URLSession"
Grep: "User-Agent|userAgent"
Grep: "401|403|429|500"
Grep: "timeout|Timeout"
Grep: "print.*request|print.*response|NSLog.*API"

References

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill acts as a pre-release review utility for macOS and iOS applications. It is structurally secure and contains no malicious instructions, obfuscation, or data exfiltration paths. It has a low-risk exposure to indirect prompt injection due to its processing of external project files, but its capabilities are strictly bounded to read-only tools.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    6/6 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 32566aa. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
What it can do
Reads files
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 3 allowed tools
ReadGlobGrep

README badge

README badge for rshankras/claude-code-apple-skills/release-review