Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-code-mode-secret-warning.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions packages/typescript/ai-code-mode/src/create-code-mode-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
36 changes: 36 additions & 0 deletions packages/typescript/ai-code-mode/src/validate-bindings.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> }
}

/**
* 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<ToolLike>): 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.`,
)
}
}
}
}
110 changes: 110 additions & 0 deletions packages/typescript/ai-code-mode/tests/validate-bindings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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()
})
})
Loading