{"record":{"id":"bc42e66a862b52b8","repo":"can1357/oh-my-pi","slug":"write-does-not-accept-the-trailing-selector-se","errorCode":null,"errorMessage":"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.","messagePattern":"write does not accept the trailing selector \":(.+?)\" — it writes a whole file\\. Remove \":(.+?)\", or if the filename truly ends with it, percent-encode the \":\" as %3A\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/path-utils.ts","lineNumber":481,"sourceCode":"\treturn { path, sel: chunks.join(\":\") };\n}\n\n/**\n * Peel a read-tool selector off an internal-URL write target so `write` resolves\n * the same file `read` does (e.g. `ssh://h/f:raw` -> `ssh://h/f`). Only the\n * whole-file display modes `raw`/`conflicts` are accepted (they do not change\n * which bytes are written); any other selector-shaped tail `splitInternalUrlSel`\n * peels — a line range, a compound like `raw:1-20`, or a malformed `:-N` — throws,\n * because `write` addresses a whole file, not a partial range, and silently\n * stripping it would write to a path the caller never named. Non-URL paths and\n * URLs without a selector pass through unchanged.\n */\nexport function peelWriteUrlSelector(rawPath: string): string {\n\tconst { path, sel } = splitInternalUrlSel(rawPath);\n\tif (sel === undefined) return rawPath;\n\t// Case-insensitive to match read's selector grammar (parseSel + the /i regexes above).\n\tif (/^(?:raw|conflicts)$/i.test(sel)) return path;\n\tthrow new ToolError(\n\t\t`write does not accept the trailing selector \":${sel}\" — it writes a whole file. ` +\n\t\t\t`Remove \":${sel}\", or if the filename truly ends with it, percent-encode the \":\" as %3A.`,\n\t);\n}\n\nfunction assertNotInternalUrl(expanded: string, original: string): void {\n\tfor (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {\n\t\tif (expanded.startsWith(prefix)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Path \"${original}\" uses internal scheme \"${prefix}\" and must be resolved through the proper protocol handler, not as a filesystem path.`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport function normalizeLocalScheme(filePath: string): string {\n\treturn filePath.replace(/^(local:)\\/(?!\\/)/, \"$1//\");\n}","sourceCodeStart":463,"sourceCodeEnd":499,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/path-utils.ts#L463-L499","documentation":"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.","triggerScenarios":"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.","commonSituations":"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`).","solutions":["Remove the trailing `:<selector>` from the path and pass only the plain file path to write.","If the filename legitimately ends with `:<sel>`, percent-encode the colon in the path as `%3A` so the selector parser does not fire.","If you intended to edit part of a file, use the edit tool or read with a selector, then write the whole file."],"exampleFix":"// before\nwrite({ path: \"docs/guide.md:raw\", content: \"...\" })\n// after\nwrite({ path: \"docs/guide.md\", content: \"...\" })","handlingStrategy":"validation","validationCode":"function isWritablePath(p) {\n  const m = p.match(/:([^/:]+)$/);\n  return !(m && /^(raw|conflicts)$/i.test(m[1]) === false && p.includes(\":\"));\n}\nif (!isWritablePath(p)) p = p.replace(/:([^/:]+)$/, \"\");","typeGuard":"function hasSelectorSuffix(p) {\n  const m = p.match(/:([^/:]+)$/);\n  return m !== null;\n}","tryCatchPattern":null,"preventionTips":["Never reuse read-tool path strings (with selectors) as write paths.","Percent-encode literal colons in filenames as %3A.","Strip selectors centrally before any write call."],"tags":["path","write-tool","selector","validation"],"backgroundTag":"invalid-path-selector","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}