JuliusBrussee/caveman · error · Error

caveman agent: invalid .caveman/provider.json

Error message

caveman agent: invalid .caveman/provider.json

What it means

localModel() reads <rootDir>/.caveman/provider.json to find a locally configured model. ENOENT (file absent) is fine and returns undefined, but any other failure — JSON.parse syntax errors, EACCES, EISDIR — rethrows as "caveman agent: invalid .caveman/provider.json". A present-but-corrupt provider file is treated as a hard configuration defect, not silently ignored.

Source

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

    : process.env.CAVE_MODEL ?? localModel(rootDir) ??
      (process.env.ANTHROPIC_API_KEY ? "anthropic/claude-haiku-4-5" : undefined);
  if (configured === undefined) throw new Error("cave_claude_model_required");
  const separator = configured.indexOf("/");
  if (separator < 1 || configured.slice(0, separator) !== "anthropic") {
    throw new Error("cave_claude_provider_unsupported");
  }
  return normalizeClaudeModel(configured.slice(separator + 1));
}

function localModel(rootDir: string): string | undefined {
  try {
    const parsed = JSON.parse(readFileSync(resolve(rootDir, ".caveman/provider.json"), "utf8")) as {
      model?: unknown;
    };
    return typeof parsed.model === "string" ? parsed.model : undefined;
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
    throw new Error("caveman agent: invalid .caveman/provider.json");
  }
}

function normalizeClaudeModel(model: string): string {
  return model.startsWith("anthropic/") ? model.slice("anthropic/".length) : model;
}

function claudeEffort(reasoning: AgentDefinition["reasoning"]): "low" | "medium" | "high" {
  if (reasoning === "medium") return "medium";
  if (reasoning === "high") return "high";
  return "low";
}

function claudeReasoningOptions(
  model: string,
  reasoning: AgentDefinition["reasoning"],
  outputMaxTokens: number | undefined,
): Pick<ClaudeSDKOptions, "thinking" | "effort"> {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Fix the JSON: cat .caveman/provider.json | jq . to find the syntax error, then correct it.
  2. Ensure the file is a plain object with a string model: { "model": "anthropic/claude-sonnet-4-5" }.
  3. If you don't want local provider config, delete the file entirely — absence is handled gracefully.
  4. Make writers atomic (write temp + rename) so crashes can't leave partial files.

Example fix

// before: .caveman/provider.json
{ "model": "anthropic/claude-sonnet-4-5", } // trailing comma → JSON.parse throws

// after
{ "model": "anthropic/claude-sonnet-4-5" }
Defensive patterns

Strategy: validation

Validate before calling

function providerModelOrNull(rootDir: string): string | null {
  try {
    const parsed = JSON.parse(readFileSync(join(rootDir, ".caveman", "provider.json"), "utf8"));
    return typeof parsed?.model === "string" ? parsed.model : null;
  } catch {
    return null; // treat unreadable/invalid as absent at YOUR layer; decide explicitly
  }
}

Type guard

function isValidProviderJson(value: unknown): value is { model: string } {
  return typeof value === "object" && value !== null
    && typeof (value as { model?: unknown }).model === "string";
}

Prevention

When it happens

Trigger: provider.json exists but contains invalid JSON (trailing comma, single quotes, comment, BOM, truncated by a concurrent write), is a directory, or is unreadable due to permissions; then a Claude-lane model resolution runs and reaches localModel().

Common situations: Hand-editing provider.json and leaving a trailing comma; a tool writing the file non-atomically so a crash leaves it half-written; committing a template with placeholder text; file owned by another user in shared CI.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/f7b9f78612946a2e. Report an issue: GitHub.