Skip to content

swift-security-testing

| Data Category | Storage | Reason | |---------------|---------|--------| | API tokens, OAuth tokens | **Keychain** | Encrypted at rest; protected by device passcode / Secure Enclave | | Passwords, private keys | **Keychain** | Never stored in plaintext | | User preferences

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.

| Data Category | Storage | Reason | |---------------|---------|--------| | API tokens, OAuth tokens | **Keychain** | Encrypted at rest; protected by device passcode / Secure Enclave | | Passwords, private keys | **Keychain** | Never stored in plaintext | | User preferences

Agent definition

swift-security-testing.md

Swift Security & Testing

Security

Keychain vs. UserDefaults

| Data Category | Storage | Reason | |---------------|---------|--------| | API tokens, OAuth tokens | **Keychain** | Encrypted at rest; protected by device passcode / Secure Enclave | | Passwords, private keys | **Keychain** | Never stored in plaintext | | User preferences (theme, language) | UserDefaults | Non-sensitive; loss is acceptable | | Feature flags | UserDefaults | Non-sensitive | | JWT refresh tokens | **Keychain** | Credential — same as token | | Device-specific identifiers | UserDefaults or Keychain depending on sensitivity | Evaluate case by case |

**Detection trigger**: Any `UserDefaults` call with a key string containing `token`, `password`, `key`, `secret`, `credential`, or `auth` is a security violation requiring Keychain migration.

// Wrong
UserDefaults.standard.set(apiToken, forKey: "auth_token")

// Correct — Keychain wrapper
struct KeychainStore {
    static func save(token: String, service: String, account: String) throws {
        let data = Data(token.utf8)
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: service,
            kSecAttrAccount: account,
            kSecValueData: data,
            kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]
        SecItemDelete(query as CFDictionary)  // Remove existing item
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else {
            throw KeychainError.saveFailed(status)
        }
    }
}

App Transport Security (ATS)

  • ATS is enabled by default — keep it enabled
  • `NSAllowsArbitraryLoads: true` in Info.plist requires documented justification (e.g., streaming media exemption per Apple documentation)
  • Use `NSExceptionDomains` for specific domains that require exceptions; keep ATS bypasses scoped to individual domains
  • All production endpoints must use HTTPS with valid certificates

Certificate Pinning

For endpoints handling financial, healthcare, or authentication data, implement certificate or public key pinning via `URLSessionDelegate`.

final class PinningDelegate: NSObject, URLSessionDelegate, @unchecked Sendable {
    private let pinnedHashes: Set<String>

    init(pinnedHashes: Set<String>) {
        self.pinnedHashes = pinnedHashes
    }

    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
              let serverTrust = challenge.protectionSpace.serverTrust,
              let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        let serverCertData = SecCertificateCopyData(certificate) as Data
        let hash = serverCertData.sha256HexString  // implement SHA-256 helper
        if pinnedHashes.contains(hash) {
            completionHandler(.useCredential, URLCredential(trust: serverTrust))
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

Secret Management

| Source | Rule | |--------|------| | API keys in source files | **Hard boundary** — decompilation extracts them trivially | | API keys in Info.plist | **Hard boundary** — same decompilation risk | | Build-time secrets | Use `.xcconfig` files excluded from version control; read via `Bundle.main.infoDictionary` | | CI/CD secrets | Environment variables injected at build time; keep out of version control | | Runtime secrets | Fetched from server after authentication; stored in Keychain |

Input Validation

Validate all data from external sources before use:

// URL from deep link or pasteboard — never force-unwrap
guard let url = URL(string: rawString), url.scheme == "https" else {
    logger.warning("Rejected invalid URL: \(rawString, privacy: .private)")
    return
}

// API response data — always decode into typed models, never assume structure
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try decoder.decode(APIResponse.self, from: data)

---

Testing

Swift Testing over XCTest for New Code

Use `import Testing` for all new test files. Migrate XCTest suites to Swift Testing only when explicitly requested.

| Feature | Swift Testing | XCTest | |---------|--------------|--------| | Test declaration | `@Test func name()` | `func testName()` | | Assertion | `#expect(condition)` | `XCTAssertTrue(condition)` | | Parameterized tests | `@Test(arguments: [...])` | Manual loop or subclassing | | Expected failure | `@Test(.disabled("reason"))` | `XCTSkip` | | Test tags | `@Test(.tags(.performance))` | None built-in |

import Testing
@testable import MyApp

@Suite("UserRepository")
struct UserRepositoryTests {
    let sut: UserRepository
    let mockClient: MockHTTPClient

    init() {
        mockClient = MockHTTPClient()
        sut = UserRepository(client: mockClient)
    }

    @Test("fetch returns decoded user on success")
    func fetchSuccess() async throws {
        mockClient.stubbedData = try JSONEncoder().encode(User.fixture)
        let user = try await sut.fetchUser(id: User.fixture.id)
        #expect(user.id == User.fixture.id)
        #expect(user.displayName == User.fixture.displayName)
    }

    @Test("fetch throws on network failure", arguments: [
        URLError(.notConnectedToInternet),
        URLError(.timedOut)
    ])
    func fetchNetworkFailure(error: URLError) async {
        mockClient.errorToThrow = error
        await #expect(throws: FetchError.self) {
            try await sut.fetchUser(id: UUID())
        }
    }
}

Fresh-Instance Isolation

  • Instantiate the sys
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