can1357/oh-my-pi · error · ToolError

write does not accept the trailing selector ":${sel}" — it w

Error message

write does not accept the trailing selector ":${sel}" — it writes a whole file. Remove ":${sel}", or if the filename truly ends with it, percent-encode the ":" as %3A.

What it means

The write tool rejects a path that carries a trailing `:selector` suffix (e.g. `notes.md:raw`). Read supports such selectors, but write always replaces the entire file, so a selector is either a copy-paste mistake or a filename that literally contains a colon. The library throws a ToolError rather than silently writing to a mangled filename.

Source

Thrown at packages/coding-agent/src/tools/path-utils.ts:481

	return { path, sel: chunks.join(":") };
}

/**
 * Peel a read-tool selector off an internal-URL write target so `write` resolves
 * the same file `read` does (e.g. `ssh://h/f:raw` -> `ssh://h/f`). Only the
 * whole-file display modes `raw`/`conflicts` are accepted (they do not change
 * which bytes are written); any other selector-shaped tail `splitInternalUrlSel`
 * peels — a line range, a compound like `raw:1-20`, or a malformed `:-N` — throws,
 * because `write` addresses a whole file, not a partial range, and silently
 * stripping it would write to a path the caller never named. Non-URL paths and
 * URLs without a selector pass through unchanged.
 */
export function peelWriteUrlSelector(rawPath: string): string {
	const { path, sel } = splitInternalUrlSel(rawPath);
	if (sel === undefined) return rawPath;
	// Case-insensitive to match read's selector grammar (parseSel + the /i regexes above).
	if (/^(?:raw|conflicts)$/i.test(sel)) return path;
	throw new ToolError(
		`write does not accept the trailing selector ":${sel}" — it writes a whole file. ` +
			`Remove ":${sel}", or if the filename truly ends with it, percent-encode the ":" as %3A.`,
	);
}

function assertNotInternalUrl(expanded: string, original: string): void {
	for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
		if (expanded.startsWith(prefix)) {
			throw new Error(
				`Path "${original}" uses internal scheme "${prefix}" and must be resolved through the proper protocol handler, not as a filesystem path.`,
			);
		}
	}
}

export function normalizeLocalScheme(filePath: string): string {
	return filePath.replace(/^(local:)\/(?!\/)/, "$1//");
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove the trailing `:<selector>` from the path and pass only the plain file path to write.
  2. If the filename legitimately ends with `:<sel>`, percent-encode the colon in the path as `%3A` so the selector parser does not fire.
  3. If you intended to edit part of a file, use the edit tool or read with a selector, then write the whole file.

Example fix

// before
write({ path: "docs/guide.md:raw", content: "..." })
// after
write({ path: "docs/guide.md", content: "..." })
Defensive patterns

Strategy: validation

Validate before calling

function isWritablePath(p) {
  const m = p.match(/:([^/:]+)$/);
  return !(m && /^(raw|conflicts)$/i.test(m[1]) === false && p.includes(":"));
}
if (!isWritablePath(p)) p = p.replace(/:([^/:]+)$/, "");

Type guard

function hasSelectorSuffix(p) {
  const m = p.match(/:([^/:]+)$/);
  return m !== null;
}

Prevention

When it happens

Trigger: Calling the write tool (via resolveToCwd/peelWriteUrlSelector) with a path argument that splitInternalUrlSel parses as path+selector where the selector is not `raw` or `conflicts` — e.g. `write path="file.ts:summary"` after reusing a read-style path.

Common situations: Copy-pasting a read-tool path (with its `:raw`/`:lines` selector) into a write call; agent-generated paths that append `:something`; filenames genuinely containing `:` (Windows drive-like names, timestamped names like `log:2026-01-01.txt`).

Related errors


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