A checkpoint is a backup of your API keys
Patterns
The nice thing about a sandbox, for years, has been that it forgets. You start
an E2B or Modal or Vercel sandbox, hand it the two keys the job needs, and when
the job finishes the whole machine evaporates with the keys inside it. That is
not good security — an agent that reads os.environ still got what it got —
but it bounds the damage in time. Whatever you leaked, you leaked for ninety
seconds.
Sprites, Fly.io's persistent sandbox, breaks that assumption on purpose, and it is the right call for what it is trying to be. A Sprite is a Firecracker microVM with a real ext4 filesystem that survives hibernation. It sleeps when nothing is using it, wakes in about 200 ms, and your installed packages, your cloned repo, and your half-finished work are all still there. That is a much better place to keep a long-running agent than a container that resets every time you look away.
It also means the box remembers your credentials. And then it takes pictures of them.
The failure mode
Sprites have checkpoints: a point-in-time snapshot of the entire filesystem, so you can roll back a bad upgrade or an experiment that went sideways. They are genuinely useful and correctly built. They also have no idea which of those bytes are a Stripe key.
So:
- March — you write
STRIPE_SECRET_KEYinto a file on the Sprite, because that is what you do on a machine that keeps its files. - April — you checkpoint before upgrading Ubuntu. Good practice. The key goes in the snapshot.
- May — you rotate the key. The file gets the new value. Everything is correct.
- June — something breaks and you
sprite restorethe April checkpoint.
The March key is now on a running machine again, and it works — because rotating a secret changes what the next fetch returns. It does not reach into a filesystem snapshot, and it does not invalidate the old value at Stripe. You now have a retired credential live on a machine you thought you had cleaned, with nothing in any log saying it came back.
This is not a bug in checkpoints. It is what a filesystem snapshot is. It is only surprising because the surrounding instinct — "it's a sandbox, the secrets die with it" — is a habit picked up from sandboxes that die.
The other two things persistence breaks
The checkpoint case is the vivid one. Two quieter ones bite more often.
Injection at create becomes a one-time act. On an ephemeral sandbox, "pass the keys in at boot" is self-healing: the next sandbox gets the current values, and the longest a stale value survives is one job. On a Sprite, the values you passed in when you provisioned it in January are the values it still has in March. Rotate a key and the Sprite does not find out. There is no re-creation to carry the change, because not re-creating things is the entire feature.
Environment variables on a service live on disk. Sprites' answer to "my
process needs to come back after a wake" is a
Service, and a service takes
--env. That looks like the ephemeral-sandbox move, but it is not: the value is
stored in the service definition, replayed on every boot, printed back by
sprite-env services get, and — yes — captured by the next checkpoint. It is a
.env file wearing a nicer hat. Worse, anything running inside the Sprite can
read it, including the agent you were trying to keep it away from.
Stop putting values in the box
The fix is not a better place inside the Sprite to hide a value. There isn't
one: checkpoints capture in-memory state too, so the /dev/shm trick does not
save you either. The fix is that the Sprite should hold one credential — a
scoped token — and resolve everything else at the moment a process starts.
Concretely, the service command stops being your program and starts being a launcher:
sprite-env services create api \
--cmd seekrit-run \
--args "--env-file,/etc/seekrit.env,--cache,--,node,server.js"
seekrit-run authenticates with the token in
/etc/seekrit.env, fetches the environment's ciphertext, decrypts it inside
the Sprite, and execs your server with the values set. Everything above turns
over:
- The values exist in one process's memory, and
execreplaced the launcher, so there is no wrapper holding a copy. A checkpoint taken a minute later has a binary and a token in it, not a Stripe key. - A Sprite that has been asleep for a week picks up whatever is current when it wakes. Rotation lands without anyone going to find the fleet.
- The service definition holds a path, not a password, so
sprite-env services getis boring again.
The token is still on the disk, and I want to be straight about that rather than pretend the problem vanished: it is one credential instead of five, bound to one environment, and revocable in one command. Mint it per Sprite and revoke it in the same teardown that destroys the Sprite, and a token resurrected by a restore fails closed instead of working.
If the thing in the Sprite is an agent, don't inject at all
Sprites' headline use case is running code you did not write — a coding agent, an eval harness, model output. For that, injecting the values correctly is still the wrong shape, for the reason it is always the wrong shape: a process that can read its own environment can send that environment somewhere.
Run seekrit-proxy as its own Sprite service and give
the agent a placeholder:
OPENAI_API_KEY='{{seekrit:OPENAI_API_KEY}}'
OPENAI_BASE_URL=http://127.0.0.1:8080/openai/v1
The real key is substituted on the way out to an allowlisted host, in a process
the agent did not start. The agent's environment, its logs, its crash dumps, and
every checkpoint of its filesystem hold the string {{seekrit:OPENAI_API_KEY}}.
The honest caveat: that proxy is in the same microVM, so this is a weaker
boundary than Cloudflare's outbound handlers, where the credential lives in a
Worker on a different machine entirely. Root in the VM can read another process's
memory. What it defeats is prompt injection, a leaked transcript, a shared
checkpoint, and every form of casual exfiltration — which is the overwhelming
majority of how these keys actually get out. Pair it with Sprites' own
network policy, which is
DNS-based, default-deny, and read-only from inside the Sprite, and an agent that
routes around the proxy reaches an allowed host with no credential, while
everything else gets REFUSED.
What Fly.io already got right
Sprites ships Connectors, which is the same idea from the other end: your GitHub or OpenRouter token lives in your Fly.io org, calls go through a gateway that identifies the Sprite, and the Sprite never holds the token. Deny-by-default, scoped by label. If your secret is one of the APIs it fronts, use it — it is strictly better than a token in the box.
It just does not cover a DATABASE_URL, an S3 credential, a webhook signing
secret, or any provider they haven't wrapped. And there's one composition that
looks appealing and doesn't work: you cannot register seekrit itself as a Custom
API connector to keep SEEKRIT_TOKEN off the Sprite. A seekrit service token is
skt_<id>_<pkcs8> — it embeds a private key, not a bearer credential, and
/v1/resolve returns ciphertext that only that key opens. Broker it and the
gateway holds the key material while the Sprite gets an envelope it can't open.
The token has to be wherever the decryption happens. That is the zero-knowledge
model working, not a gap in it.
The general version
The instinct "secrets in a sandbox are fine, the sandbox is disposable" was always doing more work than it should. What it really said was the blast radius is short, and that was a property of the container's lifetime, not of anything you decided.
Persistent sandboxes are a good idea and there will be more of them; keeping the filesystem is most of why an agent that works for an hour is better than one that works for ninety seconds. But when the box stops being disposable, the reasoning that rested on disposability has to go with it. Anything that survives a wake, survives a snapshot. Assume a credential you put in a long-lived machine is permanent, and put a revocable one there instead.
The Sprites guide has the full setup — token bootstrapping, the egress-policy rules, the proxy-as-a-service ordering, and a worked example replacing the env-file dance in Fly.io's own Claude Managed Agents integration.