Skip to content

Commit a0b217b

Browse files
PR feedback: rename symbols, remove codex stub, fix hook naming, update tests
- Rename scanFiles → scanFilesForSecrets in secrets-scan.ts - Rename callbackCommand → hookCommand in command-tree.ts - Remove codex-pre-tool-use command (out of scope) - Remove PR-specific comments from command-tree.ts - Rename agent-prompt-submit → claude-prompt-submit - Rename agent-post-tool-use → claude-post-tool-use - Consolidate duplicate hook.test.ts tests, assert description shown - Update README.md via gen:docs
1 parent bf10cc2 commit a0b217b

12 files changed

Lines changed: 136 additions & 199 deletions

File tree

README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ Revoke a user token
154154
sonar api post "/api/user_tokens/revoke" --data '{"name":"my-token"}'
155155
```
156156

157+
157158
---
158159

159160
### `sonar integrate`
@@ -180,7 +181,7 @@ Setup SonarQube integration for Claude Code. This will install secrets scanning
180181

181182
| Option | Type | Required | Description | Default |
182183
| ------------------- | ------- | -------- | --------------------------------------------------------------------------- | ------- |
183-
| `--project`, `-p` | string | No | Project key | - |
184+
| `--project`, `-p` | string | No | SonarCloud project key (overrides auto-detected project) | - |
184185
| `--non-interactive` | boolean | No | Non-interactive mode (no prompts) | - |
185186
| `--global`, `-g` | boolean | No | Install hooks and config globally to ~/.claude instead of project directory | - |
186187

@@ -391,6 +392,40 @@ Update sonar CLI to the latest version
391392

392393
---
393394

395+
### `sonar hook`
396+
397+
Internal callback handlers for agent and git hooks
398+
399+
#### `sonar hook claude-pre-tool-use`
400+
401+
PreToolUse handler: scan files for secrets before agent reads them
402+
403+
---
404+
405+
#### `sonar hook codex-pre-tool-use`
406+
407+
PreToolUse handler for Codex: scan files for secrets before agent reads them
408+
409+
---
410+
411+
#### `sonar hook agent-prompt-submit`
412+
413+
UserPromptSubmit handler: scan prompts for secrets before sending
414+
415+
---
416+
417+
#### `sonar hook agent-post-tool-use`
418+
419+
PostToolUse handler: run SQAA analysis on modified files
420+
421+
**Options:**
422+
423+
| Option | Type | Required | Description | Default |
424+
| ----------------- | ------ | -------- | ---------------------- | ------- |
425+
| `--project`, `-p` | string | No | SonarCloud project key | - |
426+
427+
---
428+
394429
## Option Types
395430

396431
- `string` — text value (e.g. `--server https://sonarcloud.io`)
@@ -425,7 +460,7 @@ Both are enabled by default and share the same opt-out toggle. To disable all da
425460
sonar config telemetry --disabled
426461
```
427462

428-
No personally identifiable information is transmitted. File paths in error reports are anonymized by replacing your home directory with `~`.
463+
No personally identifiable information is transmitted.
429464

430465
## Contributing
431466

src/cli/command-tree.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -232,36 +232,27 @@ COMMAND_TREE.command('self-update')
232232

233233
// Hidden callback command — internal handlers for agent and git hooks.
234234
// Shell hook scripts call `sonar hook <event>` to delegate all business logic to TypeScript.
235-
export const callbackCommand = COMMAND_TREE.command('hook', { hidden: true })
236-
.description('Internal callback handlers for agent and git hooks')
235+
export const hookCommand = COMMAND_TREE.command('hook', { hidden: true })
236+
.description('Internal hook handlers for agent and git hooks')
237237
.enablePositionalOptions()
238238
.anonymousAction(function (this: Command) {
239239
this.outputHelp();
240240
});
241241

242-
callbackCommand
242+
hookCommand
243243
.command('claude-pre-tool-use')
244244
.description('PreToolUse handler: scan files for secrets before agent reads them')
245245
.anonymousAction(() => claudePreToolUse());
246246

247-
// codex-pre-tool-use reuses the same handler (same hook event, different agent name)
248-
callbackCommand
249-
.command('codex-pre-tool-use')
250-
.description('PreToolUse handler for Codex: scan files for secrets before agent reads them')
251-
.anonymousAction(() => claudePreToolUse());
252-
253-
// agent-prompt-submit and agent-post-tool-use are installed by `sonar integrate claude`.
254-
// They are implemented in subsequent PRs; stubs here ensure scripts don't fail with
255-
// "unknown command" on a CLI that only has CLI-244/245.
256-
callbackCommand
257-
.command('agent-prompt-submit')
247+
hookCommand
248+
.command('claude-prompt-submit')
258249
.description('UserPromptSubmit handler: scan prompts for secrets before sending')
259250
.anonymousAction(() => {
260251
return;
261252
});
262253

263-
callbackCommand
264-
.command('agent-post-tool-use')
254+
hookCommand
255+
.command('claude-post-tool-use')
265256
.option('-p, --project <project>', 'SonarCloud project key')
266257
.description('PostToolUse handler: run SQAA analysis on modified files')
267258
.anonymousAction(() => {

src/cli/commands/_common/install/secrets.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,13 @@ function cleanupOldVersionBinaries(binDir: string, currentBinaryName: string): v
217217
export function buildLocalBinaryName(platformInfo: PlatformInfo): string {
218218
return `sonar-secrets-${SONAR_SECRETS_VERSION}${buildPlatformSuffix(platformInfo)}`;
219219
}
220+
221+
/**
222+
* Returns the path to the installed sonar-secrets binary, or null if not present.
223+
* Never downloads — use this where silent operation is required (e.g. hook handlers).
224+
*/
225+
export function resolveSecretsBinaryPath(): string | null {
226+
const platform = detectPlatform();
227+
const binaryPath = join(BIN_DIR, buildLocalBinaryName(platform));
228+
return existsSync(binaryPath) ? binaryPath : null;
229+
}

src/cli/commands/analyze/secrets.ts

Lines changed: 48 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,57 @@ const BINARY_AUTH_TOKEN_ENV = 'SONAR_SECRETS_TOKEN';
4545
const SCAN_TIMEOUT_MS = 30000;
4646
const STDIN_READ_TIMEOUT_MS = 5000;
4747

48+
export const EXIT_CODE_SECRETS_FOUND = 51;
49+
50+
/**
51+
* Run sonar-secrets binary on the given files. Returns the full spawn result.
52+
* Kills the child process on timeout.
53+
*/
54+
export async function runSecretsBinary(
55+
binaryPath: string,
56+
files: string[],
57+
auth: ResolvedAuth,
58+
): Promise<SpawnResult> {
59+
let killChild: (() => void) | undefined;
60+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
61+
try {
62+
return await Promise.race([
63+
spawnProcess(binaryPath, ['--non-interactive', ...files], {
64+
stdin: 'pipe',
65+
stdout: 'pipe',
66+
stderr: 'pipe',
67+
env: { [BINARY_AUTH_URL_ENV]: auth.serverUrl, [BINARY_AUTH_TOKEN_ENV]: auth.token },
68+
onSpawn: (kill) => {
69+
killChild = kill;
70+
},
71+
}),
72+
new Promise<never>((_, reject) => {
73+
timeoutId = setTimeout(() => {
74+
killChild?.();
75+
reject(new Error(`Scan timed out after ${SCAN_TIMEOUT_MS}ms`));
76+
}, SCAN_TIMEOUT_MS);
77+
}),
78+
]);
79+
} finally {
80+
clearTimeout(timeoutId);
81+
}
82+
}
83+
4884
async function handleCheckCommand(
4985
options: AnalyzeSecretsOptions,
5086
auth: ResolvedAuth,
5187
): Promise<void> {
5288
validateScanOptions(options);
5389
const binaryPath = await installSecretsBinary();
54-
const { authUrl, authToken } = setupScanEnvironment(binaryPath, auth);
5590
const scanStartTime = Date.now();
5691

5792
if (options.stdin) {
58-
await performStdinScan(binaryPath, authUrl, authToken, scanStartTime);
93+
await performStdinScan(binaryPath, auth, scanStartTime);
5994
} else {
60-
await performPathsScan(binaryPath, options.paths ?? [], authUrl, authToken, scanStartTime);
95+
await performPathsScan(binaryPath, options.paths ?? [], auth, scanStartTime);
6196
}
6297
}
6398

64-
interface ScanEnvironment {
65-
binaryPath: string;
66-
authUrl?: string;
67-
authToken?: string;
68-
}
69-
70-
function setupScanEnvironment(binaryPath: string, auth: ResolvedAuth): ScanEnvironment {
71-
return { binaryPath, authUrl: auth.serverUrl, authToken: auth.token };
72-
}
73-
7499
function validateScanOptions(options: { paths?: string[]; stdin?: boolean }): void {
75100
const hasPaths = (options.paths?.length ?? 0) > 0;
76101
if (!hasPaths && !options.stdin) {
@@ -84,11 +109,10 @@ function validateScanOptions(options: { paths?: string[]; stdin?: boolean }): vo
84109

85110
async function performStdinScan(
86111
binaryPath: string,
87-
authUrl: string | undefined,
88-
authToken: string | undefined,
112+
auth: ResolvedAuth,
89113
scanStartTime: number,
90114
): Promise<void> {
91-
const result = await runScanFromStdin(binaryPath, authUrl, authToken);
115+
const result = await runScanFromStdin(binaryPath, auth);
92116
const scanDurationMs = Date.now() - scanStartTime;
93117

94118
const exitCode = result.exitCode ?? 1;
@@ -102,8 +126,7 @@ async function performStdinScan(
102126
async function performPathsScan(
103127
binaryPath: string,
104128
paths: string[],
105-
authUrl: string | undefined,
106-
authToken: string | undefined,
129+
auth: ResolvedAuth,
107130
scanStartTime: number,
108131
): Promise<void> {
109132
if (paths.length === 0) {
@@ -116,7 +139,7 @@ async function performPathsScan(
116139
}
117140
}
118141

119-
const result = await runScan(binaryPath, paths, authUrl, authToken);
142+
const result = await runSecretsBinary(binaryPath, paths, auth);
120143
const scanDurationMs = Date.now() - scanStartTime;
121144

122145
const exitCode = result.exitCode ?? 1;
@@ -127,41 +150,7 @@ async function performPathsScan(
127150
}
128151
}
129152

130-
async function runScan(
131-
binaryPath: string,
132-
paths: string[],
133-
authUrl: string | undefined,
134-
authToken: string | undefined,
135-
): Promise<SpawnResult> {
136-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
137-
try {
138-
return await Promise.race([
139-
spawnProcess(binaryPath, ['--non-interactive', ...paths], {
140-
stdin: 'pipe',
141-
stdout: 'pipe',
142-
stderr: 'pipe',
143-
env: {
144-
...(authUrl && authToken
145-
? { [BINARY_AUTH_URL_ENV]: authUrl, [BINARY_AUTH_TOKEN_ENV]: authToken }
146-
: {}),
147-
},
148-
}),
149-
new Promise<never>((_resolve, reject) => {
150-
timeoutId = setTimeout(() => {
151-
reject(new Error(`Scan timed out after ${SCAN_TIMEOUT_MS}ms`));
152-
}, SCAN_TIMEOUT_MS);
153-
}),
154-
]);
155-
} finally {
156-
clearTimeout(timeoutId);
157-
}
158-
}
159-
160-
async function runScanFromStdin(
161-
binaryPath: string,
162-
authUrl: string | undefined,
163-
authToken: string | undefined,
164-
): Promise<SpawnResult> {
153+
async function runScanFromStdin(binaryPath: string, auth: ResolvedAuth): Promise<SpawnResult> {
165154
const { writeFileSync, unlinkSync } = await import('node:fs');
166155
const { tmpdir } = await import('node:os');
167156
const pathModule = await import('node:path');
@@ -171,6 +160,7 @@ async function runScanFromStdin(
171160

172161
const tempFile = pathJoin(tmpdir(), `sonar-secrets-scan-${Date.now()}.tmp`);
173162

163+
let killChild: (() => void) | undefined;
174164
let timeoutId: ReturnType<typeof setTimeout> | undefined;
175165
try {
176166
writeFileSync(tempFile, stdinData);
@@ -179,14 +169,14 @@ async function runScanFromStdin(
179169
spawnProcess(binaryPath, [tempFile], {
180170
stdout: 'pipe',
181171
stderr: 'pipe',
182-
env: {
183-
...(authUrl && authToken
184-
? { [BINARY_AUTH_URL_ENV]: authUrl, [BINARY_AUTH_TOKEN_ENV]: authToken }
185-
: {}),
172+
env: { [BINARY_AUTH_URL_ENV]: auth.serverUrl, [BINARY_AUTH_TOKEN_ENV]: auth.token },
173+
onSpawn: (kill) => {
174+
killChild = kill;
186175
},
187176
}),
188177
new Promise<never>((_resolve, reject) => {
189178
timeoutId = setTimeout(() => {
179+
killChild?.();
190180
reject(new Error(`Scan timed out after ${SCAN_TIMEOUT_MS}ms`));
191181
}, SCAN_TIMEOUT_MS);
192182
}),
@@ -270,8 +260,6 @@ function displayScanResults(scanResult: {
270260
});
271261
}
272262

273-
const EXIT_CODE_SECRETS_FOUND = 51;
274-
275263
function handleScanFailure(
276264
result: { exitCode: number | null; stderr: string; stdout: string },
277265
scanDurationMs: number,

src/cli/commands/hook/claude-pre-tool-use.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import { existsSync } from 'node:fs';
2525
import { resolveAuth } from '../../../lib/auth-resolver';
2626
import logger from '../../../lib/logger';
2727
import { readStdinJson } from './stdin';
28-
import { resolveSecretsBinaryPath, scanFiles, EXIT_CODE_SECRETS_FOUND } from './secrets-scan';
28+
import { resolveSecretsBinaryPath } from '../_common/install/secrets';
29+
import { EXIT_CODE_SECRETS_FOUND, runSecretsBinary } from '../analyze/secrets';
2930

3031
interface PreToolUsePayload {
3132
tool_name?: string;
@@ -52,7 +53,8 @@ export async function claudePreToolUse(): Promise<void> {
5253
if (!binaryPath) return; // binary not installed — allow gracefully
5354

5455
try {
55-
const exitCode = await scanFiles(binaryPath, [filePath], auth);
56+
const result = await runSecretsBinary(binaryPath, [filePath], auth);
57+
const exitCode = result.exitCode ?? 1;
5658
if (exitCode === EXIT_CODE_SECRETS_FOUND) {
5759
process.stdout.write(
5860
JSON.stringify({

0 commit comments

Comments
 (0)