# Sensitive Vercel environment variables cannot be pulled back

> Vercel sensitive (now Secret) environment variables are write only. What that meant when a key was needed from another project, and where to keep real secrets.

Canonical: https://www.notchstar.com/blog/vercel-sensitive-env-variables-cannot-be-pulled/
Last updated: 2026-11-04

*A write only variable means exactly that: what happened when a key was needed from another project, and how to plan for it*

Published 2026-11-04.

## TL;DR

- A Vercel environment variable stored as Sensitive (the current docs call the type **Secret**) is write only after it is saved. The dashboard hides the value and `vercel env pull` cannot export it.
- That behaviour is the point of the feature, but it bites when a second project needs the same value and the only copy is inside the first project's settings.
- In NotchStar's case the value was a PostHog project key, which is public by design, so it could be recovered from a live site bundle. A real secret has no such way out. The rule that follows: keep the original in a password manager or a secrets store, not in the host.

## What happened

The PostHog project is shared between products (`docs/ANALYTICS.md`). The app sends an opt in daily event with a project key compiled into the source, and the code says what kind of key it is:

```swift
/// A PostHog project key. It is public by design and can only write events.
static let projectKey = "phc_PLACEHOLDER"
```

A second Vercel project needed the same key. The first project had it as an environment variable marked sensitive. The value could not be read back from that project's settings. The project notes record it in one line (`blog/FACTS.md`, section B, item 7): variables marked sensitive cannot be pulled back, so the key could not be read from the other project's settings. The key was found in a live site bundle instead, because a PostHog project key is meant to be public.

That is the whole incident. Nothing was lost, nothing leaked, no outage happened. It is thin material for a post on its own, and the exact error text and CLI output were not recorded at the time, so none are quoted here. What is worth writing down is what the platform promises, what it has changed since, and what to do about it.

## What the platform says

Vercel's documentation, fetched on 4 November 2026 (page last updated 28 August 2026), describes the two types this way:

- **Config** values stay readable after saving, for members with access.
- **Secret** values can only be written after saving, never read back.

It also says that existing Sensitive variables continue to work as Secrets, and that a saved Secret cannot be converted to Config in place. To store the same key as Config, delete the Secret and create a new Config variable. For a Secret, the current value is hidden, and providing a new value rotates it.

So the behaviour that surprised the project is documented, and the name has moved on. A reader who searches for "sensitive environment variables" will find a page titled "Config and Secret environment variables".

What `vercel env pull` does with such a value is in the CLI reference, fetched the same day (page last updated 20 August 2026):

> For Secret variables whose values cannot be read from Vercel, the CLI keeps the real value already in your local file. If your local file has no real value for that key, the CLI writes a placeholder for you to replace.

That sentence describes today's CLI. Whether the CLI behaved the same way when the key was needed is unknown, so the incident above is described from the project notes and not from this documentation.

## The defaults changed the question

The CLI reference also lists which type a variable gets by default when it is added from the command line:

| Target | Default type |
|---|---|
| Production | Secret |
| Preview | Secret |
| Development | Config |
| Custom environments | Secret |

Variables with a public framework prefix such as `NEXT_PUBLIC_` are stored as Config. Secrets are now allowed in Development too, which reverses the earlier rule that limited Sensitive variables to Production and Preview.

Two consequences follow. First, a variable added in Production with no flags is write only from the moment it is created, so the question "where is the original" has to be answered at creation time. Second, a value that is not secret at all, such as an analytics project key, needs `--no-sensitive` or `--type config` to stay readable:

```bash
vercel env add API_URL production --value "https://api.example.com" --no-sensitive --yes
```

That command is quoted from the CLI reference. The value is the reference's own example, not a project value.

## The wrong turn

The wrong turn was treating the host as the place where the value lives. A hosting dashboard is a place where a value is used. When a value is write only, the platform cannot be the source of truth, and the first time a second consumer appears the original has to come from somewhere else.

