Skip to main content
interlace
Plugin: vercel-ai-securityRules

require-max-steps

This rule identifies tool-calling AI SDK calls whose step limit does not bound the loop: a hasToolCall-only stopWhen, Infinity, or a limit read from the request

Prevents unbounded tool calling loops in multi-step agents.

📊 Rule Details

PropertyValue
Typeproblem
Severity🟡 MEDIUM
OWASP LLMLLM10: Unbounded Consumption
CWECWE-834: Excessive Iteration
CVSS6.5
Config Defaultoff (recommended), error (strict)

🔍 What This Rule Detects

generateText / streamText with tools run one step unless told otherwise: AI SDK v4 defaults to maxSteps: 1, and v5+ defaults to stopWhen: stepCountIs(1). A call with tools and no step option therefore cannot loop, and this rule does not report it.

The rule reports a step setting that is present but does not bound the loop:

  • stopWhen: hasToolCall('done') — alone, or an array of only hasToolCall(...) conditions. It stops only if the model ever calls that tool.
  • maxSteps: Infinity.
  • maxSteps, stopWhen, or the count passed to stepCountIs(...) / isStepCount(...), read from the request (await req.json(), req.body, searchParams.get(...)) — the caller chooses the budget.

Changed in the 2026-10-10 FP/FN audit. Earlier versions reported every tool call without maxSteps/stopWhen — the SDK's single-step default, the safest configuration there is — and accepted any stopWhen, including the unbounded ones above.

❌ Incorrect Code

// Stops only if the model calls finalAnswer
await generateText({ model, tools, stopWhen: hasToolCall('finalAnswer'), prompt: 'research' });

// The caller chooses the step budget
export async function POST(req: Request) {
  const body = await req.json();
  return streamText({ model, tools, maxSteps: body.maxSteps, prompt: 'x' });
}

export async function PUT(req: Request) {
  const { steps } = await req.json();
  return streamText({ model, tools, stopWhen: stepCountIs(steps) });
}

✅ Correct Code

// No step option: the SDK runs a single step
await generateText({ model, tools: { getWeather }, prompt: 'Weather in Paris?' });

// AI SDK v5+
await streamText({ model, messages, tools: { getWeather }, stopWhen: stepCountIs(5) });

// hasToolCall bounded by a step count
await generateText({ model, tools, stopWhen: [hasToolCall('finalize'), stepCountIs(20)] });

// AI SDK v4
await generateText({ model, tools, maxSteps: 5 });

⚙️ Options

OptionTypeDefaultDescription
suggestedMaxStepsnumber5Default max steps limit to suggest

🛡️ Why This Matters

Unbounded tool loops can cause:

  • Infinite loops - AI keeps calling tools forever
  • Cost explosion - Each tool call may trigger additional API calls
  • Resource exhaustion - Downstream services overwhelmed
  • Data corruption - Repeated mutations without checks

🔄 Changes in the 2026-10-11 zero-deferral pass

  • A step count or stop condition read from the request through further same-file hops (const n = Number(body.steps); stopWhen: stepCountIs(n)) is reported.

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Custom Stop Conditions

Why: A stop condition the rule cannot read (stopWhen: myCondition) may or may not bound the loop; the rule does not guess.

Mitigation: Combine custom conditions with stepCountIs(n) in an array.

Options from Variable, Wrappers, ToolLoopAgent

Why: Options stored in variables, wrappers that add tools internally, and new ToolLoopAgent({ ... }) (which defaults to 20 steps) are not analyzed.

📚 References

Use this rule

npm install -D oxlint-plugin-vercel-ai-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-vercel-ai-security"],  "rules": {    "vercel-ai-security/require-max-steps": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-vercel-ai-security'],    rules: {      'vercel-ai-security/require-max-steps': 'error',    },  },});

oxlint-plugin-vercel-ai-security re-exports eslint-plugin-vercel-ai-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.