The split
Every secret the provider hands back — a Supabase service-role key, a
Vercel token, an AWS secret access key — goes into Phantom, not into
.stack.toml. Stack only records the slot name of
each secret; Phantom holds the actual value, encrypted end-to-end.
- ● Drives provider OAuth / API flows
- ● Creates upstream resources
- ● Tracks which secrets each service needs (slot names)
- ●
Wires
.env+.mcp.json - ○ Never holds plaintext secrets
- ● Encrypts every secret at rest (AEAD)
- ●
Proxies
phm_*tokens → real values at exec time - ● Syncs across machines via Phantom Cloud (optional)
- ● Never exposes values to agents or editors
- ○ Has no opinion on what the keys mean
Why Stack refuses to run without Phantom
When you run stack add, stack import, or stack doctor, Stack calls phantom via its CLI. If
Phantom isn't installed, there's nowhere to put the secrets — Stack would
have to write them to disk in plaintext. We refuse, loudly:
$ stack add supabase
┌ stack add supabase
│
└ Phantom is not installed. Install it first:
brew install ashlrai/phantom/phantom
or
npm i -g phantom-secrets
The one exception is stack init, which only writes the shape
file — it warns but proceeds, so you can scaffold before installing
Phantom.
The flow of a single secret
Take SUPABASE_SERVICE_ROLE_KEY as an example:
-
You run
stack add supabase. - Stack opens Supabase OAuth in a browser; you click Authorise.
-
Stack creates a new project via the Management API. The project is
assigned a project ref (e.g.
abcdefghijklmnop). -
Stack fetches the
service_rolekey from Supabase. Exactly once, in memory. -
Stack calls
phantom add SUPABASE_SERVICE_ROLE_KEY <value>. Phantom encrypts the value and writes it to the vault. -
Stack writes
secrets = ["SUPABASE_SERVICE_ROLE_KEY"]into.stack.toml. The value never touches disk. -
Stack regenerates
.env.localwith Phantom tokens:SUPABASE_SERVICE_ROLE_KEY=phm_….
phm_ token? It's a placeholder
Phantom recognises. When your app runs under phantom exec
(which is what stack exec wraps), Phantom swaps the token for
the real secret at process-spawn time. The token is safe to check in;
the real value never leaves the vault.
Runtime injection — stack exec
Your app reads secrets from process.env like normal. The trick
is how they got there. Compare:
# bun dev straight up
# .env.local has SUPABASE_SERVICE_ROLE_KEY=phm_abc123
# → your app reads "phm_abc123" and breaks
bun dev
# stack exec wraps with Phantom's proxy
# → Phantom resolves phm_abc123 to the real value at spawn
# → your app reads the real key
stack exec -- bun dev stack exec forwards everything after -- to
phantom exec. Use it for dev servers, test runners, migrations —
anything that needs real secrets locally.
Syncing the vault
Phantom Cloud (optional) gives you E2E-encrypted sync so the vault follows you across machines and into CI:
# set up cloud sync once
phantom login
phantom cloud push # upload the current vault
# on another machine (or in CI)
phantom cloud pull --force # download + decrypt the vault stack ci init (see CI integration)
writes a GitHub Actions workflow that runs phantom cloud pull
before stack doctor --json.
Which Stack commands talk to Phantom
| Command | Needs Phantom? | What it does with the vault |
|---|---|---|
stack init | No (warns) | Only scaffolds .stack.toml. |
stack add | Yes | Writes provisioned secrets into the vault. |
stack import | Yes | Routes every key/value from a .env into the vault. |
stack remove | Yes | Deletes the service's secrets from the vault. |
stack env show / diff | Yes | Reads the vault's key list (never the values). |
stack doctor | Yes | Reveals each secret briefly to verify it's still valid upstream. |
stack exec | Yes | Forwards to phantom exec — Phantom does the proxy work. |
stack list, stack info | Yes (reveals presence only) | Checks whether each declared slot is in the vault. |
stack providers, stack templates, stack projects | No | Static catalog / registry data. |
Installing Phantom
# macOS
brew install ashlrai/phantom/phantom
# anywhere Node runs
npm i -g phantom-secrets
# verify
phantom --version
phantom status For the full Phantom story, see phantom.ashlr.ai.