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

  1. Drop the trailing slash and name a file: "bundle.zip/docs/readme.md"
  2. 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

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


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