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
- Use read({ path: "src/foo.ts:50-80" }) if you meant to read that range
- Provide non-empty content: a non-empty write to that literal name is never blocked
- 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
- Never append :line or :start-end ranges to write targets
- Use read() for range access; use write() only with plain file paths
- Strip any selector suffix before composing a write path
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
- write target '${target}' is a semicolon-joined list of ${cou
- invalid template, {}, contains directory separator
- invalid suffix {}, contains directory separator
- git-subdir path "${source.path}" does not exist in cloned re
- Absolute paths are not allowed in omp:// URLs
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/61284ce5a5c19927.
Report an issue: GitHub.