For a public key that was easy to fix. The browser bundle of a site that uses the key contains it, and so does the app's source. For a private key, such as a payment provider's API key (the site's checkout function reads one from `DODO_PAYMENTS_API_KEY`), nothing public contains it. The only paths are the provider's dashboard, where a new key can be issued, or the original copy kept elsewhere. Rotating a key so it can be shared again is fine. It also means every other consumer of the old key breaks.

## What to do instead

A short routine that keeps a write only variable from becoming a trap:

1. **Decide the type at creation.** If the value is public by design, store it as Config. If not, store it as a Secret and write down where the original is.
2. **Keep the original outside the host.** A password manager entry or a secrets manager holds the value. The host receives a copy.
3. **Name the consumers.** A one line note per key listing every project that uses it makes rotation a checklist and not a search.
4. **Let a local file hold local values.** The CLI merges a pull into an existing file and keeps the real local value for a Secret. A gitignored env file per developer is therefore a stable place for local copies. This repository ignores `.env.posthog`, `.env.blog` and `.signing/` for that reason.
5. **Rotate instead of recovering.** If the original is lost, issue a new key at the provider, update every consumer, and revoke the old one.

> **Lesson:** A write only variable cannot be recovered by anyone, including the person who set it. That is a security property. It also means the host is not a backup.
## What is still bad

- **The incident is described from notes.** The error text, the CLI version and the exact steps tried were not recorded. Behaviour has changed since: the docs now use Config and Secret, and the pull command writes placeholders.
- **Which project held the key is not stated here.** The shared PostHog project is documented in `docs/ANALYTICS.md`, which says to filter events by `properties.product = 'notchstar-app'`, but the names of the Vercel projects involved are not needed for the lesson and are left out.
- **No automated check.** Nothing in the repository verifies that every secret has an original stored elsewhere. It is a habit, not a test.
- **Build logs.** The docs say Vercel replaces a Secret in build logs with `[REDACTED]` when the value is 32 characters or longer and appears in the log. A shorter value is not covered by that rule, and the docs do not say what happens to a shorter value.

## Try it yourself

You can see the behaviour on a throwaway project without risking anything. Use a made up value.

1. Create a test project and add a variable as a Secret from the CLI:

```bash
vercel env add DEMO_VALUE production --value "not-a-real-secret" --sensitive --yes
```

2. Open the project's Environment Variables page and try to view the value. The Secret label should show and the value should stay hidden.
3. Pull the variables into a scratch file and read what was written for that key:

```bash
vercel env pull .env.scratch --environment=production
grep DEMO_VALUE .env.scratch
```

4. Remove the variable with `vercel env rm DEMO_VALUE production --yes`, add it again as Config with `--no-sensitive`, and repeat step 3. The Config value is exported, the Secret is not. Delete the scratch file and the project when you are done.

The docs describe both results. Running steps 2 to 4 once on a throwaway project is the way to see which wording your CLI version uses. If you use more than one Vercel account, check which one the CLI is logged in as before running anything.

## Cheat sheet

| Question | Answer |
|---|---|
| Can a saved Secret be read in the dashboard | No, the value is hidden |
| Can `vercel env pull` export it | No, it keeps your local value or writes a placeholder |
| Can a Secret be turned into Config | Not in place: delete it and create a Config variable |
| How do I change a Secret | Provide a new value, which rotates it |
| Default type for Production and Preview | Secret |
| Keep a value readable | `--no-sensitive` or `--type config` |

- [ ] Public by design values stored as Config
- [ ] Every real secret has an original in a password manager or secrets store
- [ ] A note lists which projects use each key
- [ ] Local env files are gitignored and hold local copies
- [ ] A lost original means rotate, not recover

If you want to see the app that sends the daily event when you allow it, NotchStar is at [notchstar.com](https://www.notchstar.com/). The privacy page lists the exact payload.

## More posts

- [Vercel rewrites to an external host returned the site's own 404 with trailingSlash](https://www.notchstar.com/blog/vercel-rewrite-trailing-slash-404/)
- [Why a disk tool should only ever move to the Trash](https://www.notchstar.com/blog/disk-cleaner-only-move-to-trash-swift/)
