This guide walks through the most common Phantom workflow: a solo developer using Claude Code (or Cursor) with a Next.js/Node.js project deployed to Vercel. By the end, your AI coding tools will never see a real secret again.
The fastest way to get started is with npm (downloads the correct binary automatically):
npx phantom-secrets initThis installs Phantom AND initializes your project in one command. Alternatively:
# npm global install
npm install -g phantom-secrets
# Homebrew (macOS)
brew tap ashlrai/phantom && brew install phantom
# Cargo (from source)
cargo install --git https://github.com/ashlrai/phantom-secrets phantomVerify the install:
phantom --versionAdd Phantom's MCP server so Claude can manage your secrets directly:
claude mcp add phantom-secrets-mcp -- npx phantom-secrets-mcpOnce configured, you can just tell Claude: "Protect my API keys" and it will handle everything.
Navigate to your project directory and run phantom init:
cd my-nextjs-app
phantom initHere is what happens:
- Phantom reads your
.envfile and identifies secrets using heuristics (key patterns like*_API_KEY,*_SECRET*,*_TOKEN; value patterns likesk-*,ghp_*, connection strings). - Real secret values are stored in your OS keychain (macOS Keychain / Linux Secret Service). They never exist on disk inside your project directory again.
- Your
.envfile is rewritten in place -- real values are replaced with phantom tokens (random 256-bit tokens prefixed withphm_). - A
.phantom.tomlconfig file is created in your project root.
$ phantom init
Found 3 secret(s) to protect: OPENAI_API_KEY, ANTHROPIC_API_KEY, DATABASE_URL
Rewrote .env with phantom tokens
Saved real secrets to OS keychain
Your .env now looks like this:
# Managed by Phantom -- do not edit phantom tokens manually
OPENAI_API_KEY=phm_a7f3b9e2c4d1f8a3b6e9d2c5f8a1b4e7
ANTHROPIC_API_KEY=phm_d4e7a0b3c6f9e2d5a8c1b4f7e0d3a6c9
DATABASE_URL=phm_b1c4d7e0a3f6b9c2d5e8a1b4c7d0e3f6
NODE_ENV=development
PORT=3000Non-secret values like NODE_ENV and PORT are left untouched.
If your .env is at a non-standard path:
phantom init --from .env.localYour primary daily command is phantom exec. It starts the local proxy, runs your command, and tears everything down when the command exits.
phantom exec -- claudephantom exec -- cursor .- Phantom starts a local HTTP proxy on
127.0.0.1(ephemeral port). - It sets environment variables so SDKs route through the proxy:
OPENAI_BASE_URL=http://127.0.0.1:<port>/openaiANTHROPIC_BASE_URL=http://127.0.0.1:<port>/anthropic- (and similar for other configured services)
- Your AI tool launches. It reads
.envand sees onlyphm_tokens. - When your code makes an API call, the request hits the local proxy. The proxy swaps phantom tokens for real secrets in the request headers/body, then forwards the request over TLS to the real API.
- When you exit Claude Code (or Cursor), the proxy shuts down. The phantom tokens become inert -- they are meaningless outside the proxy.
The AI agent never sees, logs, or transmits a real secret.
The key insight: no code changes are required. Your application code stays exactly the same.
The OpenAI and Anthropic SDKs (and most API clients) respect *_BASE_URL environment variables. When Phantom sets OPENAI_BASE_URL, the SDK sends requests to the local proxy instead of api.openai.com directly. The proxy handles the rest.
Your code does not change at all:
// This code works identically with or without Phantom
import OpenAI from "openai";
const openai = new OpenAI();
// SDK reads OPENAI_API_KEY (gets phm_ token) and
// OPENAI_BASE_URL (gets http://127.0.0.1:<port>/openai)
// Proxy swaps phm_ for the real key before forwarding
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
});// Same for Anthropic
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
// SDK reads ANTHROPIC_API_KEY (gets phm_ token) and
// ANTHROPIC_BASE_URL (gets http://127.0.0.1:<port>/anthropic)Database connections are not HTTP-based, so the proxy cannot intercept them. For secrets like DATABASE_URL, Phantom injects the real value directly as an environment variable inside the exec session. The .env file still contains only the phantom token, so the AI never sees the real connection string.
Add custom API services in .phantom.toml:
[services.custom_api]
secret_key = "MY_CUSTOM_KEY"
pattern = "api.example.com"
header = "X-Api-Key"
header_format = "{secret}"Phantom can push your real secrets directly to Vercel's environment variables, so you never copy-paste secrets into a web dashboard.
phantom sync --platform vercel --project prj_abc123def456This reads real secret values from your OS keychain and sets them as environment variables in your Vercel project. Phantom tokens are never uploaded -- Vercel gets the real values.
Your project ID is in the Vercel dashboard under Settings > General, or in your local .vercel/project.json:
cat .vercel/project.json
# {"projectId": "prj_abc123def456", "orgId": "team_xyz"}You can also save the target in .phantom.toml so future syncs do not require the --project flag:
[sync.vercel]
project = "prj_abc123def456"Then simply run:
phantom sync --platform vercelNote: You need the Vercel CLI authenticated (vercel login) or a VERCEL_TOKEN set for this to work.
Starting fresh on a new laptop or CI runner? Pull secrets from a platform you have already synced to:
phantom pull --from vercel --project prj_abc123def456This fetches the real secret values from Vercel, stores them in your local OS keychain, and writes phantom tokens to your .env file.
If you already have some secrets locally and want to overwrite them with the platform values:
phantom pull --from vercel --project prj_abc123def456 --forceFor Railway projects, specify the environment and optionally a service:
phantom pull --from railway --project <project-id> --environment production --service <service-id>Phantom can generate a .env.example file that lists all required variable names without any values (real or phantom):
phantom envThis creates .env.example:
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
DATABASE_URL=
NODE_ENV=development
PORT=3000Non-secret values are preserved as-is. Secret values are left blank.
To write to a different filename:
phantom env --output .env.template| File | Commit? | Why |
|---|---|---|
.env.example |
Yes | Shows teammates what variables are needed |
.phantom.toml |
Yes | Shares proxy/service config with the team |
.env |
No | Contains phantom tokens specific to your vault |
.env.local |
No | Same reason |
Add to your .gitignore:
.env
.env.local
.env*.localWhen a new developer joins:
- They clone the repo and see
.env.example. - They copy it to
.envand fill in their own API keys. - They run
phantom initto protect those secrets. - Or, if secrets are already in Vercel:
phantom pull --from vercel --project prj_abc123def456.
phantom check scans staged files for unprotected secrets. Set it up as a git hook to block accidental leaks before they reach your repository.
Add to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/ashlrai/phantom-secrets
rev: v0.3.0
hooks:
- id: phantom-checkThen install:
pre-commit installecho '#!/bin/sh
phantom check' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit- Real API keys in
.envfiles (values that should be phantom tokens) - Hardcoded secrets in staged source files (patterns like
sk-*,ghp_*,AKIA*)
$ phantom check
BLOCKED Unprotected secrets detected!
! .env has 1 unprotected secret(s):
- OPENAI_API_KEY
fix Run phantom init to protect your secrets.
If everything is clean, phantom check exits silently with code 0.
For a deeper integration, Phantom provides an MCP server that lets Claude Code manage secrets natively -- listing, adding, and rotating secrets without ever exposing real values in the conversation.
phantom setupThis configures the MCP server in your Claude Code settings and sets up the proxy to start automatically.
Add to .claude/settings.json (project-level) or your global Claude Code settings:
{
"mcpServers": {
"phantom": {
"command": "phantom-mcp",
"args": []
}
}
}Once configured, Claude Code gains these tools:
| Tool | What it does |
|---|---|
phantom_list_secrets |
List secret names (never shows values) |
phantom_status |
Show vault, config, and proxy status |
phantom_init |
Initialize Phantom and protect .env secrets |
phantom_add_secret |
Add a secret to the vault |
phantom_remove_secret |
Remove a secret from the vault |
phantom_rotate |
Regenerate all phantom tokens |
phantom_cloud_push |
Push encrypted vault to Phantom Cloud |
phantom_cloud_pull |
Pull vault from Phantom Cloud |
phantom_cloud_status |
Check cloud auth and sync status |
Claude can call these 17 tools during a session. For example, if you say "add my new Stripe key," Claude can use phantom_add_secret to store it safely -- the real value passes through the MCP protocol but never enters Claude's context window or conversation logs.
Phantom is currently designed for solo developers. Each developer manages their own vault independently -- there is no shared team vault or centralized secret management server (yet).
Each developer on your team runs phantom init independently on their own machine. This means every team member has their own local vault backed by their OS keychain (or an encrypted file vault).
The recommended workflow for sharing secrets between developers:
-
One developer syncs to a deployment platform:
phantom sync --platform vercel --project prj_abc123def456
-
Other developers pull from that platform:
phantom pull --from vercel --project prj_abc123def456
This uses Vercel (or Railway) as the shared source of truth. Each developer ends up with the same real secret values in their own local vault, but with independently generated phantom tokens.
Use phantom env to generate a .env.example file that new developers can reference:
phantom envThis lists all required variable names without exposing any values. Commit .env.example to your repo so new teammates know which secrets they need.
.phantom.toml can (and should) be committed to git. It contains proxy configuration and service definitions -- no secrets. This ensures every developer on the team uses the same Phantom configuration.
- There is no real-time secret sharing. If one developer rotates an API key, they need to
phantom syncand every other developer needs tophantom pullagain. - Phantom tokens are unique per developer. You cannot share
.envfiles between machines and expect them to work. - There is no access control or audit log for who accessed which secret.
The Pro tier ($8/mo) adds unlimited cloud vaults and multi-device sync. Cloud sync uses end-to-end encryption (ChaCha20-Poly1305) -- the server never sees your plaintext secrets. See the Cloud Sync section for details. A future update will add shared team vaults with centralized access control, audit logging, and automatic propagation of secret updates across team members.
Phantom works in CI/CD environments where no OS keychain is available. In these environments, Phantom uses an encrypted file vault instead, unlocked by the PHANTOM_VAULT_PASSPHRASE environment variable.
Set PHANTOM_VAULT_PASSPHRASE and any required platform tokens as GitHub repository secrets, then install Phantom and pull secrets at the start of your workflow:
name: Build and Test
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Phantom
run: cargo install phantom --git https://github.com/ashlrai/phantom-secrets
- name: Pull secrets from Vercel
run: phantom pull --from vercel --project ${{ vars.VERCEL_PROJECT_ID }}
env:
PHANTOM_VAULT_PASSPHRASE: ${{ secrets.PHANTOM_VAULT_PASSPHRASE }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
- name: Run tests with secrets injected
run: phantom exec -- npm test
env:
PHANTOM_VAULT_PASSPHRASE: ${{ secrets.PHANTOM_VAULT_PASSPHRASE }}In Docker builds, install Phantom and pass the vault passphrase at runtime (never bake it into the image):
FROM rust:1.77 AS builder
RUN cargo install phantom --git https://github.com/ashlrai/phantom-secrets
FROM node:20-slim
COPY --from=builder /usr/local/cargo/bin/phantom /usr/local/bin/phantom
COPY . .
# Pass PHANTOM_VAULT_PASSPHRASE at runtime via docker run -e
CMD ["phantom", "exec", "--", "node", "server.js"]Run the container with the passphrase:
docker run -e PHANTOM_VAULT_PASSPHRASE="your-passphrase" my-appWhen no OS keychain is detected (which is the case on virtually all CI runners and Docker containers), Phantom falls back to an encrypted file vault stored at ~/.phantom/vaults/. This vault is encrypted with the passphrase provided via PHANTOM_VAULT_PASSPHRASE. The passphrase must be set before any Phantom command that accesses secrets (pull, exec, init).
The encrypted file vault provides the same security guarantees as the OS keychain -- secrets are encrypted at rest and only decrypted in memory when needed.
Everything still works. Your .env contains phantom tokens, which are harmless -- they look like gibberish API keys and will simply fail authentication if used directly. The teammate can replace them with their own real keys, or install Phantom themselves.
Phantom deliberately never displays real values. This is a security feature. If you absolutely need to retrieve a value:
- macOS: Open Keychain Access and search for "phantom"
- Linux: Use your Secret Service client (e.g.,
secret-toolor Seahorse) - Or: Check the deployment platform (Vercel dashboard, Railway dashboard) where you synced the secrets
The app will send the phantom token (phm_...) directly to the API, which will reject it as an invalid key. This is by design -- phantom tokens are worthless outside the proxy. Start the proxy with phantom exec or phantom start before running your app.
Yes. In environments without an OS keychain (Docker, CI runners), Phantom falls back to an encrypted file vault. Use phantom pull to populate secrets from your deployment platform at the start of the CI job.
Yes. Run phantom init in each package/app directory that has its own .env. Each gets its own .phantom.toml and set of phantom tokens. The proxy handles all of them in a single phantom exec session.
Run phantom init again. It detects new unprotected secrets, adds them to the vault, and rewrites the .env. Existing phantom tokens are preserved.
phantom rotateThis regenerates all phantom tokens in your .env without affecting the real secret values in the vault. Useful if you suspect a token mapping was exposed.
The proxy binds to 127.0.0.1 only -- it is never exposed to the network. It uses TLS (via rustls) for all outgoing connections to real APIs. Secrets are zeroized from memory after injection. See SECURITY.md for the full threat model.
phantom doctorThis verifies your vault, config file, keychain access, and proxy configuration. Run it whenever something feels off.
Sync your vault across machines with end-to-end encryption. The server never sees your plaintext secrets.
phantom loginThis opens your browser for GitHub OAuth. Once authenticated, your device is linked to your Phantom Cloud account.
phantom cloud pushYour vault is encrypted client-side with ChaCha20-Poly1305 before upload. The encryption key lives only in your OS keychain.
phantom login # authenticate the new device
phantom cloud pull # download and decrypt your vault- Free: 1 cloud vault (unlimited local vaults)
- Pro ($8/mo): Unlimited cloud vaults, multi-device sync
Export an encrypted backup file (independent of cloud sync):
phantom export --passphrase mypassword
# Creates phantom-export.enc
phantom import phantom-export.enc --passphrase mypassword
# Restores secrets from backup