Skip to content

feat(storage): read native iOS and Android app storage - #23

Merged
lauridskern merged 5 commits into
mainfrom
feat/native-app-storage
Oct 23, 2026
Merged

lauridskern merged 5 commits into
mainfrom
feat/native-app-storage

Conversation

@lauridskern

@lauridskern lauridskern commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

The Storage tool now works for native iOS and Android apps, not only React Native ones. Two native test apps cover it, and every store type below was checked on a real simulator or emulator.

What changed

Native storage Before Now
SwiftData (default.store) Not found Opens as a SQLite database. When an app has an App Group, SwiftData keeps this store in the group container.
App Group containers (iOS) Not read Read under AppGroup/<id>/, including their shared UserDefaults. Reset empties them too.
Core Data / custom-named SQLite Only common file extensions Any file with a -wal/-shm companion counts as SQLite. Core Data and SwiftData dates show as dates in the table grid.
Files (Codable JSON, plists, NSKeyedArchiver, text) Listed only Clicking a file opens a preview: JSON and plists as a tree, NSKeyedArchiver object graphs resolved, text as is. The new mobile_app_storage_file tool does the same for the model. Binary content is withheld.
EncryptedSharedPreferences Ciphertext shown as values Marked encrypted
Proto DataStore Listed only Decoded without a schema: numbered fields, repeats indexed, nested messages flattened

Also fixed:

  • Database row counts: some showed "?" because one sqlite3 process per table ran in parallel, and the processes failed on the write-ahead log lock. Every table is now described and counted in single queries.
  • Table paging: if you paged quickly, a slower response for an earlier page could replace a newer one. The panel now keeps one open table or file at a time and drops any response for one that's been replaced.

Privacy (after merging #29)

File previews follow the storage redaction from #29, with no panel-only bypass:

  • Structured files: JSON, plist and archive contents are filtered as one value, using the file name as context. A file named like session.json is withheld entirely.
  • Text files: withheld when they contain credential-like text.
  • Binary files: return no content.
  • Paths: sensitive paths are rejected.
  • Order: filtering happens before anything is cut short.

Proto DataStore entries go through the same entry filter as every other store. readTable keeps #29's column projection and redaction on top of this branch's single describe-tables query. docs/storage-privacy.md now covers the file tool.

The panel now names a cleared store from its own snapshot, since the server no longer echoes the name. Before this, it read "Cleared store."

Test apps

  • examples/native-storage-ios: a SwiftUI app writing UserDefaults (every value type), App Group defaults, Core Data, SwiftData, a Codable JSON file, a binary plist, an NSKeyedArchiver archive and text. run.sh <UDID> signs it ad hoc, because the simulator only creates the App Group container when the entitlement is signed.
  • examples/native-storage-android: a Kotlin app (no Compose or code generation) writing SharedPreferences, EncryptedSharedPreferences, SQLite, DataStore Preferences and Proto, MMKV and files. The release build is debuggable so run-as can read it.

Verification

  • Automated: npm test passes (783 tests). New tests use real files taken from the test apps:
    • native store detection
    • Proto DataStore decoding
    • EncryptedSharedPreferences
    • App Group path mapping, including rejection of unknown groups and ..
    • file previews in every format, including NSKeyedArchiver cycles
    • SQLite table description
    • the paging race, with a test that fails under the old check
  • Packaging: test:package, test:codex-plugin and check:mcp-budget pass.
  • Manual:
    • iOS: checked on an iOS 27 simulator with the native app, in the panel too.
    • Android: checked on an Android 36 emulator. Every store reads correctly through the plugin's server code.

Notes

  • Reset and App Groups: Reset now also empties App Group containers, since SwiftData data would otherwise survive a reset. Group containers can be shared with other apps from the same developer.
  • Keychain: not read. It isn't in the app's data directory, and its items are encrypted.
  • MMKV values: an MMKV integer 1 still reads as true, because MMKV stores no types and the bytes are identical.
  • Telemetry: new file operation (app_storage.operation.*) and ui.storage.file timing. File contents are never sent.
  • Version: 0.1.165, one above main.

- Read App Group containers on iOS simulators under AppGroup/<id>/, so shared
  UserDefaults and SwiftData stores in them are found; reset empties them too.
- Recognize SwiftData's default.store and any SQLite file with -wal or -shm
  companions, and show Core Data and SwiftData dates in the table grid.
- Read every table's row count and columns in single queries; parallel sqlite3
  processes failed on the write-ahead log lock.
- Mark EncryptedSharedPreferences as encrypted and decode Proto DataStore files
  as numbered fields without a schema.
- Add mobile_app_storage_file and file previews in the panel, decoding JSON,
  binary and XML property lists, NSKeyedArchiver archives, text, and hex.
A SwiftUI app writes UserDefaults, App Group defaults, Core Data, SwiftData,
and Codable, plist, and archived files. A Kotlin app writes SharedPreferences,
EncryptedSharedPreferences, SQLite, DataStore Preferences and Proto, MMKV, and
files.
- Merge the panel's table and file slots into one detail, loaded by one helper
  that drops a response once a newer detail replaced it; this also fixes a
  slower earlier table page overwriting the page opened after it.
- Describe every SQLite table with one query, replacing the table list and the
  per-table PRAGMA.
- Make Proto DataStore its own store kind instead of re-checking the file name.
- Report files too large to preview as their own format.
- Move varint, UTF-8, and JSON helpers into encoding.ts, share the Apple date
  conversion, and drop a cast, an export, and duplicate operation types.
Keep main's storage redaction and scoping, and apply it to this branch:
file previews are filtered like storage values with the file name as context,
reject sensitive paths, and withhold binary content; Proto DataStore entries go
through the same entry filter. readTable keeps main's column projection and
redaction on top of the single describe-tables query. The panel names a
cleared store from its snapshot now that the server does not echo it. Version
0.1.165.
@lauridskern
lauridskern merged commit 68d12c4 into main Oct 23, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant