From 391207bbae6d86aef30a035c60b8163d47f9b071 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 8 Apr 2026 13:26:49 +0200 Subject: [PATCH 1/3] fix(ai-code-mode): warn when tool parameters look like secrets Code Mode executes LLM-generated code in a sandbox. If a tool's input schema includes parameters like apiKey, token, or password, the LLM-generated code can access those values and potentially exfiltrate them via tool calls. Add warnIfBindingsExposeSecrets() that scans tool input schemas for secret-like parameter names and emits console.warn during development. --- .../ai-code-mode/src/create-code-mode-tool.ts | 3 + .../ai-code-mode/src/validate-bindings.ts | 36 ++++++ .../tests/validate-bindings.test.ts | 112 ++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 packages/typescript/ai-code-mode/src/validate-bindings.ts create mode 100644 packages/typescript/ai-code-mode/tests/validate-bindings.test.ts diff --git a/packages/typescript/ai-code-mode/src/create-code-mode-tool.ts b/packages/typescript/ai-code-mode/src/create-code-mode-tool.ts index 52180d6a..f20959f7 100644 --- a/packages/typescript/ai-code-mode/src/create-code-mode-tool.ts +++ b/packages/typescript/ai-code-mode/src/create-code-mode-tool.ts @@ -5,6 +5,7 @@ import { toolsToBindings, } from './bindings/tool-to-binding' import { stripTypeScript } from './strip-typescript' +import { warnIfBindingsExposeSecrets } from './validate-bindings' import type { ServerTool, ToolExecutionContext } from '@tanstack/ai' import type { CodeModeTool, @@ -103,6 +104,8 @@ export function createCodeModeTool( // Transform tools to bindings with external_ prefix (static bindings) const staticBindings = toolsToBindings(tools, 'external_') + warnIfBindingsExposeSecrets(Object.values(staticBindings)) + // Create the tool definition const definition = toolDefinition({ name: 'execute_typescript' as const, diff --git a/packages/typescript/ai-code-mode/src/validate-bindings.ts b/packages/typescript/ai-code-mode/src/validate-bindings.ts new file mode 100644 index 00000000..fb05d507 --- /dev/null +++ b/packages/typescript/ai-code-mode/src/validate-bindings.ts @@ -0,0 +1,36 @@ +/** + * Patterns that indicate a parameter might carry a secret value. + * Case-insensitive matching against JSON Schema property names. + */ +const SECRET_PATTERNS = + /^(api[_-]?key|secret|token|password|credential|auth[_-]?token|access[_-]?key|private[_-]?key)$/i + +interface ToolLike { + name: string + inputSchema?: { type?: string; properties?: Record } +} + +/** + * Scan tool input schemas for parameter names that look like secrets. + * Emits console.warn for each match so developers notice during development. + * + * This is a best-effort heuristic, not a security boundary. + */ +export function warnIfBindingsExposeSecrets(tools: Array): void { + for (const tool of tools) { + const properties = tool.inputSchema?.properties + if (!properties) continue + + for (const paramName of Object.keys(properties)) { + if (SECRET_PATTERNS.test(paramName)) { + console.warn( + `[TanStack AI Code Mode] Tool "${tool.name}" has parameter "${paramName}" ` + + `that looks like a secret. Code Mode executes LLM-generated code — any ` + + `value passed through this parameter is accessible to generated code and ` + + `could be exfiltrated. Keep secrets in your server-side tool implementation ` + + `instead of passing them as tool parameters.`, + ) + } + } + } +} diff --git a/packages/typescript/ai-code-mode/tests/validate-bindings.test.ts b/packages/typescript/ai-code-mode/tests/validate-bindings.test.ts new file mode 100644 index 00000000..b139b81a --- /dev/null +++ b/packages/typescript/ai-code-mode/tests/validate-bindings.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it, vi } from 'vitest' +import { warnIfBindingsExposeSecrets } from '../src/validate-bindings' + +describe('warnIfBindingsExposeSecrets', () => { + it('should warn when input schema has secret-like parameter names', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + warnIfBindingsExposeSecrets([ + { + name: 'callApi', + description: 'Call an API', + inputSchema: { + type: 'object', + properties: { + url: { type: 'string' }, + apiKey: { type: 'string' }, + }, + }, + }, + ]) + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('apiKey'), + ) + warnSpy.mockRestore() + }) + + it('should not warn for safe parameter names', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + warnIfBindingsExposeSecrets([ + { + name: 'search', + description: 'Search items', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string' }, + limit: { type: 'number' }, + }, + }, + }, + ]) + + expect(warnSpy).not.toHaveBeenCalled() + warnSpy.mockRestore() + }) + + it('should detect multiple secret patterns', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + warnIfBindingsExposeSecrets([ + { + name: 'auth', + description: 'Auth tool', + inputSchema: { + type: 'object', + properties: { + token: { type: 'string' }, + password: { type: 'string' }, + username: { type: 'string' }, + }, + }, + }, + ]) + + const calls = warnSpy.mock.calls.map((c) => c[0]) + expect(calls.some((c: string) => c.includes('token'))).toBe(true) + expect(calls.some((c: string) => c.includes('password'))).toBe(true) + // username should NOT trigger a warning + expect(calls.some((c: string) => c.includes('username'))).toBe(false) + warnSpy.mockRestore() + }) + + it('should handle tools with no inputSchema', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + warnIfBindingsExposeSecrets([ + { + name: 'simple', + description: 'Simple tool', + }, + ]) + + expect(warnSpy).not.toHaveBeenCalled() + warnSpy.mockRestore() + }) + + it('should detect api_key and api-key variations', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + warnIfBindingsExposeSecrets([ + { + name: 'tool1', + inputSchema: { + type: 'object', + properties: { api_key: { type: 'string' } }, + }, + }, + { + name: 'tool2', + inputSchema: { + type: 'object', + properties: { 'api-key': { type: 'string' } }, + }, + }, + ]) + + expect(warnSpy).toHaveBeenCalledTimes(2) + warnSpy.mockRestore() + }) +}) From 8bd10a8c40a3be20a50d1d41a18d97bc7a42a2d4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:38:43 +0000 Subject: [PATCH 2/3] ci: apply automated fixes --- .../typescript/ai-code-mode/tests/validate-bindings.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/typescript/ai-code-mode/tests/validate-bindings.test.ts b/packages/typescript/ai-code-mode/tests/validate-bindings.test.ts index b139b81a..aec4ba6e 100644 --- a/packages/typescript/ai-code-mode/tests/validate-bindings.test.ts +++ b/packages/typescript/ai-code-mode/tests/validate-bindings.test.ts @@ -19,9 +19,7 @@ describe('warnIfBindingsExposeSecrets', () => { }, ]) - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('apiKey'), - ) + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('apiKey')) warnSpy.mockRestore() }) From 7a12c6d528f914fe5bbff0259e44242512e0da30 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 8 Apr 2026 13:39:35 +0200 Subject: [PATCH 3/3] changeset: code-mode secret warning --- .changeset/fix-code-mode-secret-warning.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/fix-code-mode-secret-warning.md diff --git a/.changeset/fix-code-mode-secret-warning.md b/.changeset/fix-code-mode-secret-warning.md new file mode 100644 index 00000000..08dbdd39 --- /dev/null +++ b/.changeset/fix-code-mode-secret-warning.md @@ -0,0 +1,7 @@ +--- +'@tanstack/ai-code-mode': patch +--- + +fix(ai-code-mode): warn when tool parameters look like secrets + +Add `warnIfBindingsExposeSecrets()` that scans tool input schemas for secret-like parameter names (`apiKey`, `token`, `password`, etc.) and emits `console.warn` during development. Code Mode executes LLM-generated code — any secrets passed through tool parameters are accessible to generated code.