can1357/oh-my-pi · error · ToolError
Archive write path must target a file, not a directory
Error message
Archive write path must target a file, not a directory
What it means
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.
Source
Thrown at packages/coding-agent/src/tools/write.ts:465
absolutePath: string;
sqlitePath: string;
table: string;
key?: string;
exists: boolean;
}
function isArchivePathNotFound(error: unknown): boolean {
if (isEnoent(error)) return true;
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOTDIR";
}
function normalizeArchiveWriteSubPath(rawPath: string): string {
const normalized = rawPath.replace(/\\/g, "/");
if (normalized.length === 0) {
throw new ToolError("Archive write path must target a file inside the archive");
}
if (normalized.endsWith("/")) {
throw new ToolError("Archive write path must target a file, not a directory");
}
const parts = normalized.split("/");
const normalizedParts: string[] = [];
for (const part of parts) {
if (!part || part === ".") continue;
if (part === "..") {
throw new ToolError("Archive path cannot contain '..'");
}
normalizedParts.push(part);
}
if (normalizedParts.length === 0) {
throw new ToolError("Archive write path must target a file inside the archive");
}
return normalizedParts.join("/");
}View on GitHub (pinned to 9690622007)
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
Example fix
// before
write({ path: "bundle.zip/docs/", content: "x" })
// after
write({ path: "bundle.zip/docs/index.md", content: "x" }) Defensive patterns
Strategy: validation
Validate before calling
function validateArchiveEntry(p: string): string | null {
const sub = p.slice(p.indexOf("/") + 1).replace(/\\/g, "/");
if (sub.endsWith("/")) return "archive entries are files, not directories";
return null;
} Try / catch
try {
await write({ path: target, content });
} catch (e) {
if (e instanceof ToolError && e.message.includes("not a directory")) {
return write({ path: target.replace(/\/+$/, "") + "/file.txt", content });
}
throw e;
} Prevention
- 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
When it happens
Trigger: write({ path: "bundle.zip/docs/", content: "x" }) — in-archive sub-path ends with a slash.
Common situations: Directory-style targets copied from mkdir habits; forgetting that archives get entries, not folders.
Related errors
- Archive write path must target a file inside the archive
- Invalid old-GNU ${field}
- Invalid tar member path '${formatArchivePathForError(rawPath
- Invalid empty tar member path
- Tar directory '${formatArchivePathForError(normalized)}' can
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/3246464b2afd7632.
Report an issue: GitHub.