home / skills / charleswiltgen / axiom / axiom-storage-diag

This skill helps diagnose local file storage issues by guiding location, protection, backup, and existence checks to prevent data loss.

npx playbooks add skill charleswiltgen/axiom --skill axiom-storage-diag

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
10.4 KB
---
name: axiom-storage-diag
description: Use when debugging 'files disappeared', 'data missing after restart', 'backup too large', 'can't save file', 'file not found', 'storage full error', 'file inaccessible when locked' - systematic local file storage diagnostics
license: MIT
metadata:
  version: "1.0.0"
  last-updated: "2025-12-12"
---

# Local File Storage Diagnostics

## Overview

**Core principle** 90% of file storage problems stem from choosing the wrong storage location, misunderstanding file protection levels, or missing backup exclusions—not iOS file system bugs.

The iOS file system is battle-tested across millions of apps and devices. If your files are disappearing, becoming inaccessible, or causing backup issues, the problem is almost always in storage location choice or protection configuration.

## Red Flags — Suspect File Storage Issue

If you see ANY of these:
- Files mysteriously disappear after device restart
- Files disappear randomly (weeks after creation)
- App backup size unexpectedly large (>500 MB)
- "File not found" after app background/foreground cycle
- Files inaccessible when device is locked
- Users report lost data after iOS update
- Background tasks can't access files

❌ **FORBIDDEN** "iOS deleted my files, the file system is broken"
- iOS file system handles billions of files daily across all apps
- System behavior is documented and predictable
- 99% of issues are location/protection mismatches

## Mandatory First Steps

**ALWAYS check these FIRST** (before changing code):

```swift
// 1. Check WHERE file is stored
func diagnoseFileLocation(_ url: URL) {
    let path = url.path
    if path.contains("/tmp/") {
        print("⚠️ File in tmp/ - system purges aggressively")
    } else if path.contains("/Caches/") {
        print("⚠️ File in Caches/ - purged under storage pressure")
    } else if path.contains("/Documents/") {
        print("✅ File in Documents/ - never purged, backed up")
    } else if path.contains("/Library/Application Support/") {
        print("✅ File in Application Support/ - never purged, backed up")
    }
}

// 2. Check file protection level
func diagnoseFileProtection(_ url: URL) throws {
    let attrs = try FileManager.default.attributesOfItem(atPath: url.path)
    if let protection = attrs[.protectionKey] as? FileProtectionType {
        print("Protection: \(protection)")
        if protection == .complete {
            print("⚠️ File inaccessible when device locked")
        }
    }
}

// 3. Check backup status
func diagnoseBackupStatus(_ url: URL) throws {
    let values = try url.resourceValues(forKeys: [.isExcludedFromBackupKey])
    if let excluded = values.isExcludedFromBackup {
        print("Excluded from backup: \(excluded)")
    }
}

// 4. Check file existence and size
func diagnoseFileState(_ url: URL) {
    if FileManager.default.fileExists(atPath: url.path) {
        if let size = try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64 {
            print("File exists, size: \(size) bytes")
        }
    } else {
        print("❌ File does not exist")
    }
}
```

---

## Decision Tree

### Files Disappeared

```
Files missing? → Check where stored

├─ Disappeared after device restart
│   ├─ Was in tmp/? → EXPECTED (tmp/ purged on reboot)
│   │   → FIX: Move to Caches/ or Application Support/
│   │
│   ├─ Was in Caches/? → System purged (storage pressure)
│   │   → FIX: Move to Application Support/ if can't be regenerated
│   │
│   └─ Protection level .complete? → Inaccessible until unlock
│       → FIX: Wait for unlock or use .completeUntilFirstUserAuthentication
│
├─ Disappeared randomly (weeks later)
│   ├─ In Caches/? → System purged under storage pressure
│   │   → EXPECTED if re-downloadable
│   │   → FIX: Re-download when needed, or move to Application Support/
│   │
│   └─ In Documents or Application Support/?
│       → Check if user deleted app (purges all data)
│       → Check iOS update (rare, but check migration path)
│
└─ Only some files missing
    → Check isExcludedFromBackup + iCloud sync
    → Check if file names have special characters
    → Check file permissions
```

### Files Inaccessible

