JuliusBrussee/caveman · error · Error

caveman-code: old_string appears ${occurrences} times in ${i

Error message

caveman-code: old_string appears ${occurrences} times in ${input.path}; add surrounding context or pass replace_all

What it means

edit_file found more than one occurrence of old_string and replace_all was not set. Without a unique anchor, replacing 'the first match' would be nondeterministic from the caller's perspective, so the tool refuses and asks for more context or an explicit replace_all.

Source

Thrown at packages/agent/src/code.ts:352

      old_string: schema.string(),
      new_string: schema.string(),
      replace_all: schema.optional(schema.boolean()),
    }),
    effect: "write",
    result: "inline",
    timeoutMs: READ_TIMEOUT_MS,
    async execute(input) {
      if (input.old_string === input.new_string) {
        throw new Error("caveman-code: old_string and new_string are identical");
      }
      const target = await contained(input.path);
      const content = await readFile(target, "utf8");
      const occurrences = content.split(input.old_string).length - 1;
      if (occurrences === 0) {
        throw new Error(`caveman-code: old_string not found in ${input.path}`);
      }
      if (occurrences > 1 && input.replace_all !== true) {
        throw new Error(
          `caveman-code: old_string appears ${occurrences} times in ${input.path}; ` +
          "add surrounding context or pass replace_all",
        );
      }
      // split/join UNCONDITIONALLY. String.prototype.replace
      // interprets `$&`, `$\``, `$'`, `$$`, `$1`… in the REPLACEMENT even for a
      // string pattern, so a new_string containing any of them would silently
      // corrupt the file. The non-replace_all branch is guaranteed exactly one
      // occurrence above, so joining replaces precisely that one.
      const updated = content.split(input.old_string).join(input.new_string);
      await writeFile(target, updated, "utf8");
      const replaced = input.replace_all === true ? occurrences : 1;
      return capOutput(
        `edited ${input.path}: ${replaced} replacement${replaced === 1 ? "" : "s"}`,
        caps.edit_file,
      );
    },
  });

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Expand old_string with surrounding lines (e.g. include the preceding line or the full block) until it is unique in the file
  2. If you genuinely want every occurrence replaced, pass replace_all: true
  3. Use the reported occurrence count as feedback: it tells you exactly how much more context you need

Example fix

// before: ambiguous anchor
await edit_file({ path, old_string: "return null;", new_string: "return undefined;" });

// after: unique context
await edit_file({
  path,
  old_string: "function parse(raw) {\n  if (!raw) return null;",
  new_string: "function parse(raw) {\n  if (!raw) return undefined;",
});
// or all occurrences at once:
await edit_file({ path, old_string: "return null;", new_string: "return undefined;", replace_all: true });
Defensive patterns

Strategy: validation

Validate before calling

const occurrences = content.split(old_string).length - 1;
if (occurrences === 0) throw new Error("anchor not present");
if (occurrences > 1 && !replace_all) {
  throw new Error(`anchor is ambiguous (${occurrences} hits); add context or set replace_all`);
}

Try / catch

try {
  await editTool.execute(input);
} catch (err) {
  if (err instanceof Error && /appears \d+ times/.test(err.message)) {
    const n = Number(/appears (\d+) times/.exec(err.message)![1]);
    // widen the anchor with surrounding context and retry, or escalate replace_all
    await editTool.execute({ ...input, old_string: widen(input.old_string) });
  } else throw err;
}

Prevention

When it happens

Trigger: Passing a short or generic anchor (e.g. 'return null;' or '}' ) that occurs multiple times; editing repeated boilerplate (imports, license headers) without replace_all: true.

Common situations: Anchoring on a common one-liner; editing duplicated config blocks; model-generated edits that pick the function signature line without its body.

Related errors


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