Skip to content

Commit 7376ca7

Browse files
fix(config): mask wandb_api_key in config set output (#66)
* fix(config): mask wandb_api_key in `config set` output `config set wandb_api_key <value>` echoed the full secret to stdout, leaking it into terminal scrollback, CI logs, and tool transcripts whenever the install/migration flow ran. `config show` and `status` already masked to first-4-chars + ellipsis; only `set` was missed when `wandb_api_key` was added as a writable key. Extracted a single `maskSecret` helper used by all three call sites so the mask format lives in one place. `config get wandb_api_key` is intentionally left un-masked — it's the programmatic retrieval path (eg. the migration in the weave-install skill). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: consolidate config-set mask tests, trim comments Four tests of the same `config set` call (three on wandb_api_key, one on weave_project) collapse into one walking both branches of the mask-or-not decision: one CLI spawn for the secret path, one for the plain path. Also drops the maskSecret docstring (name is self-evident) and the test file's seven-line header in favor of a two-line "what + why". 9/9 passing (was 12/12 — the four merged into one and the suite wrapper went away). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): route cmdInstall mask calls through maskSecret Addresses review feedback on #66 — two remaining `value.slice(0, 4)}…` sites in cmdInstall (env-var notice and post-prompt echo) now go through the same helper as config show/set and status. Audited the rest of the codebase: no other call sites log secrets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 879373f commit 7376ca7

2 files changed

Lines changed: 77 additions & 5 deletions

File tree

src/cli.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ async function cmdInstall(force: boolean, nonInteractive: boolean): Promise<void
136136
}
137137

138138
if (envApiKey) {
139-
console.warn(`⚠ Using WANDB_API_KEY from environment: ${envApiKey.slice(0, 4)}`);
139+
console.warn(`⚠ Using WANDB_API_KEY from environment: ${maskSecret(envApiKey)}`);
140140
} else if (!effectiveApiKey) {
141141
console.warn('- WANDB_API_KEY not set. Run: weave-claude-code config set wandb_api_key <your-api-key>');
142142
}
@@ -164,7 +164,7 @@ async function cmdInstall(force: boolean, nonInteractive: boolean): Promise<void
164164
settings = loadSettings();
165165
settings.wandb_api_key = value;
166166
saveSettings(settings);
167-
console.log(`✓ Set wandb_api_key = ${value.slice(0, 4)}`);
167+
console.log(`✓ Set wandb_api_key = ${maskSecret(value)}`);
168168
} else {
169169
console.log('- Skipped wandb_api_key (set later: weave-claude-code config set wandb_api_key <key>)');
170170
}
@@ -186,6 +186,10 @@ async function cmdInstall(force: boolean, nonInteractive: boolean): Promise<void
186186
// config
187187
// ---------------------------------------------------------------------------
188188

