Repository navigation
Feature Request: Zero-Trust Architecture for Environment Variable Security #4961
Description
Feature Request: Zero-Trust Secret Protection
Summary
Implement client-side secret detection and placeholder replacement to prevent real secrets from being transmitted to LLM providers, even when users explicitly read environment files.
Problem Statement
When OpenCode reads .env files or other configuration containing secrets, the actual secret values are transmitted to the LLM provider. This creates security risks:
- Multi-provider exposure: OpenCode supports 75+ LLM providers—each provider receiving secrets multiplies the attack surface
- Accidental leakage: Users may not realize configuration files contain sensitive data
- No transmission-time protection: Even if
.gitignoreis respected by search tools, thereadtool can still access and transmit.envcontents directly
Current Workaround Limitations
Blocking .env files entirely (via ignore patterns) prevents legitimate use cases like analyzing configuration structure, debugging environment setup, or reviewing deployment scripts.
Proposed Solution: Content-Level Transformation
Instead of blocking files, transform sensitive values while preserving structure:
┌─────────────────────────────────────────────────────────────────┐
│ .env (local file) │ What LLM receives │
│ ──────────────────────────────── │ ─────────────────────────│
│ CLOUDFLARE_API_TOKEN=cf_abc123.. │ CLOUDFLARE_API_TOKEN= │
│ DATABASE_URL=postgres://user:pass │ <SECRET_001> │
│ STRIPE_SECRET_KEY=sk_live_xyz... │ DATABASE_URL=<SECRET_002> │
│ │ STRIPE_SECRET_KEY= │
│ │ <SECRET_003> │
├────────────────────────────────────┴───────────────────────────┤
│ Local Execution: Placeholders resolved to real values │
│ API Transmission: Only placeholders sent to provider │
└─────────────────────────────────────────────────────────────────┘
The LLM can analyze configuration structure, identify missing variables, and suggest changes—without ever seeing the actual credentials.
Implementation Approach
Using OpenCode's Plugin System
import type { Plugin } from 'opencode';const SECRET_PATTERNS = [
/^[A-Za-z0-9+/]{40,}={0,2}$/, // Base64 secrets
/^(sk|pk|api|key|token|secret)[-]/i, // Common prefixes
/^[a-f0-9]{32,64}$/i, // Hex API keys
/^(ghp|gho|ghu|ghs|ghr)[A-Za-z0-9]+/, // GitHub tokens
/^sk-[A-Za-z0-9]{48,}/, // OpenAI keys
/postgres://[^:]+:[^@]+@/, // Connection strings
];const secretMap = new Map<string, string>();
let counter = 0;function replaceSecrets(content: string): string {
return content.replace(/^([A-Z_][A-Z0-9_]*)=(.+)$/gm, (match, key, value) => {
if (SECRET_PATTERNS.some(p => p.test(value))) {
const placeholder =<SECRET_${String(++counter).padStart(3, '0')}>;
secretMap.set(placeholder, value);
return${key}=${placeholder};
}
return match;
});
}
export const SecretProtection: Plugin = async () => ({
tool: {
execute: {
// Transform before sending to LLM
before: async (input, output) => {
if (input.tool === 'read') {
const path = output.args.filePath.toLowerCase();
if (path.includes('.env') || path.includes('secret')) {
output.result = replaceSecrets(output.result);
}
}
},
// Resolve placeholders for local execution
after: async (input, output) => {
if (input.tool === 'bash') {
let cmd = output.args.command;
for (const [placeholder, secret] of secretMap) {
cmd = cmd.replace(placeholder, secret);
}
output.args.command = cmd;
}
}
}
}
});
Configuration
{
"$schema": "https://opencode.ai/config.json",
"security": {
"secretProtection": {
"enabled": true,
"patterns": {
// Additional custom patterns for your org
"custom": ["^ACME_.*_KEY$", "^INTERNAL_.*_TOKEN$"]
},
"files": {
"protected": [".env*", "*.secrets", "credentials.*"],
"excluded": [".env.example", "*.sample"]
},
"mode": "replace" // "replace" | "redact" | "warn"
}
}
}
Agent-Specific Behavior
{
"agent": {
"plan": {
// Plan agent: always redact, never resolve
"security": { "secretProtection": { "mode": "redact" } }
},
"build": {
// Build agent: replace for analysis, resolve for execution
"security": { "secretProtection": { "mode": "replace" } }
}
}
}
Use Cases
- Configuration analysis: Review
.envstructure without exposing credentials - Debugging: Analyze environment setup issues safely
- Code review: Review deployment scripts while keeping secrets local
- Team collaboration: Share sessions without exposing team secrets
Benefits
| Benefit | Description |
|---|---|
| Defense in Depth | Protection even if users accidentally share sensitive files |
| Provider Agnostic | Works across all 75+ supported LLM providers |
| Preserves Functionality | Users can still analyze .env structure and patterns |
| Backward Compatible | Opt-in feature, existing workflows unchanged |
| Plugin Architecture | Leverages OpenCode's existing extensibility model |
Prior Art
- Claude Code Issue #2695: Similar zero-trust proposal for Anthropic's Claude Code
- 1Password CLI: Shell integration that resolves secrets at execution time
- AWS Secrets Manager: Runtime secret resolution pattern
Summary
Implement client-side secret detection and placeholder replacement to prevent real secrets from being transmitted to LLM providers, even when users explicitly read environment files.
Problem Statement
When OpenCode reads .env files or other configuration containing secrets, the actual secret values are transmitted to the LLM provider. This creates security risks:
- Multi-provider exposure: OpenCode supports 75+ LLM providers—each provider receiving secrets multiplies the attack surface
- Accidental leakage: Users may not realize configuration files contain sensitive data
- No transmission-time protection: Even if
.gitignoreis respected by search tools, thereadtool can still access and transmit.envcontents directly
Current Workaround Limitations
Blocking .env files entirely (via ignore patterns) prevents legitimate use cases like analyzing configuration structure, debugging environment setup, or reviewing deployment scripts.
Proposed Solution: Content-Level Transformation
Instead of blocking files, transform sensitive values while preserving structure:
┌─────────────────────────────────────────────────────────────────┐
│ .env (local file) │ What LLM receives │
│ ──────────────────────────────── │ ─────────────────────────│
│ CLOUDFLARE_API_TOKEN=cf_abc123.. │ CLOUDFLARE_API_TOKEN= │
│ DATABASE_URL=postgres://user:pass │ <SECRET_001> │
│ STRIPE_SECRET_KEY=sk_live_xyz... │ DATABASE_URL=<SECRET_002> │
│ │ STRIPE_SECRET_KEY= │
│ │ <SECRET_003> │
├────────────────────────────────────┴───────────────────────────┤
│ Local Execution: Placeholders resolved to real values │
│ API Transmission: Only placeholders sent to provider │
└─────────────────────────────────────────────────────────────────┘
The LLM can analyze configuration structure, identify missing variables, and suggest changes—without ever seeing the actual credentials.
Implementation Approach
Using OpenCode's Plugin System
import type { Plugin } from 'opencode';
const SECRET_PATTERNS = [
/^[A-Za-z0-9+/]{40,}={0,2}$/, // Base64 secrets
/^(sk|pk|api|key|token|secret)[-_]/i, // Common prefixes
/^[a-f0-9]{32,64}$/i, // Hex API keys
/^(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]+/, // GitHub tokens
/^sk-[A-Za-z0-9]{48,}/, // OpenAI keys
/postgres:\/\/[^:]+:[^@]+@/, // Connection strings
];
const secretMap = new Map<string, string>();
let counter = 0;
function replaceSecrets(content: string): string {
return content.replace(/^([A-Z_][A-Z0-9_]*)=(.+)$/gm, (match, key, value) => {
if (SECRET_PATTERNS.some(p => p.test(value))) {
const placeholder = `<SECRET_${String(++counter).padStart(3, '0')}>`;
secretMap.set(placeholder, value);
return `${key}=${placeholder}`;
}
return match;
});
}
export const SecretProtection: Plugin = async () => ({
tool: {
execute: {
// Transform before sending to LLM
before: async (input, output) => {
if (input.tool === 'read') {
const path = output.args.filePath.toLowerCase();
if (path.includes('.env') || path.includes('secret')) {
output.result = replaceSecrets(output.result);
}
}
},
// Resolve placeholders for local execution
after: async (input, output) => {
if (input.tool === 'bash') {
let cmd = output.args.command;
for (const [placeholder, secret] of secretMap) {
cmd = cmd.replace(placeholder, secret);
}
output.args.command = cmd;
}
}
}
}
});Configuration
{
"$schema": "https://opencode.ai/config.json",
"security": {
"secretProtection": {
"enabled": true,
"patterns": {
// Additional custom patterns for your org
"custom": ["^ACME_.*_KEY$", "^INTERNAL_.*_TOKEN$"]
},
"files": {
"protected": [".env*", "*.secrets", "credentials.*"],
"excluded": [".env.example", "*.sample"]
},
"mode": "replace" // "replace" | "redact" | "warn"
}
}
}Agent-Specific Behavior
{
"agent": {
"plan": {
// Plan agent: always redact, never resolve
"security": { "secretProtection": { "mode": "redact" } }
},
"build": {
// Build agent: replace for analysis, resolve for execution
"security": { "secretProtection": { "mode": "replace" } }
}
}
}Use Cases
- Configuration analysis: Review
.envstructure without exposing credentials - Debugging: Analyze environment setup issues safely
- Code review: Review deployment scripts while keeping secrets local
- Team collaboration: Share sessions without exposing team secrets
Benefits
| Benefit | Description |
|---|---|
| Defense in Depth | Protection even if users accidentally share sensitive files |
| Provider Agnostic | Works across all 75+ supported LLM providers |
| Preserves Functionality | Users can still analyze .env structure and patterns |
| Backward Compatible | Opt-in feature, existing workflows unchanged |
| Plugin Architecture | Leverages OpenCode's existing extensibility model |
Prior Art
- Claude Code Issue [#2695](Feature Request: Zero-Trust Architecture for Environment Variable Security anthropics/claude-code#2695): Similar zero-trust proposal for Anthropic's Claude Code
- 1Password CLI: Shell integration that resolves secrets at execution time
- AWS Secrets Manager: Runtime secret resolution pattern
Activity
github-actions commentedon Dec 1, 2025
This feature request builds upon and complements several existing security-related issues. Please check these related discussions:
This proposal offers a comprehensive solution (zero-trust architecture with placeholder replacement) that could address multiple issues at once. Feel free to review these related issues to ensure this implementation doesn't duplicate existing efforts.
rekram1-node commentedon Dec 1, 2025
anyway we can add a summarized version of this, that's a long description, also some of the linked issues aren't issues and were addressed a while ago
tredondo commentedon Feb 17, 2026
@coniferconcepts great idea, but can you please trim the duplicate parts of your issue?
Zed already does this for
.envfiles: instead of reading them, the agent runs asedcommand that replaces the secrets with***:inkdust2021 commentedon Feb 28, 2026
I agree with the direction of #4961: allow reading
.env(and similar) files, but replace sensitive values with placeholders before anything is sent to the LLM provider; then restore placeholders back to real values right before local tool execution. This way, real secrets never leave the machine.Related prior art: I’ve published VibeGuard (a proxy/MITM implementation of the full “detect → placeholder → restore-before-execution” flow):
https://github.com/inkdust2021/VibeGuard/
I’d like to implement the same idea inside OpenCode on the client/plugin side (so no MITM is needed).
Plan: start with a plugin PoC using these hooks:
experimental.chat.messages.transform: replace secrets with placeholders in outgoing messages before each LLM requesttool.execute.before: restore placeholders inside tool args (e.g.bash/write/edit) right before executionIf this direction is acceptable, I’m happy to implement it and submit a PR (PoC first, then adjust based on maintainer feedback on whether it should become a core feature).
Quick question: do the current hooks fully cover “pre-send replacement + pre-exec restore”? Also, do you prefer starting with “transmission-only protection” (LLM never sees secrets), or also redacting the
read .envoutput in the conversation by default (viatool.execute.after)?paroque28 commentedon Mar 31, 2026
We've built a working implementation of this concept: claude-code-redact (
rdx).It does content-level transformation with placeholder replacement as described here, plus:
AcmeCorp→WidgetInc) so the LLM reasons naturallysk-secret123→__RDX_KEY_a1b2c3d4__) for discovered secretspassword=,Bearer ...), and optional Presidio NLPWorks today via
ANTHROPIC_BASE_URLproxy. Native provider-level hooks (see #19425, #5148) would make it cleaner.github-actions commentedon Jul 3, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.