can1357/oh-my-pi · error · ToolError

write target '${target}' ends with a read-tool selector ':${

Error message

write target '${target}' ends with a read-tool selector ':${sel}' and no such file exists — refusing to create a literal file by that name. If you meant to read it, use read({ path: "${target}" }). If you truly intend to create this file, pass its contents in `content` (a non-empty write is never blocked).

What it means

The write target ends in ':<selector>' (a read-tool range/line selector) and no file by that literal name exists. Writing would create a nonsense file like 'foo.ts:10-20', so the tool refuses for empty writes and suggests using read() instead.

Source

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

 * filename is legal on POSIX (issue #4618), that request otherwise resolves to
 * filesystem creation and reports success, leaving a stray zero-byte file the
 * model cannot recover from — the local analogue of the `xd://` near-miss guard
 * ({@link assertWriteTargetAddressable}, issue #6123).
 *
 * Fires only on the high-confidence combination the report identifies: the tail
 * parses as a read-tool selector, the literal target is missing, and no content
 * was supplied. Non-empty content is the escape hatch — it is never blocked, so
 * a deliberate write to a selector-shaped filename still succeeds. An existing
 * literal path or an ambiguous stat (`"unknown"`: EACCES, transient I/O) also
 * passes through so a real file is never shadowed by the guard.
 */
function readSelectorForEmptyWrite(target: string, content: string): string | undefined {
	if (content.length > 0) return undefined;
	return splitPathAndSel(target).sel;
}

function throwReadSelectorMisfire(target: string, sel: string): never {
	throw new ToolError(
		`write target '${target}' ends with a read-tool selector ':${sel}' and no such file exists — refusing to create a literal file by that name. ` +
			`If you meant to read it, use read({ path: "${target}" }). ` +
			`If you truly intend to create this file, pass its contents in \`content\` (a non-empty write is never blocked).`,
	);
}

/**
 * Recognize a semicolon-joined list of read-tool selectors mis-dispatched as a
 * single write target — the multi-file read expression the scout emitted in
 * issue #6809 (`a.txt:1-2;b/c.txt:3-4`). Every `;`-segment must be non-empty and
 * carry its own read selector ({@link splitPathAndSel} peels a `:N-M`, `:raw`,
 * or `:conflicts` tail). No real call targets such a list: `read` accepts one
 * path, `write` writes one file. Unlike {@link readSelectorForEmptyWrite} this
 * 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).

View on GitHub (pinned to 9690622007)

Solutions

  1. Use read({ path: "src/foo.ts:50-80" }) if you meant to read that range
  2. Provide non-empty content: a non-empty write to that literal name is never blocked
  3. Remove the ':selector' suffix and write to the plain file path

Example fix

// before
write({ path: "src/foo.ts:50-80", content: "" })
// after
read({ path: "src/foo.ts:50-80" })
Defensive patterns

Strategy: validation

Validate before calling

function hasReadSelector(target: string): boolean {
  return /:[0-9]+(-[0-9]+)?$/.test(target.trim());
}
if (hasReadSelector(p) && content.length === 0) {
  return read({ path: p });
}

Type guard

function isPlainFilePath(t: string): boolean {
  return splitPathAndSel(t.trim()).sel === undefined;
}

Try / catch

try {
  await write({ path: target, content });
} catch (e) {
  if (e instanceof ToolError && e.message.includes("read-tool selector")) {
    return read({ path: target });
  }
  throw e;
}

Prevention

When it happens

Trigger: write({ path: "src/foo.ts:50-80", content: "" }) — an empty-content write whose target parses with a read selector and no such literal file exists.

Common situations: A model confusing write with read syntax, attempting to 'write a range' of an existing file, or echoing read paths back into write.

Related errors


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