189+
function maskSecret(value: string): string {
190+
return `${value.slice(0, 4)}…`;
191+
}
192+
189193
async function cmdConfig(args: string[]): Promise<void> {
190194
const action = args[0];
191195

@@ -211,7 +215,7 @@ async function cmdConfig(args: string[]): Promise<void> {
211215
: settings.wandb_api_key
212216
? 'settings.json'
213217
: 'not set';
214-
const apiKeyDisplay = effectiveApiKey ? `${effectiveApiKey.slice(0, 4)} [${apiKeySource}]` : `(not set)`;
218+
const apiKeyDisplay = effectiveApiKey ? `${maskSecret(effectiveApiKey)} [${apiKeySource}]` : `(not set)`;
215219

216220
console.log('Current configuration:');
217221
console.log(` log_file: ${settings.log_file}`);
@@ -289,7 +293,10 @@ async function cmdConfig(args: string[]): Promise<void> {
289293
const coerced = key === 'debug' ? value === 'true' : value;
290294
(settings as unknown as Record<string, unknown>)[key] = coerced;
291295
saveSettings(settings);
292-
console.log(`✓ Set ${key} = ${value}`);
296+
const displayValue = key === 'wandb_api_key' && typeof coerced === 'string'
297+
? maskSecret(coerced)
298+
: coerced;
299+
console.log(`✓ Set ${key} = ${displayValue}`);
293300
return;
294301
}
295302

@@ -345,7 +352,7 @@ async function cmdStatus(): Promise<void> {
345352
const effectiveApiKey = process.env['WANDB_API_KEY'] ?? settings.wandb_api_key ?? null;
346353
if (effectiveApiKey) {
347354
const apiKeySource = process.env['WANDB_API_KEY'] ? 'WANDB_API_KEY env var' : 'settings.json';
348-
console.log(`✓ W&B API key: ${effectiveApiKey.slice(0, 4)} (from ${apiKeySource})`);
355+
console.log(`✓ W&B API key: ${maskSecret(effectiveApiKey)} (from ${apiKeySource})`);
349356
} else {
350357
console.log('✗ W&B API key: not configured');
351358
console.log(' Run: weave-claude-code config set wandb_api_key <your-api-key>');
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// SPDX-FileCopyrightText: 2026 CoreWeave, Inc.
2+
// SPDX-License-Identifier: MIT
3+
// SPDX-PackageName: weave-claude-code
4+
5+
// `config set` must mask wandb_api_key in stdout (it didn't — see #66) but
6+
// must still echo non-sensitive keys in full and persist the full secret.
7+
8+
import { test } from 'node:test';
9+
import assert from 'node:assert/strict';
10+
import { spawn } from 'node:child_process';
11+
import * as fs from 'node:fs';
12+
import * as path from 'node:path';
13+
import { fileURLToPath } from 'node:url';
14+
15+
const HERE = path.dirname(fileURLToPath(import.meta.url));
16+
const REPO_ROOT = path.resolve(HERE, '..');
17+
const CLI = path.join(REPO_ROOT, 'src', 'cli.ts');
18+
const SECRET = 'wandb_v1_SUPERSECRETvalueDoNotLeak0123456789';
19+
20+
function newHome(label: string): { home: string; settingsFile: string } {
21+
const home = fs.mkdtempSync(`/tmp/wcp-cfgset-${label}-`);
22+
const dir = path.join(home, '.weave-claude-code');
23+
fs.mkdirSync(path.join(dir, 'logs'), { recursive: true });
24+
const settingsFile = path.join(dir, 'settings.json');
25+
fs.writeFileSync(settingsFile, JSON.stringify({
26+
log_file: path.join(dir, 'logs', 'daemon.log'),
27+
daemon_socket: path.join(dir, 'daemon.sock'),
28+
weave_project: null,
29+
wandb_api_key: null,
30+
debug: false,
31+
installed_at: '2026-01-01T00:00:00Z',
32+
version: '0.0.0-test',
33+
}));
34+
return { home, settingsFile };
35+
}
36+
37+
function runCli(home: string, args: string[]): Promise<{ stdout: string; code: number | null }> {
38+
return new Promise((resolve, reject) => {
39+
const env = { ...process.env, HOME: home };
40+
delete env.WANDB_API_KEY;
41+
delete env.WEAVE_PROJECT;
42+
const child = spawn(process.execPath, ['--import', 'tsx', CLI, ...args], { cwd: REPO_ROOT, env });
43+
let stdout = '';
44+
child.stdout.on('data', (b) => { stdout += b.toString(); });
45+
child.on('error', reject);
46+
child.on('exit', (code) => resolve({ stdout, code }));
47+
});
48+
}
49+
50+
test('config set: masks wandb_api_key, echoes weave_project in full', async () => {
51+
const { home, settingsFile } = newHome('mask');
52+
try {
53+
const apiKey = await runCli(home, ['config', 'set', 'wandb_api_key', SECRET]);
54+
assert.equal(apiKey.code, 0);
55+
assert.equal(apiKey.stdout.includes(SECRET), false, `stdout leaked the secret:\n${apiKey.stdout}`);
56+
assert.match(apiKey.stdout, /wand/);
57+
assert.equal(JSON.parse(fs.readFileSync(settingsFile, 'utf8')).wandb_api_key, SECRET);
58+
59+
const project = await runCli(home, ['config', 'set', 'weave_project', 'my-entity/my-project']);
60+
assert.equal(project.code, 0);
61+
assert.match(project.stdout, /my-entity\/my-project/);
62+
} finally {
63+
fs.rmSync(home, { recursive: true, force: true });
64+
}
65+
});

0 commit comments

Comments
 (0)