```
Can't access file?

├─ Error: "No permission" or NSFileReadNoPermissionError
│   ├─ Device locked? → Check file protection
│   │   └─ .complete protection? → Wait for unlock
│   │       → FIX: Use .completeUntilFirstUserAuthentication
│   │
│   └─ Background task accessing? → .complete blocks background
│       → FIX: Change to .completeUntilFirstUserAuthentication
│
├─ File exists but read returns empty/nil
│   └─ Check actual file size on disk
│       → May be zero-byte file from failed write
│
└─ File exists in debugger but not at runtime
    → Check if using wrong directory (Documents vs Caches)
    → Check URL construction
```

### Backup Too Large

```
App backup > 500 MB?

├─ Check Documents directory size
│   └─ Large files (>10 MB each)?
│       ├─ Can they be re-downloaded? → Move to Caches + isExcludedFromBackup
│       └─ User-created? → Keep in Documents (warn user if >1 GB)
│
├─ Check Application Support size
│   └─ Downloaded media/podcasts?
│       → Mark isExcludedFromBackup = true
│
└─ Audit backup with code:
    ```swift
    func auditBackupSize() {
        let docsURL = FileManager.default.urls(
            for: .documentDirectory,
            in: .userDomainMask
        )[0]
        let size = getDirectorySize(url: docsURL)
        print("Documents (backed up): \(size / 1_000_000) MB")
    }
    ```
```

---

## Common Patterns by Symptom

### Pattern 1: Files in tmp/ Disappear

**Symptom**: Temp files missing after restart or even during app lifecycle

**Cause**: tmp/ is purged aggressively by system

**Fix**:
```swift
// ❌ WRONG: Using tmp/ for anything that should persist
let tmpURL = FileManager.default.temporaryDirectory
let fileURL = tmpURL.appendingPathComponent("data.json")
try data.write(to: fileURL)  // WILL BE DELETED

// ✅ CORRECT: Use Caches/ for re-generable data
let cacheURL = FileManager.default.urls(
    for: .cachesDirectory,
    in: .userDomainMask
)[0]
let fileURL = cacheURL.appendingPathComponent("data.json")
try data.write(to: fileURL)
```

### Pattern 2: Caches Purged, Data Lost

**Symptom**: Downloaded content disappears weeks later

**Cause**: Caches/ is purged under storage pressure (expected behavior)

**Fix**: Either re-download on demand OR move to Application Support if can't be regenerated
```swift
// ✅ CORRECT: Handle missing cache gracefully
func loadCachedImage(url: URL) async throws -> UIImage {
    let cacheURL = getCacheURL(for: url)

    // Try cache first
    if FileManager.default.fileExists(atPath: cacheURL.path),
       let data = try? Data(contentsOf: cacheURL),
       let image = UIImage(data: data) {
        return image
    }

    // Cache miss - re-download
    let (data, _) = try await URLSession.shared.data(from: url)
    try data.write(to: cacheURL)
    return UIImage(data: data)!
}
```

### Pattern 3: .complete Protection Blocks Background

**Symptom**: Background tasks fail with "permission denied"

**Cause**: Files with .complete protection inaccessible when locked

**Fix**:
```swift
// ❌ WRONG: .complete protection for background-accessed files
try data.write(to: url, options: .completeFileProtection)
// Background task fails when device locked

// ✅ CORRECT: Use .completeUntilFirstUserAuthentication
try data.write(
    to: url,
    options: .completeFileProtectionUntilFirstUserAuthentication
)
// Accessible in background after first unlock
```

### Pattern 4: Backup Bloat from Downloaded Content

**Symptom**: App backup >1 GB, app rejected or users complain

**Cause**: Downloaded content in Documents/ or not marked excluded

**Fix**:
```swift
// ✅ CORRECT: Exclude re-downloadable content
func downloadPodcast(url: URL) async throws {
    let appSupportURL = FileManager.default.urls(
        for: .applicationSupportDirectory,
        in: .userDomainMask
    )[0]

    let podcastURL = appSupportURL
        .appendingPathComponent("Podcasts")
        .appendingPathComponent(url.lastPathComponent)

    // Download
    let (data, _) = try await URLSession.shared.data(from: url)
    try data.write(to: podcastURL)

    // Mark excluded from backup
    var resourceValues = URLResourceValues()
    resourceValues.isExcludedFromBackup = true
    try podcastURL.setResourceValues(resourceValues)
}
```

