JuliusBrussee/caveman · error

cave_claude_tool_contract_unsupported

cave_claude_tool_contract_unsupported

Error message

cave_claude_tool_contract_unsupported:${unsupportedTool.name}

What it means

runClaudeAgentWithOptions only supports tools declared with effect "read" and inline results. Any tool in the AgentDefinition violating that contract is rejected before the Claude run starts, and the offending tool's name is appended to the message. This guarantees the Claude runtime bridge never has to model side-effecting or non-inline tool results.

Solutions

  1. Change the offending tool's definition to effect: "read" and result: "inline"
  2. Remove the unsupported tool from definition.tools when targeting the Claude runtime
  3. If the tool must write, run it in a different runtime that supports the full tool contract

Example fix

// before
tools: [{ name: "save_note", effect: "write", result: "inline", ... }]
// after
tools: [{ name: "read_note", effect: "read", result: "inline", ... }]
Defensive patterns

Strategy: validation

Validate before calling

const bad = definition.tools.find(t => t.effect !== "read" || t.result !== "inline");
if (bad) throw new Error(`tool ${bad.name} must be read/inline for the Claude runtime`);

Type guard

const isClaudeCompatible = (t) => t.effect === "read" && t.result === "inline";

Try / catch

try { await runClaudeAgent(opts); } catch (e) { if (String(e.message).startsWith("cave_claude_tool_contract_unsupported:")) { /* drop or re-scope tool */ } else throw e; }

Prevention

When it happens

Trigger: Passing an AgentDefinition whose tools array contains a tool with effect !== "read" or result !== "inline" — e.g. a write-effect filesystem tool or a tool returning a resource/file reference.

Common situations: Reusing a general-purpose agent definition (with write/bash tools) against the Claude runtime; adding a new tool with the default effect without realizing the Claude bridge only accepts read/inline tools.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

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

  if (options.lockedBuild !== undefined && options.candidatePlan !== undefined) {
    throw new Error("cave_execution_authorization_ambiguous");
  }
  if (options.candidatePlan !== undefined) {
    throw new Error("cave_claude_candidate_execution_unavailable");
  }
  if (options.lockedBuild !== undefined) {
    throw new Error("cave_claude_locked_execution_unavailable");
  }
  if (definition.memory !== undefined) {
    throw new Error("cave_claude_memory_bridge_unavailable");
  }
  if (definition.tools.some((item) => item.runtime?.kind === "subagent")) {
    throw new Error("cave_claude_subagent_bridge_unavailable");
  }
  const unsupportedTool = definition.tools.find((item) =>
    item.effect !== "read" || item.result !== "inline");
  if (unsupportedTool !== undefined) {
    throw new Error(`cave_claude_tool_contract_unsupported:${unsupportedTool.name}`);
  }
  if (options.maxTurns !== undefined &&
      (!Number.isSafeInteger(options.maxTurns) || options.maxTurns <= 0 || options.maxTurns > 1_000)) {
    throw new Error("cave_claude_max_turns_invalid");
  }
  if (options.maxBudgetUsd !== undefined &&
      (!Number.isFinite(options.maxBudgetUsd) || options.maxBudgetUsd <= 0)) {
    throw new Error("cave_claude_max_budget_invalid");
  }
  const rootDir = resolve(options.rootDir ?? process.cwd());
  const lowered = await lowerAgentContext(definition, { rootDir, input });
  const bill = contextBill(lowered.ir);
  const selected = resolveClaudeModel(definition, rootDir);
  const reasoningOptions = claudeReasoningOptions(
    selected,
    definition.reasoning,
    definition.output?.maxTokens,
  );

View on GitHub (pinned to 3ee70a1026)