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.
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow 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.mdSwift 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
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
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

