mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 16:04:07 +00:00
Block provider credentials from workspace dotenv [AI] (#83655)
* fix: block provider credentials from workspace dotenv * addressing codex review * fix(dotenv): document provider credential sources --------- Co-authored-by: Agustin Rivera <agustin@rivera-web.com> Co-authored-by: Devin Robison <drobison00@users.noreply.github.com>
This commit is contained in:
@@ -1070,12 +1070,13 @@ Hardening tips:
|
||||
|
||||
OpenClaw loads workspace-local `.env` files for agents and tools, but never lets those files silently override gateway runtime controls.
|
||||
|
||||
- Provider credential environment variables are blocked from untrusted workspace `.env` files. Examples include `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `GROQ_API_KEY`, `DEEPSEEK_API_KEY`, `PERPLEXITY_API_KEY`, `BRAVE_API_KEY`, `TAVILY_API_KEY`, `EXA_API_KEY`, `FIRECRAWL_API_KEY`, and provider auth keys declared by installed trusted plugins. Put provider credentials in the Gateway process environment, `~/.openclaw/.env` (`$OPENCLAW_STATE_DIR/.env`), the config `env` block, or optional login-shell import.
|
||||
- Any key that starts with `OPENCLAW_*` is blocked from untrusted workspace `.env` files.
|
||||
- Channel endpoint settings for Matrix, Mattermost, IRC, and Synology Chat are also blocked from workspace `.env` overrides, so cloned workspaces cannot redirect bundled connector traffic through local endpoint config. Endpoint env keys (such as `MATRIX_HOMESERVER`, `MATTERMOST_URL`, `IRC_HOST`, `SYNOLOGY_CHAT_INCOMING_URL`) must come from the gateway process environment or `env.shellEnv`, not from a workspace-loaded `.env`.
|
||||
- The block is fail-closed: a new runtime-control variable added in a future release cannot be inherited from a checked-in or attacker-supplied `.env`; the key is ignored and the gateway keeps its own value.
|
||||
- Trusted process/OS environment variables (the gateway's own shell, launchd/systemd unit, app bundle) still apply - this only constrains `.env` file loading.
|
||||
- Trusted process/OS environment variables, global runtime dotenv, config `env`, and enabled login-shell import still apply - this only constrains workspace `.env` file loading.
|
||||
|
||||
Why: workspace `.env` files frequently live next to agent code, get committed by accident, or get written by tools. Blocking the whole `OPENCLAW_*` prefix means adding a new `OPENCLAW_*` flag later can never regress into silent inheritance from workspace state.
|
||||
Why: workspace `.env` files frequently live next to agent code, get committed by accident, or get written by tools. Blocking provider credentials prevents a cloned workspace from substituting attacker-controlled provider accounts. Blocking the whole `OPENCLAW_*` prefix means adding a new `OPENCLAW_*` flag later can never regress into silent inheritance from workspace state.
|
||||
|
||||
### Logs and transcripts (redaction and retention)
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ title: "Environment variables"
|
||||
---
|
||||
|
||||
OpenClaw pulls environment variables from multiple sources. The rule is **never override existing values**.
|
||||
Workspace `.env` files are a lower-trust source: OpenClaw ignores provider credentials and protected runtime controls from workspace `.env` before applying precedence.
|
||||
|
||||
## Precedence (highest → lowest)
|
||||
|
||||
1. **Process environment** (what the Gateway process already has from the parent shell/daemon).
|
||||
2. **`.env` in the current working directory** (dotenv default; does not override).
|
||||
3. **Global `.env`** at `~/.openclaw/.env` (aka `$OPENCLAW_STATE_DIR/.env`; does not override).
|
||||
2. **`.env` in the current working directory** (dotenv default; does not override; provider credentials and protected runtime controls are ignored).
|
||||
3. **Global `.env`** at `~/.openclaw/.env` (aka `$OPENCLAW_STATE_DIR/.env`; recommended for provider API keys; does not override).
|
||||
4. **Config `env` block** in `~/.openclaw/openclaw.json` (applied only if missing).
|
||||
5. **Optional login-shell import** (`env.shellEnv.enabled` or `OPENCLAW_LOAD_SHELL_ENV=1`), applied only for missing expected keys.
|
||||
|
||||
@@ -21,6 +22,21 @@ On Ubuntu fresh installs that use the default state dir, OpenClaw also treats `~
|
||||
|
||||
If the config file is missing entirely, step 4 is skipped; shell import still runs if enabled.
|
||||
|
||||
## Provider credentials and workspace `.env`
|
||||
|
||||
Do not keep provider API keys only in a workspace `.env`. OpenClaw ignores provider credential environment variables from workspace `.env` files, including common keys such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `GROQ_API_KEY`, `DEEPSEEK_API_KEY`, `PERPLEXITY_API_KEY`, `BRAVE_API_KEY`, `TAVILY_API_KEY`, `EXA_API_KEY`, and `FIRECRAWL_API_KEY`.
|
||||
|
||||
Use one of these trusted sources for provider credentials:
|
||||
|
||||
- The Gateway process environment, such as a shell, launchd/systemd unit, container secret, or CI secret.
|
||||
- The global runtime dotenv file at `~/.openclaw/.env` or `$OPENCLAW_STATE_DIR/.env`.
|
||||
- The config `env` block in `~/.openclaw/openclaw.json`.
|
||||
- Optional login-shell import when `env.shellEnv.enabled` or `OPENCLAW_LOAD_SHELL_ENV=1` is enabled.
|
||||
|
||||
If you previously stored provider keys only in a workspace `.env`, move them to one of the trusted sources above. Workspace `.env` can still provide ordinary project variables that are not credentials, endpoint redirects, host overrides, or `OPENCLAW_*` runtime controls.
|
||||
|
||||
See [Workspace `.env` files](/gateway/security#workspace-env-files) for the security rationale.
|
||||
|
||||
## Config `env` block
|
||||
|
||||
Two equivalent ways to set inline env vars (both are non-overriding):
|
||||
|
||||
@@ -1085,6 +1085,9 @@ lives on the [First-run FAQ](/help/faq-first-run).
|
||||
- a global fallback `.env` from `~/.openclaw/.env` (aka `$OPENCLAW_STATE_DIR/.env`)
|
||||
|
||||
Neither `.env` file overrides existing env vars.
|
||||
Provider credential variables are an exception for workspace `.env`: keys such as
|
||||
`GEMINI_API_KEY`, `XAI_API_KEY`, or `MISTRAL_API_KEY` are ignored from workspace
|
||||
`.env` and should live in the process environment, `~/.openclaw/.env`, or config `env`.
|
||||
|
||||
You can also define inline env vars in config (applied only if missing from the process env):
|
||||
|
||||
|
||||
48
src/docs/environment-docs.test.ts
Normal file
48
src/docs/environment-docs.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
async function readDoc(relativePath: string): Promise<string> {
|
||||
return await fs.readFile(path.join(process.cwd(), relativePath), "utf8");
|
||||
}
|
||||
|
||||
const providerCredentialExamples = [
|
||||
"GEMINI_API_KEY",
|
||||
"XAI_API_KEY",
|
||||
"MISTRAL_API_KEY",
|
||||
"BRAVE_API_KEY",
|
||||
] as const;
|
||||
|
||||
describe("environment docs", () => {
|
||||
it("documents the trusted sources for provider credentials", async () => {
|
||||
const markdown = await readDoc("docs/help/environment.md");
|
||||
|
||||
expect(markdown).toContain("Provider credentials and workspace `.env`");
|
||||
expect(markdown).toContain(
|
||||
"OpenClaw ignores provider credential environment variables from workspace `.env` files",
|
||||
);
|
||||
expect(markdown).toContain("~/.openclaw/.env");
|
||||
expect(markdown).toContain("$OPENCLAW_STATE_DIR/.env");
|
||||
expect(markdown).toContain("The config `env` block");
|
||||
expect(markdown).toContain("OPENCLAW_LOAD_SHELL_ENV=1");
|
||||
|
||||
for (const key of providerCredentialExamples) {
|
||||
expect(markdown).toContain(key);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the security guide aligned with the workspace dotenv credential boundary", async () => {
|
||||
const markdown = await readDoc("docs/gateway/security/index.md");
|
||||
|
||||
expect(markdown).toContain(
|
||||
"Provider credential environment variables are blocked from untrusted workspace `.env` files",
|
||||
);
|
||||
expect(markdown).toContain("provider auth keys declared by installed trusted plugins");
|
||||
expect(markdown).toContain("~/.openclaw/.env");
|
||||
expect(markdown).toContain("$OPENCLAW_STATE_DIR/.env");
|
||||
|
||||
for (const key of providerCredentialExamples) {
|
||||
expect(markdown).toContain(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,14 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { loadCliDotEnv } from "../cli/dotenv.js";
|
||||
import {
|
||||
clearCurrentPluginMetadataSnapshot,
|
||||
setCurrentPluginMetadataSnapshot,
|
||||
} from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { listKnownProviderAuthEnvVarNames } from "../secrets/provider-env-vars.js";
|
||||
import { loadDotEnv, loadWorkspaceDotEnvFile } from "./dotenv.js";
|
||||
|
||||
const loggerMocks = vi.hoisted(() => ({
|
||||
@@ -108,6 +116,54 @@ type DotEnvFixture = {
|
||||
stateDir: string;
|
||||
};
|
||||
|
||||
function emptyOwnerMaps(): PluginMetadataSnapshot["owners"] {
|
||||
return {
|
||||
channels: new Map(),
|
||||
channelConfigs: new Map(),
|
||||
providers: new Map(),
|
||||
modelCatalogProviders: new Map(),
|
||||
cliBackends: new Map(),
|
||||
setupProviders: new Map(),
|
||||
commandAliases: new Map(),
|
||||
contracts: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
function createManifestBackedProviderSnapshot(
|
||||
plugin: PluginManifestRecord,
|
||||
): PluginMetadataSnapshot {
|
||||
const policyHash = resolveInstalledPluginIndexPolicyHash({});
|
||||
return {
|
||||
policyHash,
|
||||
index: {
|
||||
version: 1,
|
||||
hostContractVersion: "test",
|
||||
compatRegistryVersion: "test",
|
||||
migrationVersion: 1,
|
||||
policyHash,
|
||||
generatedAtMs: 0,
|
||||
installRecords: {},
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
},
|
||||
registryDiagnostics: [],
|
||||
manifestRegistry: { plugins: [plugin], diagnostics: [] },
|
||||
plugins: [plugin],
|
||||
diagnostics: [],
|
||||
byPluginId: new Map([[plugin.id, plugin]]),
|
||||
normalizePluginId: (pluginId: string) => pluginId,
|
||||
owners: emptyOwnerMaps(),
|
||||
metrics: {
|
||||
registrySnapshotMs: 0,
|
||||
manifestRegistryMs: 0,
|
||||
ownerMapsMs: 0,
|
||||
totalMs: 0,
|
||||
indexPluginCount: 0,
|
||||
manifestPluginCount: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function withDotEnvFixture(run: (fixture: DotEnvFixture) => Promise<void>) {
|
||||
const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-dotenv-test-"));
|
||||
const cwdDir = path.join(base, "cwd");
|
||||
@@ -749,6 +805,78 @@ describe("loadCliDotEnv", () => {
|
||||
});
|
||||
|
||||
describe("workspace .env blocklist completeness", () => {
|
||||
it("keeps trusted global dotenv for global plugin provider auth vars", async () => {
|
||||
await withIsolatedEnvAndCwd(async () => {
|
||||
await withDotEnvFixture(async ({ cwdDir, stateDir }) => {
|
||||
const plugin: PluginManifestRecord = {
|
||||
id: "runtime-cloud",
|
||||
channels: [],
|
||||
providers: ["runtime-cloud"],
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
origin: "global",
|
||||
rootDir: "/plugins/runtime-cloud",
|
||||
source: "/plugins/runtime-cloud/index.js",
|
||||
manifestPath: "/plugins/runtime-cloud/openclaw.plugin.json",
|
||||
providerAuthEnvVars: {
|
||||
"runtime-cloud": ["RUNTIME_CLOUD_API_KEY"],
|
||||
},
|
||||
};
|
||||
await writeEnvFile(
|
||||
path.join(cwdDir, ".env"),
|
||||
"RUNTIME_CLOUD_API_KEY=workspace-plugin-key\n",
|
||||
);
|
||||
await writeEnvFile(
|
||||
path.join(stateDir, ".env"),
|
||||
"RUNTIME_CLOUD_API_KEY=global-plugin-key\n",
|
||||
);
|
||||
|
||||
delete process.env.RUNTIME_CLOUD_API_KEY;
|
||||
vi.spyOn(process, "cwd").mockReturnValue(cwdDir);
|
||||
setCurrentPluginMetadataSnapshot(createManifestBackedProviderSnapshot(plugin), {
|
||||
config: {},
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
try {
|
||||
loadDotEnv({ quiet: true });
|
||||
|
||||
expect(process.env.RUNTIME_CLOUD_API_KEY).toBe("global-plugin-key");
|
||||
} finally {
|
||||
clearCurrentPluginMetadataSnapshot();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps registered provider auth vars from trusted global dotenv", async () => {
|
||||
await withIsolatedEnvAndCwd(async () => {
|
||||
await withDotEnvFixture(async ({ cwdDir, stateDir }) => {
|
||||
const providerAuthKeys = listKnownProviderAuthEnvVarNames().toSorted();
|
||||
await writeEnvFile(
|
||||
path.join(cwdDir, ".env"),
|
||||
`${providerAuthKeys.map((key) => `${key}=workspace-${key}`).join("\n")}\n`,
|
||||
);
|
||||
await writeEnvFile(
|
||||
path.join(stateDir, ".env"),
|
||||
`${providerAuthKeys.map((key) => `${key}=global-${key}`).join("\n")}\n`,
|
||||
);
|
||||
|
||||
clearEnv(providerAuthKeys);
|
||||
vi.spyOn(process, "cwd").mockReturnValue(cwdDir);
|
||||
|
||||
loadDotEnv({ quiet: true });
|
||||
|
||||
for (const key of providerAuthKeys) {
|
||||
expect(process.env[key], `${key} should come from trusted global .env`).toBe(
|
||||
`global-${key}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks runtime-control variables from workspace .env", async () => {
|
||||
await withIsolatedEnvAndCwd(async () => {
|
||||
await withDotEnvFixture(async ({ cwdDir }) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import dotenv from "dotenv";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { listKnownProviderAuthEnvVarNames } from "../secrets/provider-env-vars.js";
|
||||
import { resolveConfigDir } from "../utils.js";
|
||||
import { resolveRequiredHomeDir } from "./home-dir.js";
|
||||
import {
|
||||
@@ -13,10 +14,95 @@ import {
|
||||
|
||||
const logger = createSubsystemLogger("infra:dotenv");
|
||||
|
||||
const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
|
||||
"ALL_PROXY",
|
||||
const BLOCKED_PROVIDER_AUTH_WORKSPACE_DOTENV_KEYS = [
|
||||
"AI_GATEWAY_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"ANTHROPIC_OAUTH_TOKEN",
|
||||
"ARCEEAI_API_KEY",
|
||||
"AZURE_OPENAI_API_KEY",
|
||||
"AZURE_SPEECH_API_KEY",
|
||||
"AZURE_SPEECH_KEY",
|
||||
"AZURE_SPEECH_REGION",
|
||||
"BRAVE_API_KEY",
|
||||
"BYTEPLUS_API_KEY",
|
||||
"BYTEPLUS_SEED_SPEECH_API_KEY",
|
||||
"CEREBRAS_API_KEY",
|
||||
"CHUTES_API_KEY",
|
||||
"CHUTES_OAUTH_TOKEN",
|
||||
"CLOUDFLARE_AI_GATEWAY_API_KEY",
|
||||
"COMFY_API_KEY",
|
||||
"COMFY_CLOUD_API_KEY",
|
||||
"COPILOT_GITHUB_TOKEN",
|
||||
"DASHSCOPE_API_KEY",
|
||||
"DEEPGRAM_API_KEY",
|
||||
"DEEPINFRA_API_KEY",
|
||||
"DEEPSEEK_API_KEY",
|
||||
"ELEVENLABS_API_KEY",
|
||||
"EXA_API_KEY",
|
||||
"FAL_API_KEY",
|
||||
"FAL_KEY",
|
||||
"FIRECRAWL_API_KEY",
|
||||
"FIREWORKS_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"GH_TOKEN",
|
||||
"GITHUB_TOKEN",
|
||||
"GOOGLE_API_KEY",
|
||||
"GOOGLE_CLOUD_API_KEY",
|
||||
"GRADIUM_API_KEY",
|
||||
"GROQ_API_KEY",
|
||||
"HF_TOKEN",
|
||||
"HUGGINGFACE_HUB_TOKEN",
|
||||
"INWORLD_API_KEY",
|
||||
"KILOCODE_API_KEY",
|
||||
"KIMICODE_API_KEY",
|
||||
"KIMI_API_KEY",
|
||||
"LITELLM_API_KEY",
|
||||
"LM_API_TOKEN",
|
||||
"MINIMAX_API_KEY",
|
||||
"MINIMAX_CODE_PLAN_KEY",
|
||||
"MINIMAX_CODING_API_KEY",
|
||||
"MINIMAX_OAUTH_TOKEN",
|
||||
"MISTRAL_API_KEY",
|
||||
"MODELSTUDIO_API_KEY",
|
||||
"MOONSHOT_API_KEY",
|
||||
"NVIDIA_API_KEY",
|
||||
"OLLAMA_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"OPENCODE_API_KEY",
|
||||
"OPENCODE_ZEN_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
"PERPLEXITY_API_KEY",
|
||||
"QIANFAN_API_KEY",
|
||||
"QWEN_API_KEY",
|
||||
"RUNWAY_API_KEY",
|
||||
"RUNWAYML_API_SECRET",
|
||||
"SENSEAUDIO_API_KEY",
|
||||
"SGLANG_API_KEY",
|
||||
"SPEECH_KEY",
|
||||
"SPEECH_REGION",
|
||||
"STEPFUN_API_KEY",
|
||||
"SYNTHETIC_API_KEY",
|
||||
"TAVILY_API_KEY",
|
||||
"TOGETHER_API_KEY",
|
||||
"TOKENHUB_API_KEY",
|
||||
"VENICE_API_KEY",
|
||||
"VLLM_API_KEY",
|
||||
"VOLCANO_ENGINE_API_KEY",
|
||||
"VOLCENGINE_TTS_API_KEY",
|
||||
"VOLCENGINE_TTS_APPID",
|
||||
"VOLCENGINE_TTS_TOKEN",
|
||||
"VOYAGE_API_KEY",
|
||||
"VYDRA_API_KEY",
|
||||
"XAI_API_KEY",
|
||||
"XIAOMI_API_KEY",
|
||||
"XI_API_KEY",
|
||||
"ZAI_API_KEY",
|
||||
"Z_AI_API_KEY",
|
||||
] as const;
|
||||
|
||||
const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
|
||||
...BLOCKED_PROVIDER_AUTH_WORKSPACE_DOTENV_KEYS,
|
||||
"ALL_PROXY",
|
||||
"BROWSER_EXECUTABLE_PATH",
|
||||
"CLAWHUB_AUTH_TOKEN",
|
||||
"CLAWHUB_CONFIG_PATH",
|
||||
@@ -36,7 +122,6 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
|
||||
"NODE_TLS_REJECT_UNAUTHORIZED",
|
||||
"NO_PROXY",
|
||||
"NPM_EXECPATH",
|
||||
"OPENAI_API_KEY",
|
||||
"OPENAI_API_KEYS",
|
||||
"OPENCLAW_AGENT_DIR",
|
||||
"OPENCLAW_ALLOW_PLUGIN_INSTALL_OVERRIDES",
|
||||
@@ -120,13 +205,30 @@ function shouldBlockRuntimeDotEnvKey(key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldBlockWorkspaceDotEnvKey(key: string): boolean {
|
||||
function buildProviderAuthWorkspaceDotEnvBlocklist(): ReadonlySet<string> {
|
||||
const keys = new Set<string>(BLOCKED_PROVIDER_AUTH_WORKSPACE_DOTENV_KEYS);
|
||||
for (const rawKey of listKnownProviderAuthEnvVarNames({
|
||||
includeUntrustedWorkspacePlugins: false,
|
||||
})) {
|
||||
const key = normalizeEnvVarKey(rawKey, { portable: true });
|
||||
if (key) {
|
||||
keys.add(key.toUpperCase());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function shouldBlockWorkspaceDotEnvKey(
|
||||
key: string,
|
||||
getProviderAuthBlockedKeys: () => ReadonlySet<string>,
|
||||
): boolean {
|
||||
const upper = key.toUpperCase();
|
||||
return (
|
||||
shouldBlockWorkspaceRuntimeDotEnvKey(upper) ||
|
||||
BLOCKED_WORKSPACE_DOTENV_KEYS.has(upper) ||
|
||||
BLOCKED_WORKSPACE_DOTENV_PREFIXES.some((prefix) => upper.startsWith(prefix)) ||
|
||||
BLOCKED_WORKSPACE_DOTENV_SUFFIXES.some((suffix) => upper.endsWith(suffix))
|
||||
BLOCKED_WORKSPACE_DOTENV_SUFFIXES.some((suffix) => upper.endsWith(suffix)) ||
|
||||
getProviderAuthBlockedKeys().has(upper)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,9 +282,14 @@ function readDotEnvFile(params: {
|
||||
}
|
||||
|
||||
export function loadWorkspaceDotEnvFile(filePath: string, opts?: { quiet?: boolean }) {
|
||||
let providerAuthBlockedKeys: ReadonlySet<string> | undefined;
|
||||
const getProviderAuthBlockedKeys = () => {
|
||||
providerAuthBlockedKeys ??= buildProviderAuthWorkspaceDotEnvBlocklist();
|
||||
return providerAuthBlockedKeys;
|
||||
};
|
||||
const parsed = readDotEnvFile({
|
||||
filePath,
|
||||
shouldBlockKey: shouldBlockWorkspaceDotEnvKey,
|
||||
shouldBlockKey: (key) => shouldBlockWorkspaceDotEnvKey(key, getProviderAuthBlockedKeys),
|
||||
quiet: opts?.quiet ?? true,
|
||||
});
|
||||
if (!parsed) {
|
||||
|
||||
Reference in New Issue
Block a user