can1357/oh-my-pi · error · ToolError

write target '${target}' is a semicolon-joined list of ${cou

Error message

write target '${target}' is a semicolon-joined list of ${count} read-tool selectors, not a filesystem path — refusing to create it. write creates a single file; issue one read() per path to read these ranges (e.g. read({ path: "<one path>:<range>" })).

What it means

The write target looks like multiple read-tool selectors joined by semicolons (e.g. 'a.ts:1-5;b.ts:6-9'). write only creates a single file, so it refuses to create such a literal path and points you at per-path read() calls.

Source

Thrown at packages/coding-agent/src/tools/write.ts:196

 * fires regardless of `content` — the non-empty-content escape hatch exists for
 * a lone selector-shaped *filename*, never a `;`-list, and honoring it here
 * silently creates a nested directory tree (`a.txt:1-2;b/`) in the workspace.
 * The caller still probes the literal target first, so an existing POSIX file
 * by that exact name stays writable (same escape as the single-selector guard).
 */
function readSelectorListMisfire(target: string): number | undefined {
	if (!target.includes(";")) return undefined;
	const segments = target.split(";");
	if (segments.length < 2) return undefined;
	for (const segment of segments) {
		const trimmed = segment.trim();
		if (trimmed.length === 0 || splitPathAndSel(trimmed).sel === undefined) return undefined;
	}
	return segments.length;
}

function throwReadSelectorListMisfire(target: string, count: number): never {
	throw new ToolError(
		`write target '${target}' is a semicolon-joined list of ${count} read-tool selectors, not a filesystem path — refusing to create it. ` +
			`write creates a single file; issue one read() per path to read these ranges (e.g. read({ path: "<one path>:<range>" })).`,
	);
}

async function assertNotReadSelectorMisfire(target: string, content: string, cwd: string): Promise<void> {
	const listCount = readSelectorListMisfire(target);
	if (listCount !== undefined && (await probeLiteralPathExists(target, cwd)) === "missing") {
		throwReadSelectorListMisfire(target, listCount);
	}
	const sel = readSelectorForEmptyWrite(target, content);
	if (sel === undefined) return;
	if ((await probeLiteralPathExists(target, cwd)) !== "missing") return;
	throwReadSelectorMisfire(target, sel);
}

const BULK_DIRECTIVE_RE = /^#?(\d+)\s*[:=]\s*(@ours|@theirs|@base|@both)$/;
/**

View on GitHub (pinned to 9690622007)

Solutions

  1. Issue one read() per path/range instead of a single write
  2. Split into separate write() calls, one plain file path each

Example fix

// before
write({ path: "a.ts:1-5;b.ts:6-9", content: "" })
// after
read({ path: "a.ts:1-5" }); read({ path: "b.ts:6-9" })
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeSelectorList(target: string): boolean {
  const segs = target.trim().split(";").map(s => s.trim()).filter(Boolean);
  if (segs.length < 2) return false;
  return segs.every(s => splitPathAndSel(s).sel !== undefined);
}
if (looksLikeSelectorList(p)) {
  for (const s of p.split(";")) await read({ path: s.trim() });
}

Type guard

function isSinglePath(t: string): boolean {
  return t.trim().split(";").length === 1;
}

Try / catch

try {
  await write({ path: target, content });
} catch (e) {
  if (e instanceof ToolError && e.message.includes("semicolon-joined list")) {
    for (const seg of target.split(";")) await read({ path: seg.trim() });
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: write({ path: "a.ts:1-5; b.ts:6-9", content: "" }) — target parses into ≥2 selector segments.

Common situations: Batch-style attempts to read or touch several file ranges in one call, or models concatenating read paths with semicolons.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/7df918ab25d8148e. Report an issue: GitHub.