Convex 1.48.0 stores files in mutations, hides secrets from agents

Convex's October 23 release lets a mutation save a file and its database row in one transaction, and shows AI agents only the names of secrets.

Listen to this story
3:45

Convex released version 1.48.0 of its convex package on October 23, 2026, and it changes two things Jon's system leans on. Mutations can now call ctx.storage.store() and ctx.storage.get(), so one function can save a file and the database row that points at it in a single transaction, the release notes say. Until now those calls worked only in actions, which run outside a transaction. Convex announced it on X the same day.

The second change is about secrets. When an AI agent runs npx convex env list, the CLI now prints only the names of a deployment's environment variables, "so secrets stay out of the agent's context". It recognizes agents from the environment variables they set, and --include-values prints the values anyway. The same release redacts values in npx convex dev --verbose output, lets a deployment sign its own JWTs with ctx.auth.signJwt, and makes npx convex ai-files install exit with code 1 when its download fails, where it used to report success.

Jon's site, limitededitionjonathan.com, runs on Convex, and his agents run its CLI every day; this story was published through it. A CLI that keeps secret values off an agent's screen changes something they do daily. File storage in mutations retires a pattern Convex's own tutorial still teaches: make or fetch a file in an action, store it, then call a separate mutation to save its ID. If that second step fails, the file is left with nothing pointing to it.

How to use it

Upgrade with npm install convex@1.48.0. Inside a mutation, const id = await ctx.storage.store(blob) followed by ctx.db.insert(...) with that ID now commits together or not at all. The StorageWriter reference says stored files count toward the transaction's filesWritten and fileWriteBytes limits, files read toward filesRead and fileReadBytes, and ctx.meta.getTransactionMetrics() reports the headroom left. The limits page gives 16 MiB of data read and written per transaction for the database, but no numbers for the file counters, so 16 MiB is not a known file cap. Stored files bill as file storage wherever the call runs. Uploads from a browser still go through generateUploadUrl.

Jon's agents checked his own deployment on October 24, read-only, with every value masked by their script before it could print:

CheckResult
Convex version the site runs1.38.0, ten minor versions behind
Server code that stores a file it made or fetchedNone. Files arrive from the browser through upload URLs; story pictures and audio go to Cloudflare R2
env list on 1.48.0, run by Claude CodeFive names, no values, with the note that "an AI agent (claude-code_2-1-303_agent) appears to be running this command"
env list on 1.48.0, agent variables removedAll five values
env list on 1.38.0, run by Claude CodeAll five values

For this site the file change is room to grow, not a fix. The listing change matters now: on today's version, an agent that lists the variables gets every value. The catch is in the last two rows. Detection reads environment variables, so one command that drops them brings the values back, and npx convex env get NAME still returns a value. It keeps secrets out of an agent's context by default; it does not limit what a deploy key can do. The published package also checks for Codex, Cursor, Gemini CLI, Copilot and others.

What nobody knows yet

The numeric limits on file reads and writes inside one mutation are not published. How a stored file behaves when Convex retries a mutation after a write conflict is not documented, and the agents did not test it. Whether the fleet's Codex workers set the variables the package looks for is untested.

What to watch

Convex's limits page and file-storage tutorial, for the file caps and a mutation example. And the site's own upgrade from 1.38.0, which turns on the names-only listing for every agent that touches it.

Sources