Skip to content

Feature Request: Zero-Trust Architecture for Environment Variable Security #4961

Closed as not planned

Description

@coniferconcepts

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:

  1. Multi-provider exposure: OpenCode supports 75+ LLM providers—each provider receiving secrets multiplies the attack surface
  2. Accidental leakage: Users may not realize configuration files contain sensitive data
  3. No transmission-time protection: Even if .gitignore is respected by search tools, the read tool can still access and transmit .env contents 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 = &lt;SECRET_${String(++counter).padStart(3, '0')}&gt;;
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 .env structure 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
# 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:

  1. Multi-provider exposure: OpenCode supports 75+ LLM providers—each provider receiving secrets multiplies the attack surface
  2. Accidental leakage: Users may not realize configuration files contain sensitive data
  3. No transmission-time protection: Even if .gitignore is respected by search tools, the read tool can still access and transmit .env contents 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 .env structure 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

Activity

  1. github-actions commented on Dec 1, 2025

    @github-actions
    Contributor

    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.

  2. rekram1-node commented on Dec 1, 2025

    @rekram1-node
    Collaborator

    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

  3. tredondo commented on Feb 17, 2026

    @tredondo

    @coniferconcepts great idea, but can you please trim the duplicate parts of your issue?

    Zed already does this for .env files: instead of reading them, the agent runs a sed command that replaces the secrets with ***:

    sed '/^[^=]*=/s/^\([^=]*=\).*/\1***/' .env
    
  4. inkdust2021 commented on Feb 28, 2026

    @inkdust2021
    Contributor

    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 request
    • tool.execute.before: restore placeholders inside tool args (e.g. bash / write / edit) right before execution

    If 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 .env output in the conversation by default (via tool.execute.after)?

  5. paroque28 commented on Mar 31, 2026

    @paroque28

    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:

    • Format-preserving replacements (AcmeCorp → WidgetInc) so the LLM reasons naturally
    • Auto-tokens (sk-secret123 → __RDX_KEY_a1b2c3d4__) for discovered secrets
    • Round-trip un-redaction — responses are un-redacted before tool calls execute, so files always have real values
    • Multi-layer detection — regex, entropy, context (password=, Bearer ...), and optional Presidio NLP
    • In-memory mapping only — no reverse map on disk to steal

    Works today via ANTHROPIC_BASE_URL proxy. Native provider-level hooks (see #19425, #5148) would make it cleaner.

  6. github-actions commented on Jul 3, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions