From ed2ae13bd9b3c7e6644b11b109b8fc67932819cb Mon Sep 17 00:00:00 2001 From: Maxwell Calkin Date: Sun, 8 Mar 2026 17:52:46 -0400 Subject: [PATCH] fix: validate maxTurns to prevent NaN from reaching SDK parseInt() returns NaN for non-numeric strings like "abc", which was silently passed to the SDK's maxTurns option causing unpredictable behavior. Now validates the parsed value and throws a clear error for non-numeric or negative values. Co-Authored-By: Claude Opus 4.6 --- src/parse-sdk-options.ts | 19 ++++++++++++- test/parse-sdk-options.test.ts | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/parse-sdk-options.ts b/src/parse-sdk-options.ts index 35df281..8877d9f 100644 --- a/src/parse-sdk-options.ts +++ b/src/parse-sdk-options.ts @@ -233,11 +233,28 @@ export function parseSdkOptions(options: ClaudeOptions): ParsedSdkOptions { }; } + // Parse and validate maxTurns + let maxTurns: number | undefined; + if (options.maxTurns) { + const parsed = parseInt(options.maxTurns, 10); + if (Number.isNaN(parsed)) { + throw new Error( + `Invalid max_turns value: "${options.maxTurns}". Must be a valid integer.`, + ); + } + if (parsed < 0) { + throw new Error( + `Invalid max_turns value: "${options.maxTurns}". Must be a non-negative integer.`, + ); + } + maxTurns = parsed; + } + // Build SDK options - use merged tools from both direct options and claudeArgs const sdkOptions: SdkOptions = { // Direct options from ClaudeOptions inputs model: options.model, - maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : undefined, + maxTurns, allowedTools: mergedAllowedTools.length > 0 ? mergedAllowedTools : undefined, disallowedTools: diff --git a/test/parse-sdk-options.test.ts b/test/parse-sdk-options.test.ts index 9c1095c..b7179d2 100644 --- a/test/parse-sdk-options.test.ts +++ b/test/parse-sdk-options.test.ts @@ -298,6 +298,56 @@ describe("parseSdkOptions", () => { }); }); + describe("maxTurns validation", () => { + test("should parse valid maxTurns value", () => { + const options: ClaudeOptions = { + maxTurns: "5", + }; + + const result = parseSdkOptions(options); + + expect(result.sdkOptions.maxTurns).toBe(5); + }); + + test("should return undefined maxTurns when not provided", () => { + const options: ClaudeOptions = {}; + + const result = parseSdkOptions(options); + + expect(result.sdkOptions.maxTurns).toBeUndefined(); + }); + + test("should throw error for non-numeric maxTurns", () => { + const options: ClaudeOptions = { + maxTurns: "abc", + }; + + expect(() => parseSdkOptions(options)).toThrow( + 'Invalid max_turns value: "abc". Must be a valid integer.', + ); + }); + + test("should throw error for negative maxTurns", () => { + const options: ClaudeOptions = { + maxTurns: "-5", + }; + + expect(() => parseSdkOptions(options)).toThrow( + 'Invalid max_turns value: "-5". Must be a non-negative integer.', + ); + }); + + test("should parse maxTurns with leading/trailing text as NaN and throw", () => { + const options: ClaudeOptions = { + maxTurns: "ten", + }; + + expect(() => parseSdkOptions(options)).toThrow( + 'Invalid max_turns value: "ten". Must be a valid integer.', + ); + }); + }); + describe("other extraArgs passthrough", () => { test("should pass through json-schema in extraArgs", () => { const options: ClaudeOptions = {