{"record":{"id":"3246464b2afd7632","repo":"can1357/oh-my-pi","slug":"archive-write-path-must-target-a-file-not-a-direc","errorCode":null,"errorMessage":"Archive write path must target a file, not a directory","messagePattern":"Archive write path must target a file, not a directory","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/write.ts","lineNumber":465,"sourceCode":"\tabsolutePath: string;\n\tsqlitePath: string;\n\ttable: string;\n\tkey?: string;\n\texists: boolean;\n}\n\nfunction isArchivePathNotFound(error: unknown): boolean {\n\tif (isEnoent(error)) return true;\n\treturn typeof error === \"object\" && error !== null && \"code\" in error && error.code === \"ENOTDIR\";\n}\n\nfunction normalizeArchiveWriteSubPath(rawPath: string): string {\n\tconst normalized = rawPath.replace(/\\\\/g, \"/\");\n\tif (normalized.length === 0) {\n\t\tthrow new ToolError(\"Archive write path must target a file inside the archive\");\n\t}\n\tif (normalized.endsWith(\"/\")) {\n\t\tthrow new ToolError(\"Archive write path must target a file, not a directory\");\n\t}\n\n\tconst parts = normalized.split(\"/\");\n\tconst normalizedParts: string[] = [];\n\tfor (const part of parts) {\n\t\tif (!part || part === \".\") continue;\n\t\tif (part === \"..\") {\n\t\t\tthrow new ToolError(\"Archive path cannot contain '..'\");\n\t\t}\n\t\tnormalizedParts.push(part);\n\t}\n\n\tif (normalizedParts.length === 0) {\n\t\tthrow new ToolError(\"Archive write path must target a file inside the archive\");\n\t}\n\n\treturn normalizedParts.join(\"/\");\n}","sourceCodeStart":447,"sourceCodeEnd":483,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/write.ts#L447-L483","documentation":"Archive entries are files; a sub-path ending in '/' denotes a directory, which write cannot create or fill. The normalizer rejects it before any archive mutation.","triggerScenarios":"write({ path: \"bundle.zip/docs/\", content: \"x\" }) — in-archive sub-path ends with a slash.","commonSituations":"Directory-style targets copied from mkdir habits; forgetting that archives get entries, not folders.","solutions":["Drop the trailing slash and name a file: \"bundle.zip/docs/readme.md\"","If you need directory structure, write concrete file entries; parents are implied"],"exampleFix":"// before\nwrite({ path: \"bundle.zip/docs/\", content: \"x\" })\n// after\nwrite({ path: \"bundle.zip/docs/index.md\", content: \"x\" })","handlingStrategy":"validation","validationCode":"function validateArchiveEntry(p: string): string | null {\n  const sub = p.slice(p.indexOf(\"/\") + 1).replace(/\\\\/g, \"/\");\n  if (sub.endsWith(\"/\")) return \"archive entries are files, not directories\";\n  return null;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await write({ path: target, content });\n} catch (e) {\n  if (e instanceof ToolError && e.message.includes(\"not a directory\")) {\n    return write({ path: target.replace(/\\/+$/, \"\") + \"/file.txt\", content });\n  }\n  throw e;\n}","preventionTips":["Never end archive write targets with '/'","Model directory structure implicitly by writing file entries (a/b/c.txt)","Validate targets end with a file name before calling write"],"tags":["archive","path-validation","directory-target"],"backgroundTag":"archive-path-is-directory","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}