Connect Cursor to your whole stack without pasting a token

Cursor agents need GitHub, Linear, and Postgres access, and the usual fix is pasting tokens into .cursor/mcp.json or .env where they get committed and read by the agent. Here is how to keep keys out of Cursor entirely.

May 27, 202613 min read

Connect Cursor to your whole stack without pasting a token.

You opened Cursor, wired up the GitHub MCP server so the agent could read issues and open PRs, added the Postgres server so it could inspect your schema, and maybe Linear and Sentry on top of that. Each one wanted a credential. So you did the obvious thing: you pasted a personal access token and a database URL into .cursor/mcp.json, restarted, and it worked.

It works. That is the problem. It works right up until the moment that file ends up somewhere it should not, or the agent reads it back while building context, or you hand the project to a teammate and your ghp_... token rides along in Git history.

This post is about the gap between "it works" and "the secret is actually safe," specifically for Cursor. We will look at how Cursor handles MCP credentials, where the common patterns leak, what the secrets-injection tools do well, and the one move that is genuinely different: keeping the real key out of Cursor and out of the agent's process entirely.

How Cursor loads MCP credentials

Cursor reads MCP configuration from two files and merges them: .cursor/mcp.json in your project, and ~/.cursor/mcp.json globally. A local (STDIO) server entry looks like this:

json
{
  "mcpServers": {
    "github": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_paste_your_real_token_here" }
    },
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres",
               "postgresql://user:password@localhost:5432/mydb"]
    }
  }
}

This is the canonical shape. The MCP server is a child process. Cursor launches it with command and args, and whatever you put in env becomes that process's environment. The GitHub server reads GITHUB_PERSONAL_ACCESS_TOKEN; the Postgres server takes a connection string straight on the command line. For remote (HTTP/SSE) servers, the equivalent is a headers object carrying a bearer token.

Three things follow from this design, and all three bite.

First, the secret sits in a plaintext file on disk. .cursor/mcp.json is a normal project file. If you put a literal token in env, that token is now in that file in the clear.

Second, that file is in your project. People forget to add it to .gitignore, commit it, push it, and the token is in Git history. Even if you delete it in a later commit, it is still recoverable from history, and rotating it is the only real remedy.

Third, the value you paste is almost always more privileged than the task needs. A classic GitHub PAT with repo scope can read and write every repository you can touch. A DATABASE_URL with your app's credentials can do anything that role can do. You handed the agent a master key to do a job that needed a single drawer.

The "just use env vars" answer, and why it only half works

The intuitive fix is: do not put the literal in the file, reference an environment variable instead. Cursor documents this. You can write ${env:NAME} and Cursor will substitute the value from your shell environment, and for STDIO servers you can point at a .env file with an envFile key.

json
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_TOKEN}" }
    }
  }
}

In many local setups this works. But there are two real problems with leaning on it.

The first is reliability. Interpolation is documented, but community reports suggest it has been inconsistent across Cursor versions. There are reports that ${env:NAME} is not always resolved inside headers for remote HTTP/SSE servers, so the literal string ${env:VAR} can get sent to the server instead of the value, and others report it failing to pick up variables defined in shell rc files. The behavior appears to be version-dependent, so treat it as "exists but can be flaky," not "always works." envFile is STDIO-only; remote servers do not support it. The friction here is exactly what pushes frustrated people back to pasting the raw value, which is the thing we are trying to avoid.

The second problem is deeper and survives even when interpolation works perfectly: .gitignore does not hide a secret from the agent. Cursor's whole job is to read your project files to build context. It can read .env. Coding agents have been observed surfacing keys they read from .env, for example by writing a key into a generated file or echoing it into chat. .gitignore stops a git commit. It does nothing about the agent that is actively reading your working tree. Cursor does offer .cursorignore to exclude files from indexing and context, and you should use it, but excluding a file is a softer guarantee than not having the secret on disk at all.

Warning

.gitignore is a commit filter, not an access control. It prevents a secret from being committed. It does not stop Cursor's agent from reading that file while it works, and it does not stop the live token from sitting in the MCP server's process environment where a buggy or over-eager tool can read and leak it.

