How to Protect API Keys From a Rogue VS Code Extension

The GitHub breach started in a VS Code extension. Learn how to protect API keys so a compromised IDE tool or leaked repo cannot hand out working keys.

Ashley Innocent

Ashley Innocent

11 June 2026

How to Protect API Keys From a Rogue VS Code Extension

Apidog for Enterprise

On-Premises Deploy

SSO & RBAC

SOC 2 Compliant

Explore Apidog Enterprise

On May 20, 2026, GitHub confirmed that attackers stole data from roughly 3,800 of its internal code repositories. The entry point was not a zero-day in GitHub’s servers. It was a poisoned VS Code extension installed on a single employee’s laptop. Once that extension ran with the same file-system permissions as the developer, it could read everything in reach: source code, config files, and any credentials sitting in the workspace. If you want to protect API keys from the same class of attack, the lesson is uncomfortable but simple. The weakest link is rarely the cloud. It is the developer machine, and the tools running on it.

TL;DR

To protect API keys against a compromised IDE extension or a leaked repository, stop storing live credentials where dev tools can read them. Keep secrets out of source code and out of committed .env files. Treat .gitignore as a convenience, not a security control. Scope every key to a single environment, use short-lived and least-privilege credentials, and rotate on a schedule. Tools like Apidog help by keeping API credentials in environment variables with local-only values instead of loose plaintext scattered across your repo and workspace.

button

Why the GitHub breach is a wake-up call for every developer

GitHub’s incident reads like a textbook supply-chain attack. The threat group, tracked as TeamPCP, has a history of trojanizing packages across npm, PyPI, and PHP ecosystems. This time the malicious payload rode in on a VS Code extension. According to TechCrunch’s reporting, the attackers exfiltrated data from about 3,800 internal repositories and are now selling the dataset for more than $50,000 on underground forums. GitHub says it has no evidence that customer data stored outside those internal repos was affected, and the investigation is ongoing.

Here is the part that should make you pause. A VS Code extension is just code. When you install one, it runs inside the editor process with your user permissions. It can list files, open them, and read their contents. It can watch for file changes. It can make outbound network requests. None of that is a vulnerability; it is the extension model working as designed. Most extensions need file access to do their job. The problem is that a malicious or compromised extension uses the exact same access to harvest whatever it finds.

What does it find? In a typical project, it finds a lot. A .env file at the repo root. A config/secrets.yml. A hardcoded token in a quick test script you meant to delete. AWS credentials in ~/.aws/credentials. A .npmrc with an auth token. SSH keys. The TeamPCP group is the same actor behind the “Mini Shai-Hulud” npm worm campaign, which harvested developer, CI/CD, cloud, and AI-tooling credentials from infected machines. The pattern is consistent: get code running on a developer’s box, then scrape for anything that looks like a credential.

This is not new in kind, only in profile. We wrote about a similar exposure pattern in our breakdown of the API security lessons from the Vercel breach, and the supply-chain mechanics line up closely with what we covered in the npm supply chain security guide. The GitHub incident is the same story with a bigger name attached. So the question worth asking today is direct: if a malicious extension ran in your editor right now, what would it walk away with?

Hardcoded keys and committed .env files are a standing liability

Most credential leaks are not sophisticated. They are a developer pasting a key into code “for now” and forgetting, or a .env file slipping into a commit. Both create a liability that sits in your repository indefinitely.

Hardcoded keys are the obvious sin. You have seen code like this:

import requests

# Quick test of the payments endpoint
STRIPE_KEY = "sk_live_51Qk2mNExampleKeyDoNotShipThis"

response = requests.post(
    "https://api.stripe.com/v1/charges",
    auth=(STRIPE_KEY, ""),
    data={"amount": 2000, "currency": "usd", "source": "tok_visa"},
)
print(response.json())

That sk_live_ key is now part of the file. It is in your working directory where any extension can read it. If the file is committed, the key is in your Git history forever, even after you delete the line in a later commit. Anyone who clones the repo, or any tool that scans it, gets the key.

The .env file is supposed to be the fix. The idea is sound: keep secrets out of code, load them at runtime. A real .env looks like this:

# .env  (loaded at runtime, never meant to ship)
DATABASE_URL=postgres://app_user:Zk7%2BqN9wLx@db.internal:5432/payments
STRIPE_SECRET_KEY=sk_live_51Qk2mNExampleKeyDoNotShipThis
OPENAI_API_KEY=sk-proj-aB3dEf9hKlMnOpQrStUvWxYz1234567890
AWS_ACCESS_KEY_ID=AKIA4EXAMPLE7QRSTUVW
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
JWT_SIGNING_SECRET=8f2a91c4e7b6d3508f2a91c4e7b6d350