---

## Production Crisis Scenario

**SYMPTOM**: Users report lost photos after iOS update

**DIAGNOSIS STEPS**:

1. **Check storage location** (5 min):
   ```swift
   // Were photos in Caches/?
   let photosInCaches = path.contains("/Caches/")
   // If yes → system purged them (expected)
   ```

2. **Check if backed up** (5 min):
   ```swift
   // Check if excluded from backup
   let excluded = try? url.resourceValues(
       forKeys: [.isExcludedFromBackupKey]
   ).isExcludedFromBackup
   // If excluded=true AND not synced → lost
   ```

3. **Check migration path** (10 min):
   - Did app container path change?
   - Did we migrate data from old location?

**ROOT CAUSES** (90% of cases):
- Photos in Caches/ (purged under storage pressure)
- Photos excluded from backup + no cloud sync
- Migration code missing after major iOS update

**FIX**:
- User photos MUST be in Documents/
- Never exclude user-created content from backup
- Always have cloud sync OR backup for user content

---

## Quick Diagnostic Checklist

Run this on any storage problem:

```swift
func diagnoseStorageIssue(fileURL: URL) {
    print("=== Storage Diagnosis ===")

    // 1. Location
    diagnoseFileLocation(fileURL)

    // 2. Protection
    try? diagnoseFileProtection(fileURL)

    // 3. Backup status
    try? diagnoseBackupStatus(fileURL)

    // 4. File state
    diagnoseFileState(fileURL)

    // 5. Directory size
    if let parentURL = fileURL.deletingLastPathComponent() as URL? {
        let size = getDirectorySize(url: parentURL)
        print("Parent directory size: \(size / 1_000_000) MB")
    }

    print("=== End Diagnosis ===")
}
```

---

## Related Skills

- `axiom-storage` — Correct storage location decisions
- `axiom-file-protection-ref` — Understanding protection levels
- `axiom-storage-management-ref` — Purge behavior and capacity APIs

---

**Last Updated**: 2025-12-12
**Skill Type**: Diagnostic

Overview

This skill performs systematic local file storage diagnostics for xOS apps to find why files disappear, become inaccessible, or bloat backups. It codifies quick checks and a decision tree so you can identify wrong storage locations, file protection mismatches, and backup-exclusion mistakes. Use it to triage storage incidents before assuming system-level bugs.

How this skill works

The skill inspects file URLs, directory locations, file protection attributes, backup exclusion flags, and file existence/size. It runs a short checklist and follows a decision tree that maps symptoms (disappeared files, inaccessible files, large backups) to likely root causes and fixes. Outputs concrete remediation steps like moving files, changing protection, or marking files excluded from backup.

When to use it

  • Files disappear after restart or randomly
  • App backup size unexpectedly large (>500 MB)
  • Background tasks cannot read files or get permission errors
  • Users report lost data after OS update
  • Files inaccessible while device is locked

Best practices

  • Always verify the directory: tmp vs Caches vs Application Support vs Documents
  • Choose file protection based on access needs (.complete vs .completeUntilFirstUserAuthentication)
  • Exclude re-downloadable or downloaded media from backups, keep user-created data in Documents
  • Handle cache misses gracefully by re-downloading and not assuming persistence
  • Audit directory sizes regularly to detect backup bloat early

Example use cases

  • Diagnose ‘files missing after reboot’ by checking for tmp/ or Caches/ storage and moving persistent files to Application Support or Documents
  • Investigate ‘permission denied’ in background tasks by checking file protection and switching to first-user-authentication protection when appropriate
  • Reduce app backup size by locating large files in Documents/App Support and marking re-downloadable content as excluded from backup
  • Triage post-update lost-data reports by checking migration paths, backup flags, and whether data lived in a purgable directory

FAQ

What if a file exists during debugging but not at runtime?

Confirm you are using the same sandbox path and directory (Documents vs Caches vs tmp). Verify URL construction and that the file was written to the correct container before runtime access.

When should I use .completeUntilFirstUserAuthentication?

Use it for files that must be readable by background tasks or soon after boot but still benefit from encryption. Avoid .complete for any file needed while the device is locked.

How do I stop backup bloat from downloaded media?

Store re-downloadable media in Caches or Application Support and set isExcludedFromBackup=true for those files. Keep user-created content in Documents and do not exclude it.