# API key prefixes: which service issued this key?

pplx-, tvly-, xai-, fw_, sk-ant-, ghp_, sk_live_, hf_: which vendor issued each key prefix, how to rotate it, and how to keep coding agents out of .env.

An API key's first few characters usually name the service that issued it. `pplx-` is Perplexity, `tvly-` is Tavily, `xai-` is xAI, `fw_` is Fireworks AI, `sk-ant-` is Anthropic, `ghp_` is GitHub. If you found one in a `.env`, a log, a commit or an agent transcript, the table below tells you whose dashboard to open, and the sections after it cover rotation and keeping coding agents out of `.env`.

## The prefix table

Each row is checked against the vendor's own docs. Where we could not confirm a prefix on a vendor page, the row says so instead of guessing.

| Prefix | Issuer | What it is | Source |
|---|---|---|---|
| `pplx-` | Perplexity | API key | [Perplexity API key management](https://docs.perplexity.ai/docs/admin/api-key-management) |
| `tvly-` | Tavily | API key | [Tavily headers and authentication](https://docs.tavily.com/documentation/api-headers) |
| `xai-` | xAI | API key | [xAI accounts and authorization](https://docs.x.ai/developers/rest-api-reference/management/auth) |
| `fw_` | Fireworks AI | API key | [Fireworks docs](https://docs.fireworks.ai/firepass) |
| `fpk_` | Fireworks AI | Fire Pass key | [Fireworks Fire Pass](https://docs.fireworks.ai/firepass) |
| `sk-ant-api` | Anthropic | Claude API key (`sk-ant-api03-...` in examples) | [Claude API authentication](https://platform.claude.com/docs/en/manage-claude/authentication) |
| `sk-ant-admin` | Anthropic | Admin API key | [Claude Admin API](https://platform.claude.com/docs/en/api/administration-api) |
| `sk-`, `sk-proj-` | OpenAI, widely reported | API key, project key | Unverified: no OpenAI page we could load states the prefix |
| `AIza` | Google | Google Cloud API key | [Google Cloud API keys](https://docs.cloud.google.com/docs/authentication/api-keys) |
| `ghp_` | GitHub | Personal access token (classic) | [GitHub token expiration and revocation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation) |
| `github_pat_` | GitHub | Fine-grained personal access token | same |
| `gho_` / `ghu_` / `ghs_` / `ghr_` | GitHub | OAuth app / user-to-server / server-to-server / refresh token | [GitHub's token formats post](https://github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/) |
| `sk_live_` / `sk_test_` | Stripe | Secret key, live / sandbox | [Stripe API keys](https://docs.stripe.com/keys) |
| `rk_live_` / `rk_test_` | Stripe | Restricted key | same |
| `pk_live_` / `pk_test_` | Stripe | Publishable key (safe in client code) | same |
| `hf_` | Hugging Face | User access token | [Hugging Face user access tokens](https://huggingface.co/docs/hub/security-tokens) |
| `r8_` | Replicate | API token | [Replicate API tokens](https://replicate.com/docs/topics/security/api-tokens) |
| `ntn_` | Notion | Integration token (since September 25, 2024) | [Notion changelog](https://developers.notion.com/page/changelog) |
| `secret_` | Notion (older tokens), also generic | Integration token created before the switch to `ntn_` | same |
| `up_` | Unverified | No vendor source found | — |
| `sn4_` | Unverified | No vendor source found | — |

Notes on the rows:

- **`sk-` versus `sk_`.** Anthropic and the OpenAI-style keys use a hyphen. Stripe uses an underscore. A `sk_live_` key is Stripe and it is live money.
- **`secret_` alone tells you little.** Notion used it until September 2024, and older Notion tokens still work. Other tools use it too. Check the variable name next to it in the file.
- **Notion advises against** validating its tokens with a regex, because the format can change. A prefix helps you triage a finding. It is not a contract.
- **`up_` and `sn4_`.** We found no vendor page that claims either one. Don't trust a blog that names an issuer without a link to the vendor. Look at the variable name and the code that reads it.

## Why a key prefix shows up in search

People usually land here for one of three reasons:

- **A scanner flagged it.** GitHub secret scanning, a pre-commit hook or a CI job reports a match and shows only the first few characters.
- **It showed up in an agent transcript.** A coding agent opened `.env` to debug something and quoted a value back.
- **It is in a file nobody remembers creating.** An old `.env` in a repo you just inherited.

The next step is the same each time: find the issuer, rotate the key, then close the leak. A key in a transcript or a public commit counts as leaked even if nobody has used it yet. Our piece on [how agents leak .env secrets](/blog/stop-your-agent-from-leaking-env-secrets) covers the usual paths.

## How to rotate each one

Rotate means: create a new key, deploy it wherever the old one was read, then disable or delete the old one.

| Issuer | Where | Notes from the vendor docs |
|---|---|---|
| Perplexity | API portal, or the `/generate_auth_token` and `/revoke_auth_token` endpoints | A revoked key cannot be recovered |
| Tavily | Your Tavily dashboard | Replace `TAVILY_API_KEY` |
| xAI | xAI Console, API Keys page | Keys are bound to a team; the full key is shown once |
| Fireworks AI | Fireworks account settings | Replace `FIREWORKS_API_KEY` |
| Anthropic | Claude Console, API keys page | Disable is reversible, Delete is permanent; set an expiration on the new key |
| OpenAI | OpenAI platform API keys page | Replace `OPENAI_API_KEY` |
| Google | Google Cloud console, API keys | Add API and application restrictions to the new key |
| GitHub | Your GitHub token settings | `POST /credentials/revoke` takes a `ghp_` or `github_pat_` token and needs no authentication |
| Stripe | Dashboard, API keys | Stripe says to rotate a leaked key immediately; prefer a restricted `rk_` key for the replacement |
| Hugging Face | Settings → Access Tokens | `POST https://huggingface.co/api/credentials/revoke` revokes leaked tokens in bulk |
| Replicate | replicate.com/account/api-tokens | Disable the token, create a new one |
| Notion | Your integration's settings | New tokens come out as `ntn_` |

Two vendor tips. Stripe's [key best practices](https://docs.stripe.com/keys-best-practices) suggest searching your code, config and CI for `sk_live_` and `rk_live_`. Anthropic's docs suggest an expiration on every key, so a leaked one stops working on its own.

## Keep coding agents out of .env

Rotation fixes the last leak. These settings make the next one less likely. Each tool's controls are different, and each has a gap.

### Claude Code

Add `Read` deny rules in `.claude/settings.json` ([Claude Code permissions](https://code.claude.com/docs/en/permissions)):

```json
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ]
  }
}
```

Bare filenames follow gitignore rules, so `Read(.env)` matches any `.env` at or under the current directory. Use `//` for an absolute path and `~/` for your home directory.

Per the docs, Read deny rules cover the built-in file tools, Bash file commands Claude Code recognizes (`cat`, `head`, `tail`, `sed`, `tee`) and redirect targets. They do not cover `grep -r pattern .` run from the directory that holds the file, or a Python or Node script that opens the file itself. For that, the docs point to the [sandbox](https://code.claude.com/docs/en/sandboxing). A `.claudeignore` file has no effect. Our [deny rules not working](/blog/claude-code-deny-rules-not-working) post lists the reported bypasses.

### Codex

Codex blocks paths through a permission profile in `~/.codex/config.toml` ([Codex permissions](https://developers.openai.com/codex/permissions)):

```toml
default_permissions = "project-edit"

[permissions.project-edit]
extends = ":workspace"

[permissions.project-edit.filesystem]
glob_scan_max_depth = 3

[permissions.project-edit.filesystem.":workspace_roots"]
"." = "write"
"**/*.env" = "deny"
```

`deny` blocks both reads and writes from sandboxed commands, and deny wins over write and read on the same path. Two limits from the docs: on Linux, WSL and native Windows, deny globs are expanded when each sandboxed process starts, so a `.env` created later stays readable to that process; and an unbounded `**` pattern needs `glob_scan_max_depth`. An admin can enforce reads denied for every user with `deny_read` under `[permissions.filesystem]` in `requirements.toml`.

Check it on your version. [openai/codex#22179](https://github.com/openai/codex/issues/22179) reported deny globs not blocking reads in 0.130.0 under the older syntax. For a hook that blocks edits to `.env`, see [Codex hooks examples](/blog/codex-hooks-examples).

### Cursor

Cursor ignores `.env*` by default, along with everything in `.gitignore` ([Cursor ignore files](https://cursor.com/docs/context/ignore-files)). Ignored files are blocked from Agent, Tab and `@` mentions. Add more in `.cursorignore`:

```
**/.env
**/.env.*
**/*.pem
```

The gap, in Cursor's words: "The terminal and MCP server tools used by Agent cannot block access to code governed by `.cursorignore`." An agent that runs `cat .env` in the terminal still reads it. Cursor also notes that complete protection isn't guaranteed.

### OpenCode

OpenCode 2's default policy asks before reading `*.env` and `*.env.*`, and allows `*.env.example` ([OpenCode permissions](https://opencode.ai/docs/permissions/)). To deny instead of ask, add rules to `opencode.jsonc`. The last matching rule wins, so the exception goes last:

```jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "permissions": [
    { "action": "read", "resource": "*.env", "effect": "deny" },
    { "action": "read", "resource": "*.env.*", "effect": "deny" },
    { "action": "read", "resource": "*.env.example", "effect": "allow" }
  ]
}
```

This is the V2 format: `permissions`, `shell` and `subagent` replace V1's `permission`, `bash` and `task`. The read rule covers the read tool. A shell command that prints the file is a separate `shell` action, so review your shell rules too.

## What this does not cover

A prefix names the issuer. It does not prove the key is live, and it does not tell you who used it. Only the vendor's usage logs answer that.

Every agent control above lives inside one tool. Each has a documented way around it, usually the shell, and none of them carries over to the next tool. The strongest fix is to keep real secrets out of files an agent can open. Load them from a secrets manager at run time and keep only `.env.example` in the repo.

ACP can add a record. It checks each tool call from the harness hooks and logs it, so you can see which commands touched which files, and it redacts secrets in the stored previews. Be clear about the limit: ACP's read-and-send detector (a sensitive file like `.env` named in the same command as `curl`, `wget`, `scp` or `nc`) ships off unless a preset turns it on. Don't count on ACP to stop a `.env` read. The install box below sets up the logging in one line.

## Frequently asked questions



## Where to read more

- [Stop your agent from leaking .env secrets](/blog/stop-your-agent-from-leaking-env-secrets): how the leaks happen
- [Claude Code permissions](https://code.claude.com/docs/en/permissions): Read deny rules and what they cover
- [Codex permissions](https://developers.openai.com/codex/permissions): permission profiles and deny globs
- [Cursor ignore files](https://cursor.com/docs/context/ignore-files): defaults and the terminal gap
- [OpenCode permissions](https://opencode.ai/docs/permissions/): V2 rule format and defaults
- [GitHub token expiration and revocation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation): every GitHub prefix
