ios-logging — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ios-logging (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Production-grade skill for eliminating silent failures in iOS apps. Most production errors don't crash — they vanish through try?, Task {}, .replaceError(), and print()-only catch blocks.
AI coding assistants systematically generate observability-blind code because their training data is overwhelmingly tutorial code: print(error) in every catch block, try? everywhere, Task {} with no error handling, no crash SDK integration, no privacy annotations, and zero consideration for MetricKit or PII compliance. This skill intercepts those patterns and enforces observable error handling from the start.
Logging is the key to debugging. When a bug appears in production across thousands of devices, you can't attach a debugger. Remote logging through crash reporting SDKs transforms a 3-day debugging mystery into a 15-minute investigation. This skill enforces observable error handling: every error is logged with os.Logger (with privacy annotations), reported to a remote crash/analytics SDK, and surfaced to the user or operator.
Three non-negotiable rules:
Logger with privacy annotationsdo/catch for network, persistence, auth, and user-facing operationsNot every caught error is a bug. Reporting every error to the crash SDK makes dashboards useless because real signals drown in noise. Split errors into two buckets:
| Kind | Examples | Logger | Crash SDK |
|---|---|---|---|
| Expected — user condition or transient, not a bug | 401 unauthorized, 403 forbidden, HTTP 408/429/503 timeout/retry, offline, cancellation | .info/.notice/.warning (log for context) | No `recordNonFatal`. Add a breadcrumb at most. |
| Unexpected — real bug or server failure | HTTP 500/502, decoding/contract failure, Core Data validation error, unreachable code path, unknown error | .error or .fault | Yes — recordNonFatal with context |
Why this matters: A team with a broken login flow cannot tell the difference between 10,000 "expired session" events and one "decoder crash on the checkout screen" if both land in the same bucket. Crash SDK quota (Crashlytics caps non-fatals at 8 per session) also gets consumed by noise before real bugs can be captured.
Rule of thumb: Ask "would an on-call engineer want to be paged if this fires 1,000x a day?" If no → log only. If yes → log + recordNonFatal. Retries are a special case: breadcrumb each attempt, recordNonFatal only after all retries are exhausted (see references/silent-failures.md for the retry pattern).
When setting up crash reporting, recommend one of these based on the project's needs:
What ecosystem is the project in?
├── Firebase-heavy (Auth, Firestore, Push) → Firebase Crashlytics (free, tight integration)
├── Standalone / wants rich observability → Sentry (best error context, breadcrumbs, performance)
├── Needs product analytics + errors → Sentry (crashes) + PostHog (analytics, session replay)
└── Enterprise / custom → Sentry or Datadog + Google Analytics for funnelsdSYM upload is always required — regardless of which SDK you choose, configure DEBUG_INFORMATION_FORMAT = DWARF with dSYM File for all build configurations (including Release) and all targets, then add the SDK's dSYM upload script to Build Phases. Without dSYMs, crash reports and MetricKit stacks show hex addresses instead of function names. This applies to dual-SDK setups too — both SDKs need dSYMs.
Recommend connecting these services via MCP servers or CLI tools so the AI assistant can query production errors, search crash patterns, and pull breadcrumb trails directly during debugging sessions:
firebase crashlytics:*) — list crashes, download reportsPresentation Layer -> SwiftUI error state + centralized ErrorHandling
Application Layer -> ErrorReporter protocol (abstracts Sentry/Crashlytics)
Logging Layer -> os.Logger with subsystem/category and privacy annotations
Diagnostics Layer -> MetricKit (OOM, watchdog kills, hangs — out-of-process)
Crash Layer -> Sentry OR Crashlytics (not both for fatals) + dSYMsIs this a best-effort operation where failure is genuinely irrelevant?
├── YES (temp file cleanup, optional cache read, cosmetic prefetch)
│ └── try? is acceptable
└── NO (network, persistence, auth, user-facing, payment, navigation)
└── MUST use do/catch with Logger.error() + ErrorReporter.recordNonFatal()Is this production code?
├── YES -> os.Logger with privacy annotations
│ ├── Debug tracing -> .debug (free in production, not persisted)
│ ├── Supplementary context -> .info (memory-only, NOT persisted — only captured alongside faults)
│ ├── Operational events -> .notice (persisted to disk) ← successful sync, background task done, user actions
│ ├── Recoverable errors -> .error (always persisted)
│ └── Bugs / unrecoverable -> .fault (persisted + process chain)
└── NO (unit tests, playgrounds, scripts)
└── print() is fine⚠️ Common mistake — `.info` is NOT persisted to disk. For events that operators or on-call engineers need to see in production logs (successful sync completions, user actions, background task results), use .notice. Only use .info for supplementary context you want captured alongside faults but don't need independently. When mapping events to levels, ask: "Does someone need to find this log entry independently?" If yes → .notice. If it's only useful as context around a fault → .info.
Do you need the value visible to log readers?
├── YES (URL paths, error codes, status codes, operation names)
│ └── privacy: .public
└── NO — Is it PII (user ID, email, name, device ID)?
├── Need to correlate events for the same user across log lines?
│ ├── YES → privacy: .private(mask: .hash) ← stable hash, enables correlation
│ └── NO → privacy: .private ← hidden as <private>
└── Is it a secret (password, token, API key)?
└── DO NOT LOG IT. If you must reference it: privacy: .sensitive⚠️ `.sensitive` ≠ hash. .sensitive always redacts — it is for passwords/tokens that should not appear even in debug logs. It does NOT produce a hash for correlation. For cross-event user correlation without exposing identity, always use .private(mask: .hash).
⚠️ Operational PII leaks are the most commonly missed risk. Even when the primary log message looks harmless, these sources silently leak PII:
/users/[email protected]/profile embeds an email in the pathAuthorization, Cookie, X-User-ID headers carry credentials and identifiersSELECT * FROM users WHERE email = 'jane@...'Always log safe summaries (endpoint path, status code, payload size, operation name) instead of raw values. See references/pii-compliance.md for redaction patterns.
catch {
// 1. ALWAYS: Structured log with privacy annotations
Logger.<category>.error("Operation failed: \(error.localizedDescription, privacy: .public)")
// 2. ALWAYS: Report to crash SDK
ErrorReporter.shared.recordNonFatal(error, context: ["operation": "..."])
// 3. CONDITIONALLY: User feedback (if user-facing operation)
// 4. CONDITIONALLY: Recovery action (retry, rollback, logout)
}When discussing MetricKit setup, always cover all three:
MXMetricManager.shared.pastDiagnosticPayloads and must be processed at launch, or that session's diagnostic data is lost forever.See references/metrickit.md for complete setup code and comparison table.
Does the Task body contain try or await that can throw?
├── YES -> MUST wrap in do/catch with observability inside the Task
│ └── Also: distinguish CancellationError (normal) from real errors
└── NO -> Task {} is fine as-isOn first use of any scanning workflow, check for .claude/ios-logging-config.md in the project root. If it doesn't exist, run the Configuration Phase below before scanning. If it exists, read it and use those preferences for all fixes.
Ask the user these questions and persist answers to .claude/ios-logging-config.md:
.private, no PHI in logs)try? with do/catch, add error states, restructure where needed.claude/ios-logging-config.md---
crash_sdk: sentry
error_reporter_type: ErrorReporter
error_reporter_import: "import ErrorReporting"
logger_extension: "Sources/Core/Logger+Extensions.swift"
logger_subsystem: "Bundle.main.bundleIdentifier!"
pii_level: standard
fix_style: full
---The config is a simple YAML frontmatter file. The skill reads it at the start of every scanning workflow and uses the values to generate correct import statements, SDK calls, and Logger patterns without asking the user again.
When: User asks to "scan for silent failures", "audit error handling", "find missing logging", "check for try?", or any variant of "make sure nothing fails silently."
This is the primary scanning workflow — modeled after ios-security's Phase 0 → Scan → Report pattern.
#### Phase 0: Discover & Configure
.xcodeproj/.xcworkspace to list all targets.swift files per targetSilent Failure Scan — choose scope:
Target scope:
A: Main target only (~5 min)
B: All targets including extensions (~10 min)
C: Specific target (you specify)
Scan depth:
1: Critical patterns only (try?, Task {}, print(), empty catch)
2: Full scan (adds Combine, URLSession status, NotificationCenter, Core Data, BGTask)
3: Full + infrastructure (adds dSYM check, extension SDK init, MetricKit, privacy manifests)
Example: "B2" = all targets, full scan#### Phase 1: Scan
Run grep-based detection first (zero-token, fast), then semantic review on flagged files.
Depth 1 — Critical patterns:
| Pattern | Detection | Fix |
|---|---|---|
try? on non-trivial operations | grep -rn 'try?' --include='*.swift' | Replace with do/catch + Logger + ErrorReporter |
Task {} / Task.detached {} with throwing code | grep -rn 'Task\s*{' --include='*.swift' — then check if body has try/await without do/catch | Wrap in do/catch, distinguish CancellationError |
print( in production code | grep -rn 'print(' --include='*.swift' — exclude test targets | Replace with Logger.<category>.<level>() with privacy annotations |
| Empty catch blocks | grep -rn 'catch\s*{' --include='*.swift' — then check if body is empty or only has break/return | Add Logger.error + ErrorReporter.recordNonFatal |
Catch blocks with only print | Semantic: catch blocks where the only action is print(error) | Add Logger + ErrorReporter, remove print |
else with silent return | Semantic: guard/if-else where the else branch returns/breaks without logging | Add Logger.warning explaining what condition was unexpected |
Depth 2 — Full scan (adds to depth 1):
| Pattern | Detection | Fix |
|---|---|---|
.replaceError() killing Combine pipelines | grep -rn '.replaceError' --include='*.swift' | Move error handling inside flatMap |
receiveCompletion with only print | Semantic: sink completion handlers with just print | Add Logger + ErrorReporter |
| URLSession without status code check | Semantic: URLSession.shared.data( without httpResponse.statusCode | Add HTTP status validation + error reporting |
| NotificationCenter observer not stored | Semantic: addObserver(forName: return value discarded | Store token, add typed Notification.Name |
Core Data try? context.save() | grep -rn 'try?.*save()' --include='*.swift' | Replace with do/catch, NSError userInfo extraction, rollback |
.task {} with try? | grep -rn 'try?' --include='*.swift' in .task context | Replace with do/catch, CancellationError filter |
| BGTask without do/catch | Semantic: BGProcessingTask/BGAppRefreshTask handlers | Add do/catch + expirationHandler |
Depth 3 — Infrastructure (adds to depth 2):
| Check | Detection | Fix |
|---|---|---|
| dSYM configuration | Check Build Settings for DEBUG_INFORMATION_FORMAT | Set to DWARF with dSYM File for all targets |
| Extension SDK initialization | Check extension entry points for crash SDK start() | Add separate SDK init + disable autoSessionTracking |
| MetricKit subscriber | grep -rn 'MXMetricManager' --include='*.swift' | Add MXMetricManagerSubscriber if missing |
| PrivacyInfo.xcprivacy | Check for file existence | Create if missing (required since May 2024) |
| Dual crash reporter conflicts | Check for both Sentry + Crashlytics initialization | Warn about signal handler conflicts |
#### Phase 2: Report
Output findings grouped by severity:
## Silent Failure Scan Report
### Configuration
- SDK: [from config]
- Scope: [user choice]
- Files scanned: N
### CRITICAL (errors vanishing completely)
[try? on network/auth/payment, Task {} swallowing, empty catch blocks]
### HIGH (errors logged locally but not reported remotely)
[catch blocks with only print() or Logger but no ErrorReporter]
### MEDIUM (weak observability)
[missing privacy annotations, missing CancellationError filter, URLSession status unchecked]
### Summary
| Severity | Count |
|----------|-------|
| Critical | N |
| High | N |
| Medium | N |
| **Total** | **N** |
### Auto-fix available
[List of files where the skill can apply fixes automatically using the config preferences]After the report, offer: "Should I fix these? I'll use [SDK from config] and [fix style from config]."
When: Setting up observability for an iOS project from scratch, or migrating from print() to Logger.
.claude/ios-logging-config.md doesn't existreferences/logger-setup.md)references/crash-sdk-integration.md)print() calls — replace with appropriate Logger leveltry? usages — convert critical ones to do/catch (references/silent-failures.md)Task {} blocks — ensure do/catch wraps any throwing codeflatMap (references/silent-failures.md)references/metrickit.md)references/enterprise-patterns.md)When: Code review that touches error handling, networking, persistence, or async code.
catch block: does it have Logger + ErrorReporter? (references/silent-failures.md)try?: is failure genuinely irrelevant? If not, flag itTask {} with try: is there a do/catch inside?.task {} modifier: CancellationError handled separately?flatMap, not at the pipeline end?references/logger-setup.md)references/pii-compliance.md)references/silent-failures.md)When: Adding Sentry, Crashlytics, or PostHog to an iOS project.
references/crash-sdk-integration.mdreferences/crash-sdk-integration.md)references/enterprise-patterns.md)When: Setting up the development environment to query production errors from your AI assistant.
claude mcp add sentry or configure in .mcp.jsonnpm install -g firebase-tools && firebase loginfirebase crashlytics:symbols:upload, listing recent crashesThis connectivity is what makes remote logging truly powerful — instead of context-switching to dashboards, your debugging workflow stays in the editor.
| Reference | When to Read |
|---|---|
references/silent-failures.md | Writing or reviewing error handling code, diagnosing vanishing errors |
references/logger-setup.md | Setting up os.Logger, choosing log levels, adding privacy annotations |
references/crash-sdk-integration.md | Integrating Sentry/Crashlytics/PostHog, ErrorReporter protocol, breadcrumbs |
references/metrickit.md | Adding MetricKit for OOM/watchdog/hang detection |
references/objc-exceptions.md | Bridging Swift/ObjC error handling, NSException edge cases |
references/pii-compliance.md | GDPR/CCPA logging compliance, privacy manifests, redaction patterns |
references/enterprise-patterns.md | Centralized error handling, retry with backoff, extension monitoring |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.