different-ai/openwork · error

Archive contains an unsafe path

Error message

Archive contains an unsafe path

What it means

importWorkspaceConfig imports a workspace config ZIP and extracts only safe entries. Before writing any file it runs isSafeArchivePath, which rejects absolute paths, Windows drive letters, and any segment that normalizes to '..' or an empty part. This is a zip-slip guard: it prevents a crafted archive from writing files outside targetDir via traversal paths.

Source

Thrown at apps/desktop/electron/workspace-archive.mjs:299

      excluded,
    }, null, 2)}\n`,
  });
  await writeZip(outputPath, files);
  return { outputPath, included: included.length, excluded };
}

export async function importWorkspaceConfig({ archivePath, targetDir, name }) {
  if (await pathExists(targetDir)) {
    if (!(await isDirectoryEmpty(targetDir))) throw new Error("Target folder must be empty");
  }
  await mkdir(targetDir, { recursive: true });

  const archiveStats = await stat(archivePath);
  if (archiveStats.size > MAX_ARCHIVE_BYTES) throw new Error("Workspace archive is too large.");
  const buffer = await readFile(archivePath);
  for (const entry of listZipEntries(buffer)) {
    if (entry.name === "manifest.json" || entry.name.endsWith("/")) continue;
    if (!isSafeArchivePath(entry.name)) throw new Error("Archive contains an unsafe path");
    if (!(entry.name === "opencode.json" || entry.name.startsWith(".opencode/"))) continue;
    if (isSecretName(path.basename(entry.name))) continue;
    const outPath = path.join(targetDir, ...entry.name.split("/"));
    await mkdir(path.dirname(outPath), { recursive: true });
    await writeFile(outPath, readZipEntryData(buffer, entry));
  }

  const opencodeDir = path.join(targetDir, ".opencode");
  if (!(await pathExists(opencodeDir))) throw new Error("Archive is missing .opencode config");

  const openworkPath = path.join(opencodeDir, "openwork.json");
  let preset = "starter";
  let workspaceName = typeof name === "string" && name.trim() ? name.trim() : null;

  if (await pathExists(openworkPath)) {
    const raw = await readFile(openworkPath, "utf8");
    try {
      const config = JSON.parse(raw);

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Open the archive and inspect entry names for '..', leading '/', or 'X:' prefixes; rebuild the archive with plain relative paths (e.g. '.opencode/agents/foo.md').
  2. Re-export the archive with exportWorkspaceConfig from a working workspace, which always emits safe relative names.
  3. If a tool keeps producing backslash names, normalize them to '/' before zipping — ZIP entry names must use forward slashes.

Example fix

// before: entry name '../../evil.sh' aborts the whole import
// after: repack the archive with safe relative entry names
zip -r workspace.zip manifest.json opencode.json .opencode/
Defensive patterns

Strategy: validation

Validate before calling

import { listZipEntries } from "./workspace-archive.mjs";
function assertSafeArchive(buffer) {
  for (const e of listZipEntries(buffer)) {
    if (e.name === "manifest.json" || e.name.endsWith("/")) continue;
    if (e.name.startsWith("/") || /^[A-Za-z]:/.test(e.name) ||
        e.name.split("/").some((p) => p === ".." || p === "")) {
      throw new Error(`Unsafe archive entry: ${e.name}`);
    }
  }
}

Type guard

function isSafeArchivePath(name) {
  if (!name || name.startsWith("/") || /^[A-Za-z]:/.test(name)) return false;
  const parts = name.split("/");
  return !parts.some((p) => p === ".." || p === "");
}

Try / catch

try {
  await importWorkspaceConfig({ archivePath, targetDir });
} catch (err) {
  if (err.message === "Archive contains an unsafe path") {
    // reject the archive; do not retry, it is malicious or corrupt
  } else throw err;
}

Prevention

When it happens

Trigger: Calling importWorkspaceConfig({ archivePath, targetDir }) with a ZIP whose entry names contain '..' segments (e.g. '../../.bashrc'), start with '/', or look like 'C:\evil'.

Common situations: Importing an archive edited or rebuilt by a third-party tool that stores backslash or drive-letter paths; a shared/hand-modified archive; a malicious archive from an untrusted source.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/19e3c14f242384bf. Report an issue: GitHub.