paperclipai/paperclip · critical

Daytona syncOut refusing unparseable symlink entry: ${line}

Error message

Daytona syncOut refusing unparseable symlink entry: ${line}

What it means

Thrown by `assertTarballEntriesConfined` (file-sync.ts:210) when a tar entry whose permission type-flag starts with `l` (symlink) does not contain the `" -> "` separator that delimits the link target in GNU `tar -tvf` output. Without the target, the guard cannot verify the symlink stays inside the extraction dir, so it refuses the entry closed.

Source

Thrown at packages/plugins/sandbox-providers/daytona/src/file-sync.ts:210

 */
async function assertTarballEntriesConfined(archivePath: string): Promise<void> {
  const { stdout } = await execFileAsync("tar", ["-tvf", archivePath], {
    env: { ...process.env, COPYFILE_DISABLE: "1" },
    maxBuffer: 32 * 1024 * 1024,
  });
  const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
  for (const line of lines) {
    // GNU tar -tvf: "<perms> <owner>/<group> <size> <date> <time> <name>[ -> target]".
    const match = line.match(/^(\S+)\s+\S+\s+\d+\s+\S+\s+\S+\s+(.*)$/);
    if (!match) {
      throw new Error(`Daytona syncOut refusing tarball with an unparseable entry listing: ${line}`);
    }
    const typeFlag = match[1][0];
    let name = match[2];
    let linkTarget: string | null = null;
    if (typeFlag === "l") {
      const idx = name.indexOf(" -> ");
      if (idx === -1) throw new Error(`Daytona syncOut refusing unparseable symlink entry: ${line}`);
      linkTarget = name.slice(idx + " -> ".length);
      name = name.slice(0, idx);
    } else if (typeFlag === "h") {
      const idx = name.indexOf(" link to ");
      if (idx === -1) throw new Error(`Daytona syncOut refusing unparseable hardlink entry: ${line}`);
      linkTarget = name.slice(idx + " link to ".length);
      name = name.slice(0, idx);
    }
    const cleanName = name.replace(/\/+$/, "");
    if (cleanName.length > 0 && posixPathEscapes(cleanName)) {
      throw new Error(`Daytona syncOut refusing tarball member that escapes the extraction dir: ${name}`);
    }
    if (linkTarget !== null) {
      const resolved = path.posix.join(path.posix.dirname(cleanName), linkTarget);
      if (path.posix.isAbsolute(linkTarget) || posixPathEscapes(resolved)) {
        throw new Error(
          `Daytona syncOut refusing tarball link whose target escapes the extraction dir: ${name} -> ${linkTarget}`,
        );

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Use GNU tar in the sandbox so symlink entries render as `... name -> target`.
  2. Avoid filenames containing newlines in synced-out trees (sanitize before tar).
  3. Re-pack the archive with a compatible tar implementation before outbound sync.
Defensive patterns

Strategy: try-catch

Validate before calling

// Reject filenames containing newlines before syncOut so tar -tvf lines stay parseable.
function hasNewlineInName(name) { return /[\r\n]/.test(name); }

Try / catch

try {
  await performSyncOut({ sandbox, operations, remoteDir, timeoutSeconds });
} catch (err) {
  if (err instanceof Error && /unparseable symlink entry/.test(err.message)) {
    // ensure GNU tar in sandbox; strip/escape filenames with newlines; then retry
  } else throw err;
}

Prevention

When it happens

Trigger: An untrusted sandbox-authored tarball contains a symlink entry whose verbose listing line has a leading `l` perm flag but no `" -> <target>"` suffix — e.g. a truncated line, a filename literally containing newlines that split the entry, or a non-GNU tar emitting symlinks differently.

Common situations: BSD tar or a different tar implementation formats symlinks without the ` -> ` marker; a filename with embedded newlines breaks the per-line parse; or a deliberately crafted archive to confuse the confinement check.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/9ce30a7971b3aeca. Report an issue: GitHub.