A .vsix is just a ZIP archive of plain JavaScript. Here is the post-mortem of how our build-time secrets failed, how we designed dynamic device registration with OS SecretStorage, and how we solved the 'scanner scanning the scanner' paradox.
TL;DR: Distributing a client-server VS Code extension means accepting a hard truth: a
.vsixfile is just a ZIP archive containing JavaScript that can be extracted and inspected. This article breaks down how our initial attempt to inject build-time backend secrets was scrapped, how we engineered an anonymous dynamic device handshake with OS-levelSecretStorage, and how we resolved the bizarre paradox of static security scanners flagging our own secret detection patterns.
The Illusion of the Extension Package
When developers distribute software through the VS Code Marketplace or the Open VSX Registry, it is easy to unconsciously think of the .vsix file as a compiled, opaque binary.
It is not.
A .vsix package is an Open Packaging Conventions (OPC) archive — an ordinary ZIP file containing your extension’s manifest (package.json), assets, and compiled JavaScript output (out/).
Anyone can verify this with standard shell utilities:
# Rename and extract any published extension
cp DotEnvy-2.1.0.vsix DotEnvy.zip
unzip -q DotEnvy.zip -d extracted/
cd extracted/extension/out
# Search for sensitive variables
grep -rn "SHARED_SECRET" .
If you injected an API key, an HMAC signing secret, or a database token into your client code at compile time, you just published that secret to the entire world.
Context: Why Did We Need a Secret in the First Place?
In Part 1 of this series, I explained how we built Aegis — a 4-layer AI secret detection pipeline for DotEnvy.
The first three layers (Regex, Community Blacklist, and Entropy Gating) execute locally inside the editor process. But Layer 4 requires invoking our Python microservice (aegis.dotsuite.dev) on Railway to run contextual neural classification.
To protect the backend against unauthorized abuse, spam, and denial-of-service, we required incoming HTTP requests to be signed using HMAC-SHA256:
$$\text{Signature} = \text{HMAC-SHA256}(\text{SharedSecret}, \text{Timestamp} + "." + \text{Payload})$$
This design introduced our fundamental dilemma:
How does an open-source, publicly distributed client authenticate with a protected backend service without shipping a hardcoded secret in its package?
Attempt 1: The Build-Time Secret Trap (v2.1.0)
Our initial approach relied on build-time environment variable substitution. In our deployment pipeline, a Node script injected a secret before invoking vsce package:
// scripts/build-with-env.js (Flawed Approach)
const secret = process.env.PRODUCTION_EXTENSION_SECRET;
let code = fs.readFileSync('out/utils/llmAnalyzer.js', 'utf8');
code = code.replace('REPLACE_AT_BUILD_TIME', secret);
fs.writeFileSync('out/utils/llmAnalyzer.js', code);
While this kept the secret out of our public git repository, it was an architectural disaster:
-
The VSIX Leak: As demonstrated above, extracting the
.vsixexposed the secret immediately. - Automated Scanner Flags: Automated CI/CD bots (GitHub Secret Scanning, Trufflehog, CodeQL) automatically inspect release assets. A high-entropy hex string inside compiled JavaScript triggers immediate alerts.
- Single Point of Compromise: If one user decompiles the extension and leaks the shared secret, the backend can no longer trust any extension client without rotating the key and breaking every installed instance worldwide.
We scrapped this design entirely.
Attempt 2: Zero Embedded Client Secrets & Dynamic Device Handshake (v2.1.2)
We established a strict architectural rule: Zero Embedded / Static Client Secrets.
Not a single secret, token, or private key may exist in the source code, build scripts, or packaged .vsix bundle.
Instead, we designed an anonymous, zero-touch dynamic device registration handshake:
┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐
│ DotEnvy Client (First Activation) │ │ Aegis Backend (aegis.dotsuite.dev) │
└───────────────────┬────────────────────┘ └───────────────────┬────────────────────┘
│ │
│ 1. POST /extension/register │
│ { machine_id: hash(vscode.env.machineId) } │
├───────────────────────────────────────────────────►│ Strict IP Rate Limiter
│ │ (Max 10 per hour per IP)
│ │
│ │ 2. Backend generates unique
│ │ cryptographic client_secret
│ 3. Returns per-device secret │ and stores active record
│◄───────────────────────────────────────────────────┤
│ │
▼ ▼
Stored Exclusively in Subsequent requests signed via
OS-level SecretStorage HMAC-SHA256(timestamp + body)
(macOS Keychain / Windows with per-machine rate limiting
Credential Manager / libsecret) (30 req/min)
1. Anonymous Environment Identity
When DotEnvy launches for the first time, it generates a non-reversible identifier from VS Code's environment API:
private getMachineId(): string {
return vscode.env.machineId || 'anonymous-machine';
}
vscode.env.machineId provides a pseudonymous environment identifier that lets the backend maintain per-client state and rate limits without requiring a user account.
2. Ephemeral Dynamic Registration
The extension calls /extension/register:
- Defense in Depth: The registration endpoint is protected by strict IP-based rate limiting (maximum 10 registrations per hour per IP) to prevent Sybil registration floods.
-
Unique Credential Issuance: The backend generates an independent 256-bit cryptographically random secret (
client_secret), persists the mapping in PostgreSQL, and returns it over TLS.
3. OS-Level SecretStorage
The extension never writes the returned secret to disk, .env files, or standard workspace settings. It stores the credential directly inside VS Code's SecretStorage API:
public async setSharedSecret(secret: string): Promise<void> {
await this.secrets.store('dotenvy.llm.sharedSecret', secret);
this.sharedSecret = secret;
}
Under the hood, VS Code delegates SecretStorage to native operating system keyrings:
- macOS: Apple Keychain
- Windows: Windows Credential Manager
- Linux: Secret Service API (gnome-keyring / KWallet)
4. Authenticated Request Signing
For all subsequent requests, DotEnvy signs its payloads using HMAC-SHA256:
private signRequest(body: string): { timestamp: string; signature: string } {
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = crypto
.createHmac('sha256', this.sharedSecret!)
.update(`${timestamp}.${body}`)
.digest('hex');
return { timestamp, signature };
}
On the backend, Aegis:
- Validates the timestamp against a 5-minute sliding window to prevent replay attacks.
- Looks up the
client_secretcorresponding to theX-Machine-IDheader. - Uses Python's constant-time comparison
hmac.compare_digestto prevent timing attacks. - Applies a machine-level rate limit (30 requests/minute).
If a specific client secret is ever compromised on a developer's local machine, revoking that single record does not disrupt any other user.
The Automated Gatekeeper: CI/CD Pre-Publish Validation
To ensure no developer or automated script ever accidentally reintroduces an embedded secret, we added an automated pre-publish scanner to scripts/build-with-env.js:
// scripts/build-with-env.js
const fs = require('fs');
const path = require('path');
const outDir = path.join(__dirname, '..', 'out');
function scanDir(dir) {
let flagged = 0;
const files = fs.readdirSync(dir);
for (const f of files) {
const fullPath = path.join(dir, f);
if (fs.statSync(fullPath).isDirectory()) {
flagged += scanDir(fullPath);
} else if (fullPath.endsWith('.js')) {
const content = fs.readFileSync(fullPath, 'utf8');
// Check for illegal embedded secret patterns
if (/const\s+embeddedSecret\s*=\s*["'][A-Za-z0-9_-]{20,}["']/.test(content)) {
console.error(`❌ Security Alert: Hardcoded embeddedSecret found in ${fullPath}!`);
flagged++;
}
}
}
return flagged;
}
const flags = scanDir(outDir);
if (flags > 0) {
console.error(`\n❌ Pre-publish check failed: ${flags} leaked secret(s) found in out/!`);
process.exit(1);
} else {
console.log('✅ Security check passed: 0 embedded secrets found in compiled out/ bundle.');
}
This verification script runs automatically as part of npm run package. If a secret is detected, the build halts with an exit code of 1, aborting the release pipeline.
The "Scanner Scanning the Scanner" Paradox (v2.1.3)
Once we eliminated embedded secrets, we collided with an ironic roadblock: automated static security scanners started blocking our extension releases.
DotEnvy is a security extension designed to detect leaked credentials. Its pattern registry naturally contains regular expressions for popular API tokens:
// ❌ v2.1.0: Literal regex prefixes
const KNOWN_SECRET_PATTERNS = [
{ name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/ },
{ name: 'Stripe Live Key', regex: /sk_live_[0-9a-zA-Z]{24,}/ },
{ name: 'GitHub Token', regex: /ghp_[a-zA-Z0-9]{36}/ },
{ name: 'Google API Key', regex: /AIza[0-9A-Za-z\-_]{35}/ },
];
When GitHub’s push protection and marketplace upload scanners inspected our compiled output, their naive string-matching heuristics triggered on our own code:
"Warning: Leaked AWS Access Key pattern detected in out/utils/llmAnalyzer.js"
The scanners couldn't differentiate between code containing a secret and code designed to detect a secret.
Avoiding Static Scanner False Positives via Runtime Pattern Construction
To resolve these false positives without weakening our detection capabilities, we refactored the pattern registry to assemble sensitive token prefixes dynamically at runtime:
// ✅ v2.1.3: Assembled at runtime — clean scanner bill of health
const KNOWN_SECRET_PATTERNS = [
{
name: 'AWS Access Key',
regex: new RegExp(['A', 'KIA', '[0-9A-Z]{16}'].join(''))
},
{
name: 'Stripe Live Key',
regex: new RegExp(['sk', '_live_', '[0-9a-zA-Z]{24,}'].join(''))
},
{
name: 'GitHub Token',
regex: new RegExp(['g', 'hp_', '[a-zA-Z0-9]{36}'].join(''))
},
{
name: 'Google API Key',
regex: new RegExp(['AI', 'za', '[0-9A-Za-z\\-_]{35}'].join(''))
},
];
Engineering Note: This runtime string assembly is not an evasion technique against security controls; it is a pragmatic packaging workaround to prevent regex-based linters from confusing detection definitions with genuine credential leaks. Inside Node's V8 engine, the compiled
RegExpobjects are identical and execute at native engine speed.
Build Hygiene & VSIX Purity
Our final hardening step addressed package purity:
-
Automatic Pre-Build Purging: We updated our
package.jsonscripts to mandaterm -rf outbefore every invocation oftsc. Stale, uncompiled test artifacts from previous development sessions can never accidentally be swept into a production.vsix. -
Aggressive
.vscodeignoreHardening: We audited and excluded all non-production files:- Development linter configs (
eslint.config.mjs) - Internal test runners and mock fixtures
- Temporary extraction folders (
extracted-vsix/) - Documentation drafts and article markdowns
- Development linter configs (
The resulting .vsix bundle contains solely the minified runtime code and localized UI assets.
Checklist for VS Code Extension Developers
If you are building a VS Code extension that interacts with a private backend, keep these rules in mind:
| Rule | Bad Practice ❌ | Good Practice ✅ |
|---|---|---|
| Client Secrets | Injecting API keys in build scripts | Zero embedded secrets; dynamic device handshake |
| Local Storage | Saving tokens in workspaceState
|
Storing credentials in native OS SecretStorage
|
| Request Signing | Sending plain machine IDs | HMAC-SHA256 with timestamp sliding window |
| Scanner Hygiene | Hardcoded token regex literals | Dynamic runtime pattern construction |
| Package Hygiene | Relying on default package contents | Automated rm -rf out + strict .vscodeignore
|
Links & Verification
- 🔗 VS Code Marketplace: marketplace.visualstudio.com/items?itemName=FreeRave.dotenvy
- 🌐 Open VSX Registry: open-vsx.org/extension/freerave/dotenvy
- 📦 GitHub Repository: github.com/kareem2099/dotenvy
- 📖 Read Part 1: DotEnvy Aegis: Building a 4-Layer AI Secret Detection Pipeline for VS Code
- 📝 Full Changelog: CHANGELOG.md
Written by FreeRave. If you've encountered packaging headaches or static scanner false alarms in your extensions, feel free to reach out and share your experience.
Top comments (0)