From 7bb1dbd65eb6aeb7941064ce1cbec2b832e2052c Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 3 Aug 2026 16:58:06 -0700 Subject: [PATCH 1/4] fix: make verbose scan reruns and cost failures actionable --- sdk/typescript/README.md | 4 +- sdk/typescript/src/cli.ts | 34 +++++- sdk/typescript/tests-ts/cli.test.ts | 172 ++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 6 deletions(-) diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index abbf9f6d..0307c76a 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -193,6 +193,7 @@ npx @openai/codex-security scans list /path/to/repository npx @openai/codex-security scans list --scan-root /path/outside/repository/results npx @openai/codex-security scans show SCAN_ID npx @openai/codex-security scans rerun SCAN_ID +npx @openai/codex-security scans rerun SCAN_ID --verbose npx @openai/codex-security scans match PREVIOUS_SCAN_ID CURRENT_SCAN_ID npx @openai/codex-security scans match --all npx @openai/codex-security scans compare PREVIOUS_SCAN_ID CURRENT_SCAN_ID @@ -478,7 +479,8 @@ a false positive and explain why. Later scans dismiss a matching finding only when the same reason still applies. `scans rerun SCAN_ID` repeats the original configuration against the current -checkout so a fixed vulnerability can be checked again. +checkout so a fixed vulnerability can be checked again. Add `--verbose` to +inspect the rerun configuration, progress, and cost. `scans match BEFORE_SCAN_ID AFTER_SCAN_ID` links findings with the same root cause; `scans match --all` matches all completed scans of the current repository, diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index c1046056..875cb8c0 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -62,6 +62,7 @@ import { OutputInsideProtectedRootError, PluginPythonUnavailableError, redactedErrorMessage, + ScanCostLimitExceededError, ScanInterruptedError, } from "./errors.js"; import type { SeverityLevel } from "./models.js"; @@ -877,8 +878,14 @@ export async function main( args: z.object({ scanId: z.string().min(1).describe("Saved scan identifier."), }), + options: z.object({ + verbose: z + .boolean() + .default(false) + .describe("Print scan diagnostics to stderr."), + }), output: z.record(z.string(), z.unknown()).optional(), - async run({ args, error: incurError }) { + async run({ args, error: incurError, format, options }) { let scanArguments: ScanArguments; try { const { recipe } = await dependencies.runWorkbench([ @@ -897,7 +904,12 @@ export async function main( exitCode, }); } - const outcome = await runScan(scanArguments, errorOutput, dependencies); + const outcome = await runScan( + { ...scanArguments, verbose: options.verbose }, + errorOutput, + dependencies, + format !== "json" && format !== "jsonl", + ); exitCode = outcome.exitCode; if (outcome.error !== undefined) { return incurError({ @@ -2732,6 +2744,11 @@ async function runScan( codex_version: CODEX_EXECUTABLE_VERSION, codex_sdk_version: CODEX_SDK_VERSION, mode: arguments_.mode, + workers: arguments_.workers, + subagents: arguments_.subagents, + stop_after_no_new: arguments_.stopAfterNoNew, + max_discovery_runs: arguments_.maxDiscoveryRuns, + max_cost_usd: arguments_.maxCostUsd, target: arguments_.paths.length > 0 ? "paths" @@ -2950,15 +2967,22 @@ async function runScan( }; } if (failed) { + const costLimitFailure = + failure instanceof ScanCostLimitExceededError ? failure : undefined; const message = failure instanceof OutputInsideProtectedRootError ? redactedErrorMessage(protectedRootErrorMessage(failure)) : scanFailureMessage(failure, selectedAuthentication); diagnostic("scan.failed", { - classification: isLocalScanFailure(failure) - ? "local" - : classifyConnectionFailure(failure), + classification: + costLimitFailure !== undefined + ? "cost_limit_exceeded" + : isLocalScanFailure(failure) + ? "local" + : classifyConnectionFailure(failure), partial_output: scanDir !== null, + max_cost_usd: costLimitFailure?.maxCostUsd, + estimated_usd: costLimitFailure?.cost.estimatedUsd, }); errorOutput.write(`${message}\n`); if (failure instanceof ScanInterruptedError) { diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index c6326889..29279431 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -120,6 +120,20 @@ describe("CLI", () => { }, }); + const rerunSchema = capture(); + expect( + await main( + ["scans", "rerun", "--schema", "--format", "json"], + rerunSchema.stream, + capture().stream, + dependencies(), + ), + ).toBe(0); + expect(JSON.parse(rerunSchema.text())).toMatchObject({ + args: { properties: { scanId: { type: "string" } } }, + options: { properties: { verbose: { type: "boolean" } } }, + }); + const matchSchema = capture(); expect( await main( @@ -1376,6 +1390,46 @@ describe("CLI", () => { } }); + test("keeps structured scan reruns noninteractive even when stderr is a terminal", async () => { + for (const options of [ + ["--json"], + ["--format", "json"], + ["--format", "jsonl"], + ]) { + const stdout = capture(); + const stderr = capture(true); + let timers = 0; + const deps = dependencies({ + onWorkbench: () => ({ + recipe: { + repository: "/original/repository", + target: { kind: "repository", paths: [] }, + mode: "standard", + config: {}, + }, + }), + }); + deps.setInterval = () => { + timers += 1; + return {} as NodeJS.Timeout; + }; + + expect( + await main( + ["scans", "rerun", "scan-original", ...options], + stdout.stream, + stderr.stream, + deps, + ), + ).toBe(0); + expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); + expect(stderr.text()).toContain("Preparing scan"); + expect(stderr.text()).not.toContain("\u001B"); + expect(stderr.text()).not.toContain("\r"); + expect(timers).toBe(0); + } + }); + test("rejects structured modes before starting interactive Codex commands", async () => { for (const [command, arguments_] of [ ["validate", ["finding"]], @@ -2175,6 +2229,52 @@ describe("CLI", () => { ); }); + test("includes deep scan controls and the cost limit in verbose configuration", async () => { + const stdout = capture(); + const stderr = capture(); + + expect( + await main( + [ + "scan", + ".", + "--mode", + "deep", + "--workers", + "2", + "--subagents", + "0", + "--stop-after-no-new", + "3", + "--max-discovery-runs", + "4", + "--max-cost", + "0.5", + "--verbose", + "--json", + ], + stdout.stream, + stderr.stream, + dependencies(), + ), + ).toBe(0); + expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); + + const configuration = stderr + .text() + .split("\n") + .find((line) => + line.startsWith("codex-security: debug: scan.configuration"), + ); + + expect(configuration).toContain('mode="deep"'); + expect(configuration).toContain("workers=2"); + expect(configuration).toContain("subagents=0"); + expect(configuration).toContain("stop_after_no_new=3"); + expect(configuration).toContain("max_discovery_runs=4"); + expect(configuration).toContain("max_cost_usd=0.5"); + }); + test("includes selected reasoning effort in verbose scan diagnostics", async () => { const stdout = capture(); const stderr = capture(); @@ -2318,6 +2418,39 @@ describe("CLI", () => { expect(stderr.text()).toContain('reasoning_effort="high"'); }); + test("accepts an explicit verbose flag for saved scan reruns", async () => { + const stdout = capture(); + const stderr = capture(); + + expect( + await main( + ["scans", "rerun", "scan-original", "--verbose", "--json"], + stdout.stream, + stderr.stream, + dependencies({ + onWorkbench: () => ({ + recipe: { + repository: "/original/repository", + target: { kind: "repository", paths: [] }, + mode: "standard", + config: { + model: "gpt-original", + model_reasoning_effort: "high", + }, + }, + }), + }), + ), + ).toBe(0); + expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); + expect(stderr.text()).toContain( + "codex-security: debug: scan.configuration", + ); + expect(stderr.text()).toContain("codex-security: debug: scan.started"); + expect(stderr.text()).toContain("codex-security: debug: scan.completed"); + expect(stderr.text()).toContain('model="gpt-original"'); + }); + test("reports selected profile settings for verbose scan reruns", async () => { const stdout = capture(); const stderr = capture(); @@ -3752,6 +3885,45 @@ describe("CLI", () => { ); }); + test("classifies exceeded scan budgets in verbose failure diagnostics", async () => { + const stdout = capture(); + const stderr = capture(); + const cost = fakeResult([], "complete", { + input_tokens: 1_250, + cached_input_tokens: 200, + output_tokens: 30, + }).cost!; + const deps = dependencies(); + deps.createSecurity = () => ({ + run: async (_repository, options) => { + options?.onOutputDirReady?.("/tmp/scan"); + throw new ScanCostLimitExceededError(0.005, cost, "/tmp/scan"); + }, + preflight: async () => fakePreflight(), + close: async () => {}, + }); + + expect( + await main( + ["scan", ".", "--verbose", "--json", "--max-cost", "0.005"], + stdout.stream, + stderr.stream, + deps, + ), + ).toBe(2); + expect(stdout.text()).toBe(""); + + const failure = stderr + .text() + .split("\n") + .find((line) => line.startsWith("codex-security: debug: scan.failed")); + + expect(failure).toContain('classification="cost_limit_exceeded"'); + expect(failure).toContain("partial_output=true"); + expect(failure).toContain("max_cost_usd=0.005"); + expect(failure).toContain("estimated_usd=0.00625"); + }); + test("accepts a scan at its estimated cost limit", async () => { const stdout = capture(); const result = fakeResult([], "complete", { From 9d7434ab64809c6069961173ce8a048f3fb050eb Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 3 Aug 2026 17:08:01 -0700 Subject: [PATCH 2/4] fix: preserve rerun authentication and avoid partial settings --- sdk/typescript/src/cli.ts | 7 +- sdk/typescript/tests-ts/cli.test.ts | 114 ++++++++++++---------------- 2 files changed, 51 insertions(+), 70 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 875cb8c0..92e49684 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -885,7 +885,7 @@ export async function main( .describe("Print scan diagnostics to stderr."), }), output: z.record(z.string(), z.unknown()).optional(), - async run({ args, error: incurError, format, options }) { + async run({ args, error: incurError, options }) { let scanArguments: ScanArguments; try { const { recipe } = await dependencies.runWorkbench([ @@ -908,7 +908,6 @@ export async function main( { ...scanArguments, verbose: options.verbose }, errorOutput, dependencies, - format !== "json" && format !== "jsonl", ); exitCode = outcome.exitCode; if (outcome.error !== undefined) { @@ -2744,10 +2743,6 @@ async function runScan( codex_version: CODEX_EXECUTABLE_VERSION, codex_sdk_version: CODEX_SDK_VERSION, mode: arguments_.mode, - workers: arguments_.workers, - subagents: arguments_.subagents, - stop_after_no_new: arguments_.stopAfterNoNew, - max_discovery_runs: arguments_.maxDiscoveryRuns, max_cost_usd: arguments_.maxCostUsd, target: arguments_.paths.length > 0 diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index 29279431..537d9fef 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -19,6 +19,7 @@ import { describe, expect, test } from "bun:test"; import type { CodexSecurityConfig, JsonObject, + ScanOptions, ScanPreflight, } from "../src/index.js"; import { redactedErrorMessage } from "../src/errors.js"; @@ -1390,46 +1391,6 @@ describe("CLI", () => { } }); - test("keeps structured scan reruns noninteractive even when stderr is a terminal", async () => { - for (const options of [ - ["--json"], - ["--format", "json"], - ["--format", "jsonl"], - ]) { - const stdout = capture(); - const stderr = capture(true); - let timers = 0; - const deps = dependencies({ - onWorkbench: () => ({ - recipe: { - repository: "/original/repository", - target: { kind: "repository", paths: [] }, - mode: "standard", - config: {}, - }, - }), - }); - deps.setInterval = () => { - timers += 1; - return {} as NodeJS.Timeout; - }; - - expect( - await main( - ["scans", "rerun", "scan-original", ...options], - stdout.stream, - stderr.stream, - deps, - ), - ).toBe(0); - expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); - expect(stderr.text()).toContain("Preparing scan"); - expect(stderr.text()).not.toContain("\u001B"); - expect(stderr.text()).not.toContain("\r"); - expect(timers).toBe(0); - } - }); - test("rejects structured modes before starting interactive Codex commands", async () => { for (const [command, arguments_] of [ ["validate", ["finding"]], @@ -2229,30 +2190,13 @@ describe("CLI", () => { ); }); - test("includes deep scan controls and the cost limit in verbose configuration", async () => { + test("includes the configured cost limit in verbose scan configuration", async () => { const stdout = capture(); const stderr = capture(); expect( await main( - [ - "scan", - ".", - "--mode", - "deep", - "--workers", - "2", - "--subagents", - "0", - "--stop-after-no-new", - "3", - "--max-discovery-runs", - "4", - "--max-cost", - "0.5", - "--verbose", - "--json", - ], + ["scan", ".", "--max-cost", "0.5", "--verbose", "--json"], stdout.stream, stderr.stream, dependencies(), @@ -2267,11 +2211,6 @@ describe("CLI", () => { line.startsWith("codex-security: debug: scan.configuration"), ); - expect(configuration).toContain('mode="deep"'); - expect(configuration).toContain("workers=2"); - expect(configuration).toContain("subagents=0"); - expect(configuration).toContain("stop_after_no_new=3"); - expect(configuration).toContain("max_discovery_runs=4"); expect(configuration).toContain("max_cost_usd=0.5"); }); @@ -2451,6 +2390,53 @@ describe("CLI", () => { expect(stderr.text()).toContain('model="gpt-original"'); }); + test("preserves authentication selection for verbose structured reruns", async () => { + for (const format of [["--json"], ["--format", "jsonl"]]) { + const stdout = capture(); + const stderr = capture(true); + let selectedAuthentication: string | undefined; + let prompts = 0; + const deps = dependencies({ + environment: { OPENAI_API_KEY: "synthetic-api-key" }, + onTurn: (_repository, options) => { + selectedAuthentication = (options as ScanOptions).auth; + }, + onWorkbench: () => ({ + recipe: { + repository: "/original/repository", + target: { kind: "repository", paths: [] }, + mode: "standard", + config: {}, + }, + }), + }); + deps.hasStoredChatGPTSignIn = async () => true; + deps.scanAuthenticationPrompt = { + isInteractive: () => true, + select: async ( + _question: string, + choices: readonly { label: string; value: Value }[], + ): Promise => { + prompts += 1; + return choices.find((choice) => choice.value === "chatgpt")!.value; + }, + }; + + expect( + await main( + ["scans", "rerun", "scan-original", "--verbose", ...format], + stdout.stream, + stderr.stream, + deps, + ), + ).toBe(0); + expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); + expect(prompts).toBe(1); + expect(selectedAuthentication).toBe("chatgpt"); + expect(stderr.text()).toContain("codex-security: debug: scan.started"); + } + }); + test("reports selected profile settings for verbose scan reruns", async () => { const stdout = capture(); const stderr = capture(); From 92f2b9d83627c4009d02350525779093a6d33e8d Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 3 Aug 2026 17:30:14 -0700 Subject: [PATCH 3/4] refactor: simplify verbose scan coverage and rerun handling --- sdk/typescript/README.md | 1 - sdk/typescript/src/cli.ts | 7 +- .../tests-ts/cli-authentication.test.ts | 22 ++- sdk/typescript/tests-ts/cli.test.ts | 156 ++---------------- 4 files changed, 31 insertions(+), 155 deletions(-) diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 0307c76a..82886c05 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -192,7 +192,6 @@ npx @openai/codex-security bulk-scan repositories.csv --output-dir /path/outside npx @openai/codex-security scans list /path/to/repository npx @openai/codex-security scans list --scan-root /path/outside/repository/results npx @openai/codex-security scans show SCAN_ID -npx @openai/codex-security scans rerun SCAN_ID npx @openai/codex-security scans rerun SCAN_ID --verbose npx @openai/codex-security scans match PREVIOUS_SCAN_ID CURRENT_SCAN_ID npx @openai/codex-security scans match --all diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 92e49684..2747672f 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -894,6 +894,7 @@ export async function main( args.scanId, ]); scanArguments = scanArgumentsFromRecipe(recipe, args.scanId); + scanArguments.verbose = options.verbose; } catch (error) { const message = redactedErrorMessage(error); errorOutput.write(`codex-security: ${message}\n`); @@ -904,11 +905,7 @@ export async function main( exitCode, }); } - const outcome = await runScan( - { ...scanArguments, verbose: options.verbose }, - errorOutput, - dependencies, - ); + const outcome = await runScan(scanArguments, errorOutput, dependencies); exitCode = outcome.exitCode; if (outcome.error !== undefined) { return incurError({ diff --git a/sdk/typescript/tests-ts/cli-authentication.test.ts b/sdk/typescript/tests-ts/cli-authentication.test.ts index f2e4c00b..287918ea 100644 --- a/sdk/typescript/tests-ts/cli-authentication.test.ts +++ b/sdk/typescript/tests-ts/cli-authentication.test.ts @@ -338,7 +338,15 @@ describe("CLI authentication", () => { }); test("offers the existing interactive prompt when both sign-ins are available", async () => { - for (const selection of ["chatgpt", "api-key"] as const) { + for (const [argv, selection] of [ + [["scan"], "chatgpt"], + [["scan"], "api-key"], + [["scans", "rerun", "scan-original", "--verbose", "--json"], "chatgpt"], + [ + ["scans", "rerun", "scan-original", "--verbose", "--format", "jsonl"], + "chatgpt", + ], + ] as const) { const stderr = capture(true); let selected: ScanOptions["auth"]; let question = ""; @@ -348,6 +356,14 @@ describe("CLI authentication", () => { onTurn: (_repository, options) => { selected = (options as ScanOptions).auth; }, + onWorkbench: () => ({ + recipe: { + repository: "/original/repository", + target: { kind: "repository", paths: [] }, + mode: "standard", + config: {}, + }, + }), }); deps.hasStoredChatGPTSignIn = async () => true; deps.scanAuthenticationPrompt = { @@ -362,9 +378,7 @@ describe("CLI authentication", () => { }, }; - expect(await main(["scan"], capture().stream, stderr.stream, deps)).toBe( - 0, - ); + expect(await main(argv, capture().stream, stderr.stream, deps)).toBe(0); expect(selected).toBe(selection); expect(question).toBe("How would you like to authenticate this scan?"); expect(choices).toEqual([ diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index 537d9fef..b1eb8841 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -2190,30 +2190,6 @@ describe("CLI", () => { ); }); - test("includes the configured cost limit in verbose scan configuration", async () => { - const stdout = capture(); - const stderr = capture(); - - expect( - await main( - ["scan", ".", "--max-cost", "0.5", "--verbose", "--json"], - stdout.stream, - stderr.stream, - dependencies(), - ), - ).toBe(0); - expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); - - const configuration = stderr - .text() - .split("\n") - .find((line) => - line.startsWith("codex-security: debug: scan.configuration"), - ); - - expect(configuration).toContain("max_cost_usd=0.5"); - }); - test("includes selected reasoning effort in verbose scan diagnostics", async () => { const stdout = capture(); const stderr = capture(); @@ -2329,38 +2305,6 @@ describe("CLI", () => { const stdout = capture(); const stderr = capture(); - expect( - await main( - ["scans", "rerun", "scan-original"], - stdout.stream, - stderr.stream, - dependencies({ - environment: { CODEX_SECURITY_LOG_LEVEL: "debug" }, - onWorkbench: () => ({ - recipe: { - repository: "/original/repository", - target: { kind: "repository", paths: [] }, - mode: "standard", - config: { - model: "gpt-original", - model_reasoning_effort: "high", - }, - }, - }), - }), - ), - ).toBe(0); - expect(stderr.text()).toContain( - "codex-security: debug: scan.configuration", - ); - expect(stderr.text()).toContain('model="gpt-original"'); - expect(stderr.text()).toContain('reasoning_effort="high"'); - }); - - test("accepts an explicit verbose flag for saved scan reruns", async () => { - const stdout = capture(); - const stderr = capture(); - expect( await main( ["scans", "rerun", "scan-original", "--verbose", "--json"], @@ -2388,53 +2332,7 @@ describe("CLI", () => { expect(stderr.text()).toContain("codex-security: debug: scan.started"); expect(stderr.text()).toContain("codex-security: debug: scan.completed"); expect(stderr.text()).toContain('model="gpt-original"'); - }); - - test("preserves authentication selection for verbose structured reruns", async () => { - for (const format of [["--json"], ["--format", "jsonl"]]) { - const stdout = capture(); - const stderr = capture(true); - let selectedAuthentication: string | undefined; - let prompts = 0; - const deps = dependencies({ - environment: { OPENAI_API_KEY: "synthetic-api-key" }, - onTurn: (_repository, options) => { - selectedAuthentication = (options as ScanOptions).auth; - }, - onWorkbench: () => ({ - recipe: { - repository: "/original/repository", - target: { kind: "repository", paths: [] }, - mode: "standard", - config: {}, - }, - }), - }); - deps.hasStoredChatGPTSignIn = async () => true; - deps.scanAuthenticationPrompt = { - isInteractive: () => true, - select: async ( - _question: string, - choices: readonly { label: string; value: Value }[], - ): Promise => { - prompts += 1; - return choices.find((choice) => choice.value === "chatgpt")!.value; - }, - }; - - expect( - await main( - ["scans", "rerun", "scan-original", "--verbose", ...format], - stdout.stream, - stderr.stream, - deps, - ), - ).toBe(0); - expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); - expect(prompts).toBe(1); - expect(selectedAuthentication).toBe("chatgpt"); - expect(stderr.text()).toContain("codex-security: debug: scan.started"); - } + expect(stderr.text()).toContain('reasoning_effort="high"'); }); test("reports selected profile settings for verbose scan reruns", async () => { @@ -3844,7 +3742,7 @@ describe("CLI", () => { expect(stderr.text()).toContain("cache_write_input_tokens=200"); }); - test("reports a scan stopped when its live cost exceeds the limit", async () => { + test("reports and classifies a scan stopped when its live cost exceeds the limit", async () => { const stdout = capture(); const stderr = capture(); const cost = fakeResult([], "complete", { @@ -3855,11 +3753,12 @@ describe("CLI", () => { expect( await main( - ["scan", ".", "--json", "--max-cost", "0.005"], + ["scan", ".", "--verbose", "--json", "--max-cost", "0.005"], stdout.stream, stderr.stream, dependencies({ - onTurn: () => { + onTurn: (_repository, options) => { + (options as ScanOptions).onOutputDirReady?.("/tmp/scan"); throw new ScanCostLimitExceededError(0.005, cost, "/tmp/scan"); }, }), @@ -3869,45 +3768,12 @@ describe("CLI", () => { expect(stderr.text()).toContain( "Scan stopped: estimated cost $0.00625 exceeded the $0.005 limit; partial output remains at /tmp/scan.", ); - }); - - test("classifies exceeded scan budgets in verbose failure diagnostics", async () => { - const stdout = capture(); - const stderr = capture(); - const cost = fakeResult([], "complete", { - input_tokens: 1_250, - cached_input_tokens: 200, - output_tokens: 30, - }).cost!; - const deps = dependencies(); - deps.createSecurity = () => ({ - run: async (_repository, options) => { - options?.onOutputDirReady?.("/tmp/scan"); - throw new ScanCostLimitExceededError(0.005, cost, "/tmp/scan"); - }, - preflight: async () => fakePreflight(), - close: async () => {}, - }); - - expect( - await main( - ["scan", ".", "--verbose", "--json", "--max-cost", "0.005"], - stdout.stream, - stderr.stream, - deps, - ), - ).toBe(2); - expect(stdout.text()).toBe(""); - - const failure = stderr - .text() - .split("\n") - .find((line) => line.startsWith("codex-security: debug: scan.failed")); - - expect(failure).toContain('classification="cost_limit_exceeded"'); - expect(failure).toContain("partial_output=true"); - expect(failure).toContain("max_cost_usd=0.005"); - expect(failure).toContain("estimated_usd=0.00625"); + expect(stderr.text()).toMatch( + /scan\.configuration[^\n]*max_cost_usd=0\.005/u, + ); + expect(stderr.text()).toContain( + 'scan.failed classification="cost_limit_exceeded" partial_output=true max_cost_usd=0.005 estimated_usd=0.00625', + ); }); test("accepts a scan at its estimated cost limit", async () => { From fa26b7d572fa88793247854c2e4fe7a65a6570ce Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 3 Aug 2026 17:35:19 -0700 Subject: [PATCH 4/4] docs: keep existing verbose scan documentation unchanged --- sdk/typescript/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 82886c05..abbf9f6d 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -192,7 +192,7 @@ npx @openai/codex-security bulk-scan repositories.csv --output-dir /path/outside npx @openai/codex-security scans list /path/to/repository npx @openai/codex-security scans list --scan-root /path/outside/repository/results npx @openai/codex-security scans show SCAN_ID -npx @openai/codex-security scans rerun SCAN_ID --verbose +npx @openai/codex-security scans rerun SCAN_ID npx @openai/codex-security scans match PREVIOUS_SCAN_ID CURRENT_SCAN_ID npx @openai/codex-security scans match --all npx @openai/codex-security scans compare PREVIOUS_SCAN_ID CURRENT_SCAN_ID @@ -478,8 +478,7 @@ a false positive and explain why. Later scans dismiss a matching finding only when the same reason still applies. `scans rerun SCAN_ID` repeats the original configuration against the current -checkout so a fixed vulnerability can be checked again. Add `--verbose` to -inspect the rerun configuration, progress, and cost. +checkout so a fixed vulnerability can be checked again. `scans match BEFORE_SCAN_ID AFTER_SCAN_ID` links findings with the same root cause; `scans match --all` matches all completed scans of the current repository,