This is better than hardcoding. But notice what it did not change. That file still lives in your workspace, in plaintext, readable by every process the editor runs. A malicious extension does not care whether your secrets are in app.py or in .env. Both are files. Both are one fs.readFileSync away. The .env pattern solves the “secrets in Git history” problem only if the file never gets committed, and it does nothing at all for the “compromised local tool” problem.

The committed .env is the worst outcome. It is alarmingly common. A developer adds .env to the project, writes real keys into it, and forgets to ignore it before the first commit. Or a teammate clones the repo, sees .env.example, copies it to .env, fills in production keys, and a later git add . sweeps it in. Now production credentials are in the shared history of a repository that might be public, might be mirrored, or might end up in a breach like GitHub’s. We go deeper on the repo-leak angle in our companion piece on API documentation and Git repository security.

.gitignore is not a security control

This deserves its own section because the misunderstanding is so widespread. Adding .env to .gitignore feels like locking the door. It is not. It is a sticky note that says “please don’t pick this up.”

Here is what .gitignore actually does. It tells Git to skip untracked files matching a pattern when you run git add. That is the entire scope. It has three failure modes that matter for security:

  1. It does nothing for already-tracked files. If .env was committed once, before you added the ignore rule, Git keeps tracking it. The ignore rule is silently overridden. You have to run git rm --cached .env and commit that removal, and the secret is still in every historical commit.
  2. It does nothing for the file on disk. .gitignore is a Git instruction. The .env file still sits in your working directory in plaintext. A malicious VS Code extension reads the filesystem, not the Git index. Your ignore rule is invisible to it. This is the failure mode that matters most for the GitHub-style attack.
  3. It is one mistake away from bypass. git add -f .env ignores the ignore rule. So does adding the file through an editor’s source-control UI. So does a different .gitignore in a subdirectory that does not repeat the pattern.

You can confirm point one yourself. If you suspect a secret was ever committed, this command shows you:

# List every commit that ever touched the file, even if it is "ignored" now
git log --all --full-history --oneline -- .env

# See the actual secret values that are still in history
git log -p --all -- .env | grep -iE "key|secret|token|password"

If that returns anything, the credential is compromised the moment the repo is exposed. The fix is not a better ignore rule. The fix is to rotate the key and remove the file from history with a tool like git filter-repo. The deeper fix is to make sure the live credential was never in a file in the first place.

.gitignore is still worth using. It catches honest mistakes and keeps your diffs clean. Just do not mistake it for a boundary an attacker respects. It is hygiene, not defense.

Scope, separate, shorten, rotate: the four habits that shrink the blast radius

You cannot guarantee a credential never leaks. You can guarantee that a leaked credential is close to worthless. Four habits do most of the work.

Scope secrets to environments

A leaked key should unlock one thing, not your whole estate. The most common scoping failure is using the same API key in development, staging, and production because it was easier. When that key leaks, every environment falls at once.

Give each environment its own credentials. Your local machine uses a development key with access to a sandbox project and test data. Staging uses a staging key. Production uses a production key that exists in exactly one place and is never copied to a laptop. If the development key leaks through a compromised extension, the attacker reaches a sandbox with fake customers. That is an annoyance, not an incident.

Separate environments properly

Environment separation is more than three different key values. It means the environments cannot reach each other. The development database is a different database, not a read replica of production. The staging payment provider is the provider’s test mode, so a leaked staging key charges nothing real. Tools and humans should not be able to point a “dev” configuration at production data by flipping one variable.

When separation is real, the question “which environment did this key come from?” has a clear answer, and that answer tells you exactly how bad a leak is.

Use least-privilege, short-lived keys

Two properties decide how much damage a stolen key does.

Privilege. A key should carry the narrowest set of permissions its job requires. A frontend build that reads a public product catalog needs a read-only key scoped to that one resource. It does not need write access, it does not need billing access, and it certainly does not need admin. Most API providers support scoped keys or fine-grained tokens; GitHub’s own fine-grained personal access tokens are a good model. If you are weighing token styles, our comparison of API keys versus OAuth walks through when short-lived OAuth tokens beat static keys outright.

Lifetime. A key that expires in an hour is a poor prize. By the time an attacker buys a stolen dataset and gets around to your key, it is dead. Static keys that live forever are the opposite: they work until someone notices, which is often months. Prefer short-lived tokens issued on demand. Where a long-lived key is unavoidable, set the shortest expiry the workflow tolerates.

Rotate on a schedule, not on panic

Rotation is changing a credential’s value so the old one stops working. Most teams rotate only after a breach, in a hurry, under stress. That is the worst time to learn your rotation process is undocumented.

Rotate on a calendar instead. Pick an interval per credential class; high-privilege production keys monthly, lower-risk keys quarterly. Routine rotation does two things. It caps the useful lifetime of any leak you have not detected yet, since a key stolen today stops working at the next cycle. And it keeps the mechanics, updating the value in every consuming environment, exercised and boring. When a real incident hits, rotation is a button you have pressed fifty times, not a fire drill. For a fuller treatment, see our roundup of API key management tools.

Keep credentials in Apidog environment variables, not loose in your workspace

Here is the honest framing. Apidog ships a VS Code extension and an MCP server of its own. The argument is not “our tool is immune to the attack class that hit GitHub.” No client-side tool is. The argument is about where your secrets live, and how exposed they are when something on your machine misbehaves.

Think about the realistic scenario. You are building and testing APIs all day. You need a bearer token, an API key, a database connection string. The default move is to drop them into a .env file or a script so your client can use them. That puts live credentials in plaintext files in the workspace, which is exactly the surface a malicious extension scrapes. Apidog’s environment system changes where those values sit.

Environment variables instead of plaintext files

In Apidog, you store credentials as environment variables rather than as loose text in your repo. A request references the variable by name, like {{access_token}} in an Authorization header, and Apidog resolves it at send time. The header in your request definition reads Bearer {{access_token}}, not Bearer sk-proj-aB3dEf.... The literal secret is not sitting in a .env file at the project root waiting to be read.

This matters for the same reason .env beats hardcoding, taken one step further. The credential is no longer a plaintext line in a file that lives next to your source. It is a managed value inside the API client, referenced indirectly.

Local values keep secrets on your machine

Apidog draws a deliberate line between two kinds of value for each variable. There is a shared, or initial, value that syncs to Apidog’s servers and is visible to your team. And there is a local, or current, value that stays on your machine and is never uploaded. The official guidance is explicit: put sensitive data such as tokens and passwords in the local value so it never leaves your client.

The practical effect is that a teammate cloning the project structure gets the variable name and shape, access_token, db_password, and the rest, but not your actual secret. Each developer fills in their own local value. No one’s live production token rides along in synced project data, and there is no shared file of real keys to leak.

Environment management and environment isolation

Apidog’s environment management is built around the scoping habit from the previous section. You define separate environments, Development, Staging, Production, and each carries its own base URL and its own set of variable values. The variables are environment-scoped: only the active environment’s values are in effect, and switching environments switches the whole credential set at once.

This gives you environment isolation by construction. Your payment_api_key variable holds a sandbox key under Development and a production key under Production. You never edit a request to move between them; you switch the environment. Because the values are bound to the environment, a development credential cannot accidentally end up in a production call, and a production secret never has to exist in your local development environment at all. A leaked development environment exposes development values. Production stays untouched.

For teams that need a hard boundary: vault secrets

If your team needs production secrets to never touch the API client at all, Apidog’s Enterprise plan adds a Vault Secret feature that fetches secrets directly from HashiCorp Vault, Azure Key Vault, or AWS Secrets Manager. Apidog stores only the vault path and metadata. The actual secret values are pulled on demand, encrypted in the local client, and never shared with teammates through the project. The credential’s home stays the dedicated secrets manager, which is exactly where a production credential should live.

To try the environment-variable workflow, Download Apidog, create a project, open Environment Management, and add your credentials as environment variables with local-only values. It takes a few minutes and it gets live secrets out of the plaintext files an extension can read.

One honest caveat. Moving secrets into Apidog reduces how many plaintext credentials sit in your repo and workspace. It does not make your machine immune to a compromised tool. Defense in depth still applies: audit the extensions you install, keep production keys least-privilege and short-lived, and rotate on schedule. Apidog handles the “where do credentials live” part. The rest is still on you.

Manually auditing an extension's source for credential exfiltration is tedious work, which makes it a strong candidate for AI-assisted code review with Codex before you trust a package with your keys.

Conclusion

The GitHub breach is a clear signal. Attackers have figured out that the developer machine, with its trusted tools and its plaintext config files, is a softer target than any production server. You cannot make that machine perfectly safe. You can make sure a compromised IDE extension or a leaked repository hands an attacker very little.

Start with one audit. Open the repo you work in most and search it for key, secret, token, and password. Check whether .env is in your Git history. Whatever you find, treat it as exposed: rotate it, then move the live value somewhere a stray tool cannot read it. Download Apidog and put your next set of API credentials in environment variables instead of a plaintext file. For the wider context, our companion articles on self-hosted API tools after the GitHub breach and API key management tools are good next reads.

button

FAQ

Can a VS Code extension really read my .env file and API keys?

Yes. A VS Code extension runs inside the editor process with your user account’s file permissions. It can list directories, open files, and read their contents, including .env files, config files, and credential files like ~/.aws/credentials. This is normal extension behavior, since many extensions legitimately need file access. The risk is that a malicious or compromised extension uses the same access to harvest secrets. That is the mechanism behind the May 2026 GitHub breach.

Is adding .env to .gitignore enough to protect API keys?

No. .gitignore only tells Git to skip untracked files during git add. It does nothing for files already committed before the rule existed, and the secret stays in Git history regardless. It also does nothing for the file on disk: the .env file still sits in your workspace in plaintext, fully readable by any local tool or extension. Treat .gitignore as a way to prevent honest mistakes, not as a security boundary.

What should I do if I find an API key in my Git history?

Treat it as compromised immediately, even if the repo is private. Rotate the key first so the exposed value stops working. Then remove the file from history using a tool like git filter-repo, and force-push the cleaned history after coordinating with your team. Finally, move the live credential out of any plaintext file so the same leak cannot happen again. See our API key management tools guide for ongoing practices.

How does storing API keys in Apidog reduce my exposure?

Apidog lets you store credentials as environment variables and reference them by name in requests, so the literal secret is not sitting in a plaintext .env file in your repo. Each variable supports a local-only value that stays on your machine and never syncs to servers or teammates, which is where the docs recommend keeping tokens and passwords. Environment-scoped variables also keep development and production credentials separate. It reduces how many live secrets sit in files a tool can scrape; it does not make your machine immune to a compromised tool.

Does Apidog also have a VS Code extension, and is that a risk?

Yes, Apidog ships a VS Code extension and an MCP server. The point of this article is not that any tool is immune to supply-chain attacks; no client-side tool is. The point is where your secrets live. Keeping credentials in environment variables with local-only values, or in a vault integration, means fewer plaintext keys are exposed if any tool on your machine, including Apidog’s own extension, is compromised. Defense in depth, auditing extensions, least privilege, and rotation still applies.

What is the difference between scoping and rotating API keys?

Scoping limits what a key can do and where it applies: a development key reaches only a sandbox, and a read-only key cannot write. Rotation changes a key’s value on a schedule so the old value stops working. Scoping shrinks the damage a leaked key can cause; rotation shrinks the time window a leaked key stays useful. You want both. Used together, a stolen key unlocks little and expires soon.

How often should I rotate API keys?

Rotate on a fixed schedule rather than only after an incident. A reasonable baseline is monthly for high-privilege production credentials and quarterly for lower-risk keys, adjusted to your risk tolerance and any compliance rules. Scheduled rotation caps the useful life of leaks you have not detected and keeps the rotation process well-practiced, so a real incident is routine rather than a scramble.

Should production API keys ever be on a developer laptop?

Ideally, no. A production credential should exist in as few places as possible, normally a dedicated secrets manager and the production runtime, never copied to a developer machine. Developers should work against development or staging credentials scoped to non-production data. If a laptop is compromised, the attacker reaches a sandbox, not live customer systems. Apidog’s environment isolation and vault integration support this by keeping production values out of local development environments.

Explore more

How to Add Test Automation to Your DevOps Pipeline

How to Add Test Automation to Your DevOps Pipeline

Test automation in DevOps maps API tests across the lifecycle: PR gates, integration checks, deploy smoke tests, and monitoring, wired up with the Apidog CLI.

16 June 2026

How to Diff OpenAPI Specs and Block Breaking Changes in CI

How to Diff OpenAPI Specs and Block Breaking Changes in CI

Diff two OpenAPI spec versions to catch breaking API changes before merge. Use oasdiff or openapi-diff in CI, then close the contract gap with the Apidog CLI.

16 June 2026

Apidog CLI vs Redocly CLI: which API CLI should you use?

Apidog CLI vs Redocly CLI: which API CLI should you use?

Apidog CLI vs Redocly CLI compared command by command: lint, bundle, split, build docs, run tests, and mock. An honest verdict on which API CLI fits your team.

16 June 2026

Practice API Design-first in Apidog

Discover an easier way to build and use APIs

How to Protect API Keys From a Rogue VS Code Extension