Skip to content

swift-security

Secure-by-default patterns for Swift iOS, macOS, and server-side. Load when task involves security, auth, Keychain, ATS, WebView, biometrics, deep links, or vulnerabilities.

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Secure-by-default patterns for Swift iOS, macOS, and server-side. Load when task involves security, auth, Keychain, ATS, WebView, biometrics, deep links, or vulnerabilities.

Agent definition

swift-security.md

Swift Secure Implementation Patterns

Secure-by-default patterns for Swift iOS, macOS, and server-side. Load when task involves security, auth, Keychain, ATS, WebView, biometrics, deep links, or vulnerabilities.

---

Store Sensitive Data in Keychain Services

Use Keychain Services for tokens, passwords, API keys, and cryptographic keys. Set `kSecAttrAccessible` to the most restrictive level appropriate for the use case.

import Security

enum KeychainError: Error {
    case duplicateItem, itemNotFound, unexpectedStatus(OSStatus)
}

func saveToKeychain(key: String, data: Data, accessibility: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: data,
        kSecAttrAccessible as String: accessibility,
    ]

    let status = SecItemAdd(query as CFDictionary, nil)
    switch status {
    case errSecSuccess: return
    case errSecDuplicateItem:
        // Update existing item
        let update: [String: Any] = [kSecValueData as String: data]
        let updateStatus = SecItemUpdate(query as CFDictionary, update as CFDictionary)
        guard updateStatus == errSecSuccess else {
            throw KeychainError.unexpectedStatus(updateStatus)
        }
    default:
        throw KeychainError.unexpectedStatus(status)
    }
}

func loadFromKeychain(key: String) throws -> Data {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne,
    ]

    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)
    guard status == errSecSuccess, let data = result as? Data else {
        throw KeychainError.itemNotFound
    }
    return data
}

**Accessibility levels** (most restrictive to least):

  • `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` — available only while unlocked, no backup migration
  • `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` — available after first unlock, no backup migration (recommended default)
  • `kSecAttrAccessibleWhenUnlocked` — available while unlocked, migrates with backups

**Why this matters**: `UserDefaults` is a plist readable on jailbroken devices and in unencrypted backups. Keychain is encrypted with device key; `ThisDeviceOnly` items don't migrate or appear in backups.

**Detection**:

rg -n 'UserDefaults.*token\|UserDefaults.*password\|UserDefaults.*secret\|UserDefaults.*key' . --type swift
rg -n 'SecItemAdd\|SecItemCopyMatching\|kSecClass' . --type swift

---

Justify Every ATS Exception

Keep ATS enabled. Document each exception with technical reason why HTTPS cannot be used.

<!-- Info.plist — minimal exceptions with justification -->
<key>NSAppTransportSecurity</key>
<dict>
    <!-- ATS is ON by default; only add exceptions that are technically necessary -->
    <key>NSExceptionDomains</key>
    <dict>
        <key>legacy-api.partner.com</key>
        <dict>
            <!-- Partner API does not support TLS 1.2; migration scheduled Q3 -->
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.0</string>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>
// Never do this in production:
// NSAllowsArbitraryLoads = true  // Disables ATS entirely

**Why this matters**: `NSAllowsArbitraryLoads = true` disables all transport security, enabling MITM attacks. App Store may reject blanket ATS exceptions.

**Detection**:

rg -n 'NSAllowsArbitraryLoads' . --type xml
rg -n 'NSExceptionDomains|NSAppTransportSecurity' . --type xml

---

Prefer Universal Links Over Custom URL Schemes

Use universal links for deep linking. If custom URL schemes necessary, validate all parameters.

// Correct: universal link handling with validation
func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL,
          let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
        return false
    }

    // Validate the host is your domain (universal links guarantee this, but defense-in-depth)
    guard components.host == "app.example.com" else { return false }

    // Validate path and parameters before routing
    switch components.path {
    case "/order":
        guard let orderId = components.queryItems?.first(where: { $0.name == "id" })?.value,
              orderId.range(of: #"^[a-f0-9-]{36}$"#, options: .regularExpression) != nil else {
            return false
        }
        navigateToOrder(orderId)
    default:
        return false
    }
    return true
}
// If custom URL schemes are used, validate everything
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
    guard url.scheme == "myapp",
          let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
        return false
    }

    // Validate each parameter individually
    let action = components.host ?? ""
    guard ["order", "profile", "settings"].contains(action) else { return false }

    // Never pass URL parameters directly to SQL, file paths, or web views
    // ...
}

**Why this matters**: Any app can register the same custom scheme and intercept your deep links. Universal links use associated domains verification to cryptographically bind URLs to your app.

**Detection**:

rg -n 'CFBundleURLSchemes|openURL|open url' . --type swift
rg -n 'NSUserActivityTypeBrowsingWeb|universalLinks' . --type swift
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked