JuliusBrussee/caveman · error

Aider config has duplicate read keys; refusing unsafe merge

Error message

Aider config has duplicate read keys; refusing unsafe merge

What it means

aiderNativeConfig() finds the `read:` key to inject the Caveman core file into Aider's read list. If the config contains more than one `read:` key, the block-list target is ambiguous and the installer refuses the unsafe merge rather than editing the wrong section.

Solutions

  1. Consolidate ~/.aider.conf.yml to a single `read:` key with one block list containing all desired paths.
  2. Re-run the Caveman enable command after fixing the config.
  3. Run `caveman doctor aider` if a stale Caveman read block contributed to the duplication.

Example fix

// before
read:
  - foo.md
read:
  - bar.md
// after
read:
  - foo.md
  - bar.md
Defensive patterns

Strategy: validation

Validate before calling

const lines = readFileSync(aiderCfg, 'utf8').split('\n');
const n = lines.filter((l) => /^read\s*:/.test(l)).length;
if (n > 1) throw new Error('consolidate to a single read: key before enabling');

Try / catch

try {
  await cavemanEnableAider();
} catch (e) {
  if (String(e.message).includes('duplicate read keys')) {
    mergeReadBlocks(aiderCfg);
    await cavemanEnableAider();
  }
}

Prevention

When it happens

Trigger: Enabling Caveman for Aider when ~/.aider.conf.yml has two or more top-level `read:` lines, e.g. one block-list `read:` plus another `read:` line appended later.

Common situations: Hand-edited configs where a second `read:` key was pasted in; merge conflicts that kept both occurrences; documentation snippets added without checking existing keys.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


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

Appendix: source

Thrown at packages/cli/src/index.ts:7426

function aiderNativeConfig(source: string, route: string, corePath: string): { text: string; routeBlock: string; previousRouteLine: string | null; readBlock: string } {
  if ([AIDER_NATIVE_ROUTE_BEGIN, AIDER_NATIVE_ROUTE_END, AIDER_NATIVE_READ_BEGIN, AIDER_NATIVE_READ_END].some((marker) => source.includes(marker))) {
    throw new Error("existing Aider Caveman block is unjournaled; run `caveman doctor aider`");
  }
  const newline = source.includes("\r\n") ? "\r\n" : "\n";
  const lines = source.replace(/\r\n/g, "\n").split("\n");
  const routeIndexes = lines.flatMap((line, index) => /^openai-api-base\s*:/.test(line) ? [index] : []);
  if (routeIndexes.length > 1) throw new Error("Aider config has duplicate openai-api-base keys; refusing unsafe merge");
  const routeBlockLines = [AIDER_NATIVE_ROUTE_BEGIN, `openai-api-base: ${yamlQuote(route)}`, AIDER_NATIVE_ROUTE_END];
  const routeBlock = routeBlockLines.join(newline);
  const previousRouteLine = routeIndexes.length === 1 ? lines[routeIndexes[0]!]! : null;
  if (routeIndexes.length === 1) lines.splice(routeIndexes[0]!, 1, ...routeBlockLines);
  else {
    if (lines.some((line) => line.trim() !== "") && lines[lines.length - 1]?.trim()) lines.push("");
    lines.push(...routeBlockLines);
  }

  const readIndexes = lines.flatMap((line, index) => /^read\s*:/.test(line) ? [index] : []);
  if (readIndexes.length > 1) throw new Error("Aider config has duplicate read keys; refusing unsafe merge");
  let readBlockLines: string[];
  if (readIndexes.length === 1) {
    const readIndex = readIndexes[0]!;
    if (!/^read\s*:\s*(?:#.*)?$/.test(lines[readIndex]!)) {
      throw new Error("Aider config uses inline/scalar read; use block-list form before enabling Caveman");
    }
    let end = readIndex + 1;
    while (end < lines.length && !/^[A-Za-z0-9][A-Za-z0-9_-]*\s*:/.test(lines[end]!)) end++;
    readBlockLines = [`  ${AIDER_NATIVE_READ_BEGIN}`, `  - ${yamlQuote(corePath)}`, `  ${AIDER_NATIVE_READ_END}`];
    lines.splice(end, 0, ...readBlockLines);
  } else {
    if (lines.some((line) => line.trim() !== "") && lines[lines.length - 1]?.trim()) lines.push("");
    readBlockLines = [AIDER_NATIVE_READ_BEGIN, "read:", `  - ${yamlQuote(corePath)}`, AIDER_NATIVE_READ_END];
    lines.push(...readBlockLines);
  }
  const readBlock = readBlockLines.join(newline);
  const text = lines.join(newline).replace(/(?:\r?\n)+$/, "") + newline;
  return { text, routeBlock, previousRouteLine, readBlock };

View on GitHub (pinned to 3ee70a1026)