JuliusBrussee/caveman · error · CavemanRunError
cave_output_schema_invalid_json
cave_output_schema_invalid_json
Error message
the SDK output was not valid JSON
What it means
When the agent definition declares `output.schema`, the executor must parse the SDK's final text as JSON (`result.structured_output` is preferred, otherwise `JSON.parse(text)`). If both are unavailable or the text is not valid JSON, the declared structured-output contract cannot be checked, so the run fails as a CavemanRunError carrying the usage receipt. The library throws rather than returning raw text because the caller explicitly asked for schema-validated structured output.
Solutions
- Add an explicit instruction in the agent prompt to reply with a single raw JSON value matching the schema (no prose, no code fences).
- Upgrade/verify the Claude Agent SDK version so `result.structured_output` is populated, which bypasses text parsing entirely.
- Inspect `result.result` text on failure — if it's fenced, either strip fences in a pre-step or rely on structured_output.
- Raise `output.maxTokens` if truncation is cutting the JSON short.
Example fix
// before
prompt: "Summarize the findings."
// after
prompt: "Summarize the findings. Reply with ONLY a JSON object: {\"summary\": string, \"items\": string[]} — no markdown, no commentary." Defensive patterns
Strategy: try-catch
Validate before calling
// Check the prompt demands JSON before declaring a schema-driven output.
if (definition.output?.schema && !/JSON/i.test(prompt)) {
throw new Error("schema declared but prompt never instructs JSON-only output");
} Type guard
function looksLikeJson(s: string): boolean {
const t = s.trim().replace(/^```(?:json)?|```$/g, "").trim();
return t.startsWith("{") || t.startsWith("[");
} Try / catch
try {
const r = await runClaudeAgent(opts);
} catch (e) {
if (e instanceof CavemanRunError && e.code === "cave_output_schema_invalid_json") {
console.warn("raw SDK text:", e.receipt); // inspect text, then retry with stricter JSON instruction
}
throw e;
} Prevention
- Always include an explicit 'reply with only raw JSON, no markdown' instruction when declaring output.schema.
- Use an SDK version that populates `structured_output` so text parsing is bypassed.
- Keep output.maxTokens comfortably above the largest expected JSON so truncation can't cut it.
- Test schema agents with a small fixture run before production.
When it happens
Trigger: Declaring `definition.output.schema` while the Claude SDK run ends with free-form text instead of JSON — e.g. the model wrapped JSON in prose/markdown fences with no `structured_output`, the prompt never instructed JSON output, or the final result text was truncated.
Common situations: Porting an agent that previously returned plain text and adding an output schema without adjusting the prompt; SDK versions that don't populate `structured_output`; long outputs truncated by maxTokens cutting the JSON mid-string; the model answering a clarification instead of JSON.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- cave_output_schema_invalid_json
- cave_output_schema_mismatch
- cave_output_schema_mismatch
- cave response must be a JSON object
- contents
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/b7671b9712bf7396.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/claude-runtime.ts:341
if (result === undefined || result.subtype !== "success" || result.is_error) {
throw carry(
`cave_claude_terminal_${result?.subtype ?? "missing"}`,
`the Claude Agent SDK ended with ${result?.subtype ?? "no result"}`,
);
}
if (assistantModel !== undefined && normalizeClaudeModel(assistantModel) !== selected) {
throw carry(
"cave_provider_model_identity_mismatch",
`expected ${selected}, the SDK answered as ${assistantModel}`,
);
}
const text = result.result;
if (definition.output?.schema !== undefined) {
let parsed: unknown;
try {
parsed = result.structured_output ?? JSON.parse(text);
} catch {
throw carry("cave_output_schema_invalid_json", "the SDK output was not valid JSON");
}
if (!Value.Check(definition.output.schema, parsed)) {
throw carry("cave_output_schema_mismatch", "the SDK output did not match the declared schema");
}
}
// Past the success gate, usage must be accountable. A success whose usage
// validateProviderUsage rejected is a real evidence failure — surface it
// carrying the receipt, never mask it.
if (usage === undefined) {
throw carry(
usageError?.message ?? "cave_claude_terminal_missing",
"the SDK reported no accountable usage on a successful result",
);
}
if (definition.output !== undefined && usage.outputTokens > definition.output.maxTokens) {
// The SDK already ran and spent by the time its aggregate usage is known,
// so this post-hoc budget breach carries the receipt of what was spent
// rather than throwing it away.View on GitHub (pinned to 3ee70a1026)