We dug into the broader version of this in stop putting API keys in environment variables. The short version: an environment variable is readable by the process and every child it spawns, and an agent is a process that spawns a lot of children.

OAuth helps, for the servers that support it

There is a genuinely cleaner path for some servers. Cursor supports OAuth for remote MCP servers, where a server advertises its OAuth configuration and Cursor manages the token after a one-time browser authorization. When a server speaks OAuth, you authorize once in the browser and Cursor holds the token. No raw secret in your config.

The catch is the long tail. The GitHub PAT, the Postgres connection string, the dozens of community STDIO servers that just read an env var: those are not going away. OAuth is the right answer when it is available, and it covers fewer servers than you would like. If you want the background on why device-and-browser OAuth flows are the better foundation for agents, the device code flow explained walks through it.

The secrets-injection pattern: keep it out of the file

The mature pattern for the STDIO long tail is to stop storing the secret in any file Cursor reads, and instead inject it at the moment the server launches. Three tools do this well, and they all share one shape: wrap the server command in a launcher that fetches the secret and hands it to the child process as an environment variable.

Doppler authenticates with a DOPPLER_TOKEN, fetches your project's secrets, and spawns the command with them in its environment. No .env written to disk.

json
{
  "mcpServers": {
    "github": {
      "command": "doppler",
      "args": ["run", "--", "npx", "-y", "@modelcontextprotocol/server-github"]
    }
  }
}

Infisical works the same way, with explicit environment and path selection, and --watch to reload on change.

json
{
  "command": "infisical",
  "args": ["run", "--env=dev", "--", "npx", "-y", "@modelcontextprotocol/server-github"]
}

1Password resolves op:// references at launch. The key detail: your .env holds only references, never values, so it is safe to keep on disk and even, if you like, to commit.

bash
# .env. safe to keep on disk; no real secret lives here
GITHUB_PERSONAL_ACCESS_TOKEN=op://Dev/GitHub MCP/token
json
{
  "command": "op",
  "args": ["run", "--env-file", ".env", "--",
           "npx", "-y", "@modelcontextprotocol/server-github"]
}

1Password has published guidance on referencing op:// secrets from .env for tooling like this instead of pasting tokens into config. This is a real improvement. The secret is no longer in your config, no longer in Git, and no longer sitting in plaintext on disk. If you already run one of these tools, use it. It is strictly better than the paste-it-in approach.

What injection still leaves on the table

Here is the honest limit of all three. Doppler, Infisical, and 1Password keep the secret out of the file. But the way they deliver it is to set the real, full secret as an environment variable in the MCP server process. That is the contract every MCP server expects. So the live token is now in the agent's process environment.

That matters because the threat model for an agent is not only "someone reads my config." It is also "the agent itself, or a tool the agent invokes, reads its own environment and leaks the value." Agents read files, run shell commands, generate code, and call tools whose behavior you did not write. Any of those can surface an environment variable. We walked through how an injected instruction turns into an exfiltrated key in how prompt injection becomes credential exfiltration. The mechanism does not care whether the token got into the environment by paste or by doppler run. Once it is in the process, it is reachable.

So there are two distinct problems here, and it is worth keeping them straight:

ConcernSecrets manager (Doppler · Infisical · 1Password)Credential broker
Where the secret is storedEncrypted, centralized, rotatableLocal encrypted vault
In mcp.json / .env as a literalNoNo
In the agent's process environmentYes, the real valueNo, only a placeholder
Reachable by the agent or its toolsYesNo
What it primarily solvesStorage, rotation, distributionThe key never reaches the agent

These overlap but they are not the same job. A secrets manager answers "where does the secret live and how is it rotated." A broker answers "the secret never reaches the agent at all." You can run both.

The broker move: inject at the proxy, not the process

The genuinely different approach is to run the MCP server, or any agent command, behind a local proxy that holds the real key and attaches it only as the request leaves your machine. Cursor's config and the server's environment carry a placeholder. The actual credential is swapped in on the outbound request to GitHub, Linear, or wherever, at the egress boundary. The agent never reads it, because it was never there.

Authsome is an open-source, local-first implementation of this. It is MIT licensed, it is a PyPI package, and it stores credentials in an encrypted SQLite vault under ~/.authsome/. No cloud, no account, no telemetry. Install it with uv:

