From 74405327bed6c256617fdc467d7ca8bfe6ff0f9f Mon Sep 17 00:00:00 2001 From: Daniel Wise Date: Thu, 28 May 2026 08:12:23 -0700 Subject: [PATCH 1/2] fix(ai): redact modern sk-proj-/sk-ant- API keys from AI context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit redactSensitiveText only matched plain-alphanumeric sk- keys, so OpenAI project keys (sk-proj-…) and Anthropic keys (sk-ant-…) — which contain '-'/'_' — slipped into recentShellHistory and history.jsonl. Broaden the pattern to include '-'/'_', and add --openai-key (spaced + = forms) to sanitizeCommandArgs so dub's own command history is redacted too. Closes DUB-85 --- packages/cli/src/lib/history.test.ts | 37 ++++++++++++++++++++++++++++ packages/cli/src/lib/history.ts | 10 +++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/history.test.ts b/packages/cli/src/lib/history.test.ts index b1173bc9..80880f6e 100644 --- a/packages/cli/src/lib/history.test.ts +++ b/packages/cli/src/lib/history.test.ts @@ -72,6 +72,24 @@ describe('history', () => { ]); }); + it('redacts --openai-key args in both spaced and = forms', () => { + const sanitized = sanitizeCommandArgs([ + 'ai', + 'env', + '--openai-key', + 'sk-proj-abc123_DEF-456ghiJKLmno0pqRstuVwx', + '--openai-key=sk-proj-zzz999_YYY-888xxxWWWvvvUUU', + ]); + + expect(sanitized).toEqual([ + 'ai', + 'env', + '--openai-key', + '[REDACTED]', + '--openai-key=[REDACTED]', + ]); + }); + it('does not redact non-secret model args', () => { const sanitized = sanitizeCommandArgs([ 'ai', @@ -104,6 +122,25 @@ describe('history', () => { expect(redacted).toContain('[REDACTED]'); }); + it('redacts modern openai/anthropic key formats containing - and _', () => { + const redacted = redactSensitiveText( + [ + 'dub ai env --openai-key sk-proj-abc123_DEF-456ghiJKLmno0pqRstuVwx', + 'pasted sk-ant-api03-abc_def-ghiJKLmno0pqRstuVwx12345 into a note', + ].join('\n'), + ); + + expect(redacted).not.toContain('sk-proj-'); + expect(redacted).not.toContain('sk-ant-'); + expect(redacted).toContain('[REDACTED]'); + }); + + it('does not redact short sk- branch names', () => { + expect(redactSensitiveText('git checkout sk-fix-login')).toBe( + 'git checkout sk-fix-login', + ); + }); + it('normalizes carriage-return spinner lines to the final visible content', () => { expect(normalizeHistoryLine('- thinking\r\\ thinking\rfinal output')).toBe( 'final output', diff --git a/packages/cli/src/lib/history.ts b/packages/cli/src/lib/history.ts index 77830c30..29c0c44c 100644 --- a/packages/cli/src/lib/history.ts +++ b/packages/cli/src/lib/history.ts @@ -23,6 +23,7 @@ const REDACTED_ARGS = new Set([ '--gemini-key', '--anthropic-key', '--gateway-key', + '--openai-key', ]); const REDACTED_PLACEHOLDER = '[REDACTED]'; @@ -118,6 +119,11 @@ export function sanitizeCommandArgs(args: string[]): string[] { if (arg.startsWith('--gateway-key=')) { sanitized[sanitized.length - 1] = `--gateway-key=${REDACTED_PLACEHOLDER}`; + continue; + } + + if (arg.startsWith('--openai-key=')) { + sanitized[sanitized.length - 1] = `--openai-key=${REDACTED_PLACEHOLDER}`; } } @@ -148,7 +154,9 @@ export function redactSensitiveText(value: string): string { REDACTED_PLACEHOLDER, ); - redacted = redacted.replace(/\bsk-[A-Za-z0-9]{12,}\b/g, REDACTED_PLACEHOLDER); + // Matches modern key formats whose body contains '-'/'_' (e.g. OpenAI + // 'sk-proj-…', Anthropic 'sk-ant-api03-…') as well as legacy 'sk-…' keys. + redacted = redacted.replace(/\bsk-[A-Za-z0-9_-]{12,}/g, REDACTED_PLACEHOLDER); return redacted; } From 5b422a6caf2d8c6e18837622ec964fc9c5320fb2 Mon Sep 17 00:00:00 2001 From: Daniel Wise Date: Thu, 28 May 2026 10:57:05 -0700 Subject: [PATCH 2/2] fix(ai)!: stop accepting API keys as CLI flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Passing a key to `dub ai env --openai-key …` (and --gemini-key, --anthropic-key, --gateway-key) leaks it into shell history and the OS process list — exposure dub cannot redact. Reject those flags with a helpful error pointing to the masked `dub ai setup` wizard or a pre-set DUBSTACK_*_API_KEY env var. Same recommendation scrubbed from docs, skills, and the provider/env error hints that previously suggested them. BREAKING CHANGE: `dub ai env` no longer accepts --gemini-key, --anthropic-key, --gateway-key, or --openai-key. Use `dub ai setup` (masked prompt) or export the DUBSTACK_*_API_KEY env var instead. Closes DUB-85 --- .agents/skills/dubstack/SKILL.md | 6 +-- QUICKSTART.md | 8 ++-- README.md | 29 +++++--------- .../docs/content/docs/guides/ai-assistant.mdx | 8 ++-- apps/docs/content/docs/index.mdx | 13 +++---- packages/cli/src/commands/ai-env.test.ts | 20 +++++++++- packages/cli/src/commands/ai-env.ts | 38 ++++++++++++++++++- packages/cli/src/index.ts | 22 ++++++----- packages/cli/src/lib/ai-provider.ts | 23 +++++------ skills/dubstack/SKILL.md | 8 ++-- 10 files changed, 105 insertions(+), 70 deletions(-) diff --git a/.agents/skills/dubstack/SKILL.md b/.agents/skills/dubstack/SKILL.md index 2bfd1a1a..d902e938 100644 --- a/.agents/skills/dubstack/SKILL.md +++ b/.agents/skills/dubstack/SKILL.md @@ -169,9 +169,9 @@ dub trunk ## AI Workflow ```bash -dub ai env --gemini-key "" -# or -dub ai env --gateway-key "" +# choose a provider and enter your key via a masked prompt +# (keys passed as CLI flags would leak into shell history) +dub ai setup dub config ai-assistant on dub config ai-defaults create on diff --git a/QUICKSTART.md b/QUICKSTART.md index da5c149c..4f8d2494 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -56,16 +56,16 @@ dub ai env \ dub config ai-provider bedrock ``` -For direct Anthropic API access: +For direct Anthropic API access (set the key with `dub ai setup` or your shell profile / secrets manager — never pass it as a CLI flag, which leaks it into shell history): ```bash -dub ai env \ - --anthropic-key "" \ - --anthropic-model "claude-sonnet-4-20250514" +dub ai env --anthropic-model "claude-sonnet-4-20250514" dub config ai-provider anthropic ``` +DubStack reads the key from the `DUBSTACK_ANTHROPIC_API_KEY` environment variable. + DubStack does not add or manage AWS secret key environment variables for Bedrock. `dub ai setup` and `dub ai env` print the exact activation command to run after updating your shell profile. diff --git a/README.md b/README.md index 671922dc..a434cc47 100644 --- a/README.md +++ b/README.md @@ -1014,21 +1014,16 @@ When the wizard writes env vars, DubStack loads them into the current `dub` proc ### `dub ai env` -Write DubStack AI provider settings into your shell profile (macOS/Linux shells). +Write DubStack AI provider model/endpoint settings into your shell profile (macOS/Linux shells). -```bash -# write Gemini key -dub ai env --gemini-key "" - -# write Gateway key -dub ai env --gateway-key "" - -# write Anthropic key -dub ai env --anthropic-key "" - -# write OpenAI key -dub ai env --openai-key "" +> **API keys are not set through `dub ai env`.** Passing a secret as a CLI flag leaks it into your shell history and the OS process list. Set keys one of two ways: +> +> - **Interactive:** `dub ai setup` — prompts for the key with masked input that never enters your shell history. +> - **Non-interactive / CI:** export the env var directly, e.g. `export DUBSTACK_OPENAI_API_KEY=""` (via your shell profile or a secrets manager). +> +> DubStack reads keys from `DUBSTACK_GEMINI_API_KEY`, `DUBSTACK_ANTHROPIC_API_KEY`, `DUBSTACK_AI_GATEWAY_API_KEY`, and `DUBSTACK_OPENAI_API_KEY`. +```bash # write Ollama endpoint dub ai env --ollama-base-url "http://localhost:11434" @@ -1053,14 +1048,8 @@ dub ai env \ --bedrock-region "us-west-2" \ --bedrock-model "us.anthropic.claude-sonnet-4-6" -# write multiple keys -dub ai env --gemini-key "" --gateway-key "" --openai-key "" - -# write key + model together -dub ai env --gemini-key "" --gemini-model "gemini-3-flash-preview" - # target a specific profile file explicitly -dub ai env --gemini-key "" --profile ~/.zshrc +dub ai env --gemini-model "gemini-3-flash-preview" --profile ~/.zshrc ``` Supported automatic profile detection: diff --git a/apps/docs/content/docs/guides/ai-assistant.mdx b/apps/docs/content/docs/guides/ai-assistant.mdx index 9f2e7eee..c3e9d6d8 100644 --- a/apps/docs/content/docs/guides/ai-assistant.mdx +++ b/apps/docs/content/docs/guides/ai-assistant.mdx @@ -122,23 +122,21 @@ When the wizard writes env vars, DubStack loads them into the current `dub` proc ## `dub ai env` -Use `dub ai env` when you want explicit shell-profile edits without the wizard. +Use `dub ai env` for explicit shell-profile edits to models and endpoints without the wizard. + +API keys are **not** set here — passing a secret as a CLI flag leaks it into your shell history and the OS process list. Set keys with `dub ai setup` (masked prompt) or export the env var directly: `DUBSTACK_GEMINI_API_KEY`, `DUBSTACK_ANTHROPIC_API_KEY`, `DUBSTACK_AI_GATEWAY_API_KEY`, or `DUBSTACK_OPENAI_API_KEY`. ```bash # Gemini -dub ai env --gemini-key "" dub ai env --gemini-model "gemini-3-flash-preview" # AI Gateway -dub ai env --gateway-key "" dub ai env --gateway-model "google/gemini-3-flash" # Anthropic -dub ai env --anthropic-key "" dub ai env --anthropic-model "claude-sonnet-4-20250514" # OpenAI -dub ai env --openai-key "" dub ai env --openai-model "gpt-5.5" # Bedrock diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx index 2f116bb6..ffcf6451 100644 --- a/apps/docs/content/docs/index.mdx +++ b/apps/docs/content/docs/index.mdx @@ -120,14 +120,11 @@ dub post-merge ## Optional: Enable AI Assistant ```bash -# Add one API key to your shell profile -dub ai env --gemini-key "" -# or: -dub ai env --gateway-key "" -# or: -dub ai env --openai-key "" - -# Reload your shell +# Choose a provider and enter your key via a masked prompt +# (the wizard never writes the key to your shell history) +dub ai setup + +# Reload your shell so the new env vars take effect source ~/.zshrc # Enable assistant for this repo diff --git a/packages/cli/src/commands/ai-env.test.ts b/packages/cli/src/commands/ai-env.test.ts index 5de21c8c..19e6cfcc 100644 --- a/packages/cli/src/commands/ai-env.test.ts +++ b/packages/cli/src/commands/ai-env.test.ts @@ -2,7 +2,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { configureAiEnv } from './ai-env'; +import { assertNoApiKeyFlags, configureAiEnv } from './ai-env'; let tempDir: string; let envSnapshot: NodeJS.ProcessEnv; @@ -288,3 +288,21 @@ describe('configureAiEnv', () => { ); }); }); + +describe('assertNoApiKeyFlags', () => { + it('throws and names the offending flag when an API key is passed', () => { + expect(() => assertNoApiKeyFlags({ openaiKey: 'sk-proj-leak' })).toThrow( + '--openai-key', + ); + }); + + it('lists every offending flag when several keys are passed', () => { + expect(() => + assertNoApiKeyFlags({ geminiKey: 'g', anthropicKey: 'a' }), + ).toThrow('--gemini-key, --anthropic-key'); + }); + + it('is a no-op when no API key flags are present', () => { + expect(() => assertNoApiKeyFlags({})).not.toThrow(); + }); +}); diff --git a/packages/cli/src/commands/ai-env.ts b/packages/cli/src/commands/ai-env.ts index 55030d84..cc500923 100644 --- a/packages/cli/src/commands/ai-env.ts +++ b/packages/cli/src/commands/ai-env.ts @@ -18,6 +18,41 @@ const OPENAI_MODEL_NAME = 'DUBSTACK_OPENAI_MODEL'; const OLLAMA_BASE_URL_NAME = 'DUBSTACK_OLLAMA_BASE_URL'; const OLLAMA_MODEL_NAME = 'DUBSTACK_OLLAMA_MODEL'; +const API_KEY_FLAGS: ReadonlyArray<[keyof ApiKeyFlagInput, string]> = [ + ['geminiKey', '--gemini-key'], + ['anthropicKey', '--anthropic-key'], + ['gatewayKey', '--gateway-key'], + ['openaiKey', '--openai-key'], +]; + +export interface ApiKeyFlagInput { + geminiKey?: string; + anthropicKey?: string; + gatewayKey?: string; + openaiKey?: string; +} + +/** + * Rejects API keys supplied as CLI flags. Passing a secret in argv leaks it + * into shell history and the OS process list — exposure dub cannot redact. + * Keys must come from the masked `dub ai setup` wizard or a pre-set env var. + */ +export function assertNoApiKeyFlags(flags: ApiKeyFlagInput): void { + const used = API_KEY_FLAGS.filter(([key]) => flags[key] !== undefined).map( + ([, name]) => name, + ); + if (used.length === 0) return; + + throw new DubError( + `Passing API keys as CLI flags is no longer supported (${used.join(', ')}).`, + [ + 'Run `dub ai setup` for an interactive, masked prompt — the key never enters your shell history or the process list.', + 'For non-interactive or CI use, set the env var directly (e.g. `export DUBSTACK_OPENAI_API_KEY=…`) via your shell profile or a secrets manager.', + 'If you already ran this with a real key, rotate it now: it was written to your shell history and was visible to other local processes.', + ], + ); +} + export interface ConfigureAiEnvOptions { geminiKey?: string; anthropicKey?: string; @@ -61,7 +96,8 @@ export async function configureAiEnv( !options.ollamaModel ) { throw new DubError('Provide at least one key, model, or Bedrock setting.', [ - "Pass at least one of '--gemini-key', '--anthropic-key', '--gateway-key', '--openai-key', '--gemini-model', '--anthropic-model', '--gateway-model', '--openai-model', '--bedrock-profile', '--bedrock-region', '--bedrock-model', '--ollama-base-url', or '--ollama-model'.", + "Pass at least one of '--gemini-model', '--anthropic-model', '--gateway-model', '--openai-model', '--bedrock-profile', '--bedrock-region', '--bedrock-model', '--ollama-base-url', or '--ollama-model'.", + "To set an API key, run 'dub ai setup' (masked prompt) or export the DUBSTACK_*_API_KEY env var.", ]); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 2154fa2c..c1bd9a5e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -22,7 +22,7 @@ import { realpathSync } from 'node:fs'; import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; import chalk, { Chalk } from 'chalk'; -import { Command } from 'commander'; +import { Command, Option } from 'commander'; import { abortCommand } from './commands/abort'; import { absorb } from './commands/absorb'; import { back, listBackHistory } from './commands/back'; @@ -3333,10 +3333,13 @@ See also: .description( 'Write DubStack AI provider settings to your shell profile (macOS/Linux)', ) - .option('--gemini-key ', 'Set DUBSTACK_GEMINI_API_KEY') - .option('--anthropic-key ', 'Set DUBSTACK_ANTHROPIC_API_KEY') - .option('--gateway-key ', 'Set DUBSTACK_AI_GATEWAY_API_KEY') - .option('--openai-key ', 'Set DUBSTACK_OPENAI_API_KEY') + // Removed: API keys must not be passed in argv (leaks into shell history + // and the process list). Hidden but still parsed so we can reject with a + // helpful message instead of a cryptic "unknown option" error. + .addOption(new Option('--gemini-key ').hideHelp()) + .addOption(new Option('--anthropic-key ').hideHelp()) + .addOption(new Option('--gateway-key ').hideHelp()) + .addOption(new Option('--openai-key ').hideHelp()) .option('--ollama-base-url ', 'Set DUBSTACK_OLLAMA_BASE_URL') .option('--gemini-model ', 'Set DUBSTACK_GEMINI_MODEL') .option('--anthropic-model ', 'Set DUBSTACK_ANTHROPIC_MODEL') @@ -3383,12 +3386,11 @@ See also: profile?: string; shell?: string; }) => { - const { configureAiEnv } = await import('./commands/ai-env'); + const { assertNoApiKeyFlags, configureAiEnv } = await import( + './commands/ai-env' + ); + assertNoApiKeyFlags(options); const result = await configureAiEnv({ - geminiKey: options.geminiKey, - anthropicKey: options.anthropicKey, - gatewayKey: options.gatewayKey, - openaiKey: options.openaiKey, ollamaBaseUrl: options.ollamaBaseUrl, geminiModel: options.geminiModel, anthropicModel: options.anthropicModel, diff --git a/packages/cli/src/lib/ai-provider.ts b/packages/cli/src/lib/ai-provider.ts index 4ef7dcb4..be4761df 100644 --- a/packages/cli/src/lib/ai-provider.ts +++ b/packages/cli/src/lib/ai-provider.ts @@ -111,12 +111,9 @@ export function resolveAiProvider(input: { } throw new DubError('AI assistant has no configured provider.', [ - "Run 'dub ai setup' for an interactive guided setup.", - "Run 'dub ai env --gemini-key ' to configure Gemini.", - "Run 'dub ai env --anthropic-key ' to configure Anthropic.", - "Run 'dub ai env --gateway-key ' to configure the AI Gateway.", + "Run 'dub ai setup' for an interactive guided setup (prompts for your key with masked input).", + 'Or set a provider key env var directly: DUBSTACK_GEMINI_API_KEY, DUBSTACK_ANTHROPIC_API_KEY, DUBSTACK_AI_GATEWAY_API_KEY, or DUBSTACK_OPENAI_API_KEY.', "Run 'dub ai env --bedrock-region --bedrock-model ' to configure Bedrock.", - "Run 'dub ai env --openai-key ' to configure OpenAI.", "Run 'dub config ai-provider ollama' to use a local Ollama endpoint.", ]); } @@ -259,8 +256,8 @@ function resolveGoogleProvider( throw new DubError( 'Gemini is selected but DUBSTACK_GEMINI_API_KEY is not set.', [ - "Run 'dub ai setup' for guided provider setup.", - "Run 'dub ai env --gemini-key ' to write the key to your shell profile.", + "Run 'dub ai setup' for guided provider setup (masked key prompt).", + 'Or set DUBSTACK_GEMINI_API_KEY in your shell profile or secrets manager.', ], ); } @@ -292,8 +289,8 @@ function resolveAnthropicProvider( throw new DubError( 'Anthropic is selected but DUBSTACK_ANTHROPIC_API_KEY is not set.', [ - "Run 'dub ai setup' for guided provider setup.", - "Run 'dub ai env --anthropic-key ' to write the key to your shell profile.", + "Run 'dub ai setup' for guided provider setup (masked key prompt).", + 'Or set DUBSTACK_ANTHROPIC_API_KEY in your shell profile or secrets manager.', ], ); } @@ -319,8 +316,8 @@ function resolveGatewayProvider( throw new DubError( 'AI Gateway is selected but DUBSTACK_AI_GATEWAY_API_KEY is not set.', [ - "Run 'dub ai setup' for guided provider setup.", - "Run 'dub ai env --gateway-key ' to write the key to your shell profile.", + "Run 'dub ai setup' for guided provider setup (masked key prompt).", + 'Or set DUBSTACK_AI_GATEWAY_API_KEY in your shell profile or secrets manager.', ], ); } @@ -406,8 +403,8 @@ function resolveOpenAiProvider( throw new DubError( 'OpenAI is selected but DUBSTACK_OPENAI_API_KEY is not set.', [ - "Run 'dub ai setup' for guided provider setup.", - "Run 'dub ai env --openai-key ' to write the key to your shell profile.", + "Run 'dub ai setup' for guided provider setup (masked key prompt).", + 'Or set DUBSTACK_OPENAI_API_KEY in your shell profile or secrets manager.', ], ); } diff --git a/skills/dubstack/SKILL.md b/skills/dubstack/SKILL.md index 367f07ae..30ea6c30 100644 --- a/skills/dubstack/SKILL.md +++ b/skills/dubstack/SKILL.md @@ -179,11 +179,9 @@ dub trunk ## AI Setup ```bash -dub ai env --gemini-key "" -# or -dub ai env --gateway-key "" -# or -dub ai env --openai-key "" +# choose a provider and enter your key via a masked prompt +# (keys passed as CLI flags would leak into shell history) +dub ai setup dub config ai-assistant on dub config ai-defaults create on