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
- Change the offending tool's definition to effect: "read" and result: "inline"
- Remove the unsupported tool from definition.tools when targeting the Claude runtime
- 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
- Keep a separate read-only tool set for the Claude runtime
- Assert tool contracts in tests before running agents
- Default new tools to effect:"read" unless they truly need write
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
- cave_claude_header_invalid
- cave_claude_max_budget_invalid
- cave_claude_max_turns_invalid
- cave_claude_tool_result_invalid
- cave_claude_tool_schema_unsupported
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)