bash
uv tool install authsome

You authorize a provider once. For GitHub this opens a browser PKCE flow:

bash
authsome login github

Then you launch your agent, or in this case the MCP server, under the proxy:

bash
authsome run -- npx -y @modelcontextprotocol/server-github

Inside that process, the GitHub credential is a placeholder, not a token. When the server makes its API call to api.github.com, the local proxy matches the destination and swaps in the real Authorization header on the way out. The token is never in mcp.json, never in .env, and never in the server's environment. To wire this into Cursor, the command becomes authsome and the rest of your invocation follows the --:

json
{
  "mcpServers": {
    "github": {
      "command": "authsome",
      "args": ["run", "--", "npx", "-y", "@modelcontextprotocol/server-github"]
    }
  }
}

That is the whole change. The same authsome run -- wrapper works for any command, which is why it slots in cleanly: every MCP server and agent framework already reads its credential from the environment, so a launch-time injector fits the existing convention without a code change. (LangChain's ChatOpenAI, for example, infers api_key from OPENAI_API_KEY if you do not pass it; agent frameworks generally read keys from env vars the same way. The broker simply makes that env var a placeholder.)

Note

Authsome ships 45 bundled providers, including GitHub, Linear, Slack, Notion, Google, and OpenAI. Stripe and Anthropic are not bundled; you add them as a custom provider with a small JSON file in ~/.authsome/providers/. A Postgres connection string is not an OAuth or API-key provider in the usual sense, so for a raw database URL a secrets manager like the ones above remains the natural fit. Be honest with yourself about which tool covers which credential.

To be fair about scope: a broker is not a replacement for a secrets manager's storage and rotation features, and a secrets manager is not a replacement for keeping the key out of the agent. If you already centralize secrets in Doppler or 1Password and you are happy with that, keep doing it. The broker is the additional layer for the credentials you care most about, the ones you do not want the agent able to read even by accident.

Multiple accounts, and the rest of the day-to-day

One real benefit of brokering, beyond the safety story, is account juggling. If you have a work GitHub and a personal one, you authorize both connections and pick a default:

bash
authsome login github --connection work
authsome login github --connection personal
authsome set-default github work

We went deep on that workflow in managing multiple GitHub accounts for AI agents, since it is a daily annoyance that the broker boundary happens to solve for free.

Every credential read, refresh, login, and revoke is written to an append-only JSONL audit log, so you can see when each credential was used. Rotating the vault master key is one command:

bash
authsome rekey

Per-provider token refresh happens automatically, so short-lived OAuth tokens stay fresh without you babysitting them. That lines up with where current best-practice guidance tends to point: eliminate long-lived static secrets, scope tightly, inject at runtime, and prefer ephemeral credentials. The OWASP Secrets Management Cheat Sheet is a good reference for the general principles. If you want the wider survey of how brokers compare to gateways and proxies, agent credential brokers in 2026 and top agent proxy tools, what to know lay out the landscape.

A practical checklist for Cursor

If you take one thing from this, make it the ladder, from worst to best:

  1. Literal token in env or headers. Avoid. It lands in a plaintext file in your project and ends up in Git.
  2. ${env:...} interpolation or envFile. Better than a literal, but reportedly inconsistent across versions and unreliable for remote headers. The secret is still on disk in your shell environment or a .env, and still readable by the agent.
  3. Secrets injector (doppler run, infisical run, op run). The secret leaves your config and your Git history. The live value still lands in the server's process environment.
  4. Credential broker at the proxy (authsome run --). The real key is never in mcp.json, never in .env, and never in the agent's environment. It is attached downstream on the outbound request.

Most teams should be at level 3 at minimum, today, for everything. For the credentials that would hurt the most if they leaked, level 4 is the one that takes the key off the table entirely.

The token you pasted into .cursor/mcp.json to get GitHub working is the same token that will leak when, not if, something reads it back. Moving it out of the file is good. Moving it out of the agent's reach is the actual fix.

Priyansh Khodiyar

Priyansh Khodiyar

Maintainer

Works on authsome and the agentr.dev tooling.