Skip to content

feat(storage): inspect and reset app storage - #22

Merged
lauridskern merged 64 commits into
mainfrom
feat/app-storage
Oct 21, 2026
Merged

lauridskern merged 64 commits into
mainfrom
feat/app-storage

Conversation

@lauridskern

@lauridskern lauridskern commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a Storage tool to the workspace for inspecting and resetting what an app has saved on an iOS simulator or a debuggable Android build. It needs no code in the app.

What it does

  • Browse
    • Reads MMKV (including custom IDs; encrypted instances are listed as locked), AsyncStorage, UserDefaults (through defaults, so values the app hasn't written to disk yet are included), SharedPreferences, DataStore, SQLite and the largest files.
    • The sidebar lists the app's own stores. SDK and system stores sit in a collapsed group.
    • Search runs across every store at once.
  • SQLite tables: each table opens as a paged grid with column types. NULL, numbers and blobs (shown as hex) each have their own look.
  • Changes: while the tab is open, storage is read every 5 seconds and each added, changed or removed key appears in a feed. JSON values show only the fields that changed. A switch shows SDK changes, which are hidden by default.
  • Clear and Reset: clear one store, or reset all app data. Both stop the app first, and both ask for confirmation.
  • Shared UI pieces:
    • A shared app-icon cache (appIcons) and avatar component (AppAvatar), now also used by the device popover.
    • New sm sizes for Tabs and InputGroup.
    • shadcn's ButtonGroup.

Tools

Tool Visible to
mobile_app_storage model and panel
mobile_app_storage_table model and panel
mobile_clear_app_storage model and panel (marked destructive)

Codex's 64 KB combined tool limit applies only to Agent Plugins (a root plugin.json). Mobile Dev uses the legacy .codex-plugin/plugin.json format, so all of its model-visible tools stay visible. This PR also corrects npm run check:mcp-budget and docs/architecture.md, which had applied that limit to every plugin.

Clear and the table reader only reach stores found in a fresh listing of the app's files. Table names must be in the database's own table list, so callers can't reach other files or inject SQL.

Telemetry

  • Server: app_storage.operation.* per operation (read, clear, reset, table).
  • UI: ui.storage.* timings plus a storage surface.
  • Privacy: keys, values, paths and table names are never sent. Details are in docs/telemetry.md.

Test app

examples/storage-test-app is an Expo app that writes realistic data to every supported store. run.sh ios|android builds and installs a standalone release build; on Android it's marked debuggable so its data can be read.

Verification

  • Automated: npm test passes, 724 tests. They cover MMKV fixtures (including expiring and encrypted files), plist, SharedPreferences and DataStore parsing, the change diff, the SDK toggle, and table paging against a real 250-row SQLite database, including the injection attempt and a path outside the app's data folder.
  • Manual on iOS: checked on an iOS 27 simulator with the test app and Expo Go.
  • Manual on Android: checked on an Android 36 emulator with the test app's release build. MMKV (including the encrypted instance), SharedPreferences, AsyncStorage (RKStorage), SQLite and the table grid all read correctly. The Changes feed picked up writes from the app, and clearing a store removed only its files after stopping the app.

Known gaps

  • Squash merge: the branch carries local version-bump commits. The release version is 0.1.161, one above main.

Read MMKV, AsyncStorage, UserDefaults, SharedPreferences, DataStore and SQLite
stores from the app's data directory without app code, mark keys changed since
the first read, and clear one store or all app data.
The model tool budget is full, so the destructive clear tool is panel-only
and the read tool's schema is shorter.
Browse groups the app's own stores apart from SDK data, renders values by
type with inline JSON trees, and searches every store at once. The Changes
tab reads storage every 5 seconds while open and lists keys added, changed,
and removed, highlighting only the changed JSON fields.
Buttons write realistic data to MMKV (default, custom ID, encrypted),
AsyncStorage, UserDefaults or SharedPreferences, SQLite, and files, and the
app shows what it reads back. run.sh builds a standalone release app; the
Android release build is debuggable so the storage tool can read it.
Use Select, Tabs, InputGroup, and ButtonGroup with outline buttons, and
shorten the sidebar hover highlights so neighbouring rows keep a gap.
…rage header

The storage header now matches the 28px controls in the workspace toolbar,
using shadcn's sm sizes for Select and Button and new sm sizes for TabsList
and InputGroup.
…picker

App icons and Android labels now come from one cache per view, keyed by
device and app, so a panel never refetches an icon another panel loaded.
AppAvatar moves to its own component with xs, sm, and md sizes.
…apsed

A collapsed simulator panel left the resize handle's line directly under the
toolbar's border. The handle stays draggable but hides its line while the
simulators are collapsed.
SelectItem's text wrapper now centers its children, so icons line up with the
text in every dropdown. The storage app picker lists each app on two lines
with a larger icon.
Files becomes a normal sidebar row with its size and file count, and the size
breakdown moves into the Files panel header. Store and file rows align their
icon with the title line.
SDK changes stay hidden by default. A switch in the feed footer shows them,
tagged SDK, and the Changes count follows the toggle.
A panel-only mobile_app_storage_table tool reads one page of rows from a
database the scan found, matching the store and table against the listing so
no other file or SQL can be named. Blobs arrive as hex previews. Clicking a
table in a database opens the grid with column types and paging.
The table no longer has its own toolbar. The store header gains an outline back
button, the table name, its column and row counts, and paging when a table has
more than one page.
…r helpers

Split storage-view.tsx into the shell, browse, changes, entries, and shared
parts modules, with one SidebarRow for stores and files and chip colors
defined once. StoragePanel runs reads and actions through one helper, and the
workspace switches tools through a panel lookup instead of ternary chains.

On the server, each tool resolves its driver and container in one place,
store lookup and database copying are shared, AppContainer loses its
duplicated copy method, AsyncStorage databases set their kind explicitly, and
table limits live in storageLimits.
Comment thread src/server/app-storage/formats.ts Fixed
Comment thread src/server/app-storage/formats.ts Fixed
Codex limits model-visible MCP tools to 8,000 bytes each and 64,000 combined
only for plugins with a root plugin.json; this plugin's .codex-plugin manifest
is the legacy format and has neither limit. The check now detects the format,
always flags input schemas Codex would compact, and fails only on limits that
apply.
… them first

CodeQL flagged the single-pass comment removal as incomplete sanitization. The
plist and SharedPreferences parsers now match comments, declarations, and
doctypes as tokens and skip them, so nothing is rewritten before parsing.
The app picker now takes labels from the shared icon cache, so Android apps
show their names instead of package names. Expo's internal preferences move
under SDKs & system, and AsyncStorage's Android database is titled default,
as on iOS.
mobile_app_storage_table and mobile_clear_app_storage were panel-only only
because of the mistaken tool budget. The model can now page through SQLite
rows and reset app data to test fresh states; clearing stays marked
destructive, and the skill asks the model to confirm before deleting data the
user may need.
@lauridskern
lauridskern merged commit 1b99c69 into main Oct 21, 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.

2 participants