Skip to content

Snippet: Env Guard Hook for OpenCode

An OpenCode env-protection.js plugin to secure your agent workflow.

snippet
dev-tools
security
ai

This is an env-protection.js plugin for OpenCode to prevent the agent from accidentally reading or modifying .env files or dumping environment variables to its memory.

Drop this into ~/.config/opencode/plugins/env-protection.js and register it in your opencode.jsonc.

// Secret-file guard for every tool that reads/writes via paths or shell.
// Blocks .env-style files, key material, home rc files holding tokens, and
// environment dumps. .env.example / .env.template / env.ts schemas pass.
export const EnvProtection = async () => {
  const EXAMPLE_BASES = new Set([
    ".env.example", ".env.template", ".env.sample", ".env.defaults",
    "env.ts", "env.mjs", "env.d.ts",
  ]);

  const HOME_RC = new Set([
    ".netrc", ".npmrc", ".pypirc", ".yarnrc", ".git-credentials",
    ".pgpass", ".my.cnf", ".htpasswd", ".tfstate", "credentials", ".vercel",
  ]);

  const SECRET_PATH = /(^|\/)(\.ssh|\.gnupg|\.aws|\.kube\/config|\.docker\/config|\.config\/gh|\.cargo\/credentials|\.codex\/config\.toml)\//;

  const isSensitive = (value) => {
    if (typeof value !== "string") return false;
    const tokens = (value.match(/[^\s"';&|<>()]+/g) || []).map((t) => t.replace(/^['"]|['"]$/g, ""));
    return tokens.some((t) => {
      const base = t.split("/").pop();
      if (EXAMPLE_BASES.has(base)) return false;
      if (base === ".env" || base.startsWith(".env.") || base === ".envrc") return true;
      if (base === ".dev.vars") return true;
      if (/\.(pem|p12|pfx|ppk|p8)$/.test(base)) return true;
      if (/\.key$/.test(base)) return true;
      if (/^(id_rsa|id_dsa|id_ed25519|id_ecdsa)\b/.test(base)) return true;
      if (/^(credentials|secrets)\.(json|ya?ml|toml)$/.test(base)) return true;
      if (HOME_RC.has(base)) return true;
      if (/_history$/.test(base) || /\.[a-z0-9]+_history$/.test(base)) return true;
      if (SECRET_PATH.test(t)) return true;
      if (/\/proc\/[^/]+\/environ/.test(t)) return true;
      return false;
    });
  };

  const isEnvDump = (command) => /\bprintenv\b/.test(command) || /(^|[\s;&|"'])env\s*($|\|)/.test(command);

  const SENSITIVE_TOOLS = new Set(["read", "edit", "write", "patch", "bash", "grep"]);

  const blocked = (tool) => `Blocked: access to secret files (${tool}) is not allowed. Use .env.example or real env vars instead.`;

  return {
    "tool.execute.before": async (input, output) => {
      if (!SENSITIVE_TOOLS.has(input.tool)) return;
      const args = output.args || {};

      if (input.tool === "bash") {
        if (isSensitive(args.command) || isEnvDump(args.command)) {
          throw new Error(blocked("bash"));
        }
      } else if (input.tool === "grep") {
        const targets = [args.include, args.path].filter(Boolean).join(" ");
        if (isSensitive(targets)) {
          throw new Error(blocked("grep"));
        }
      } else if (isSensitive(args.filePath)) {
        throw new Error(blocked(input.tool));
      }
    },
  };
};

Get new posts by email

New writing in your inbox. Unsubscribe anytime.