{"record":{"id":"14513e5da5a5940b","repo":"can1357/oh-my-pi","slug":"archive-write-path-must-target-a-file-inside-the-a","errorCode":null,"errorMessage":"Archive write path must target a file inside the archive","messagePattern":"Archive write path must target a file inside the archive","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/write.ts","lineNumber":462,"sourceCode":"}\n\ninterface ResolvedSqliteWritePath {\n\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}","sourceCodeStart":444,"sourceCodeEnd":480,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/write.ts#L444-L480","documentation":"When writing into an archive (zip/sqlite-style targets), the sub-path after the archive prefix must name an entry inside it. An empty sub-path gives the tool nothing to write, so it fails closed.","triggerScenarios":"write({ path: \"archive.zip/\" }) or a target whose in-archive sub-path normalizes to empty (e.g. only slashes or dots after the archive root).","commonSituations":"Trailing-slash typos, forgetting the entry name after the archive path, template substitution leaving the sub-path blank.","solutions":["Append the entry path: write({ path: \"archive.zip/dir/file.txt\", content })","Remove the trailing slash and supply a concrete file name inside the archive"],"exampleFix":"// before\nwrite({ path: \"bundle.zip/\", content: \"x\" })\n// after\nwrite({ path: \"bundle.zip/assets/file.txt\", content: \"x\" })","handlingStrategy":"validation","validationCode":"function validateArchiveTarget(p: string): string | null {\n  const sub = p.replace(/^[^/]+\\//, \"\").replace(/\\\\/g, \"/\");\n  if (sub.trim().length === 0 || sub === \"\") return \"empty archive sub-path\";\n  return null;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await write({ path: target, content });\n} catch (e) {\n  if (e instanceof ToolError && /must target a file inside the archive/.test(e.message)) {\n    throw new Error(`Supply an in-archive entry path, got: ${target}`);\n  }\n  throw e;\n}","preventionTips":["Always append a concrete entry file name after the archive prefix","Check for template/variable substitution leaving the sub-path empty","Strip trailing slashes from constructed archive targets"],"tags":["archive","path-validation"],"backgroundTag":"archive-path-invalid","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}