JuliusBrussee/caveman · error

cave_claude_tool_schema_unsupported

cave_claude_tool_schema_unsupported

Error message

cave_claude_tool_schema_unsupported:${item.name}

What it means

Each tool's JSON-Schema input must be convertible by z.fromJSONSchema into a z.ZodObject. Schemas that convert to another Zod type (e.g. a bare array, string, or anyOf at the top level) are rejected, with the tool name appended to the message.

Solutions

  1. Wrap the schema in an object type: {"type":"object","properties":{...},"required":[...] }
  2. Move list inputs under a named property (e.g. {"items": {...}} becomes {"type":"object","properties":{"items":...}})
  3. Simplify the root schema so z.fromJSONSchema yields a ZodObject

Example fix

// before
input: { type: "array", items: { type: "string" } }
// after
input: { type: "object", properties: { values: { type: "array", items: { type: "string" } } }, required: ["values"] }
Defensive patterns

Strategy: validation

Validate before calling

for (const t of definition.tools) {
  const conv = z.fromJSONSchema(t.input);
  if (!(conv instanceof z.ZodObject)) throw new Error(`tool ${t.name} input schema must be a top-level object`);
}

Type guard

const isObjectSchema = (t) => t.input?.type === "object";

Try / catch

try { await runClaudeAgent(opts); } catch (e) { if (String(e.message).startsWith("cave_claude_tool_schema_unsupported:")) { /* fix that tool's schema and retry */ } throw e; }

Prevention

When it happens

Trigger: Declaring a tool whose input JSON schema's top level is not an object — e.g. {"type":"array"}, {"type":"string"}, or an anyOf/oneOf at the root.

Common situations: Hand-writing tool schemas copied from OpenAI-style function definitions where parameters wrap differently; generating schemas from a serializer that emits non-object roots.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/e90b1dc8b592853c. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/claude-runtime.ts:151

    // lane can only ever be an unlocked run: a missing gateway degrades to
    // observe-only rather than blocking a first response.
    const { useGateway } = await resolveCaveRoute(gatewayURL, options, false);
    const instructions = assembleSystemPrompt(definition, lowered);
    const runID = crypto.randomUUID();
    const sessionID = `claude-${definition.id}-${runID}`;
    const prefixSHA256 = sha256(stableStringify({
      instructions,
      tools: definition.tools.map((item) => ({
        name: item.name,
        description: item.description,
        input: item.input,
      })),
    }));
    const toolNames = new Set<string>();
    const mcpTools = definition.tools.map((item) => {
      const converted = z.fromJSONSchema(item.input as Record<string, unknown>);
      if (!(converted instanceof z.ZodObject)) {
        throw new Error(`cave_claude_tool_schema_unsupported:${item.name}`);
      }
      const execute = createHarnessToolExecutor({
        definition,
        tool: item,
        sandbox,
        ...(options.sandboxProfile === undefined ? {} : { sandboxProfile: options.sandboxProfile }),
        ...(options.engineBin === undefined ? {} : { engineBin: options.engineBin }),
      });
      const wireName = `mcp__caveman_agent__${item.name}`;
      toolNames.add(wireName);
      return claudeTool(
        item.name,
        item.description,
        converted.shape,
        async (args) => {
          const value = await execute(args, controller.signal);
          if (!isRecord(value) || !Array.isArray(value.content)) {
            throw new Error(`cave_claude_tool_result_invalid:${item.name}`);

View on GitHub (pinned to 3ee70a1026)