paperclipai/paperclip · error · Error

paperclip_runner_file_handoff_<code>

paperclip_runner_file_handoff_<code>

Error message

'paperclip_runner_file_handoff_' + code

What it means

The runner-side file-handoff script (READ_REMOTE_FILE) defines fail(code), which throws Error('paperclip_runner_file_handoff_' + code) whenever a confinement, identity, or integrity check fails before any file bytes are read. Error codes are appended to the prefix, e.g. paperclip_runner_file_handoff_outside_root or _stat_mismatch. It runs inside the target machine as a Node script, so the error surfaces through the remote transport.

Solutions

  1. Inspect the suffix after 'paperclip_runner_file_handoff_' to identify which check failed (confinement, symlink, identity, hash).
  2. Request only paths strictly inside the handoff root; use path.resolve and confirm path.relative(root, file) does not start with '..' and is not absolute.
  3. Remove symlinks from the requested path or resolve them within the root before the handoff.
  4. If identity mismatch, re-run the handoff after the file has stabilized; do not modify the file during transfer.
Defensive patterns

Strategy: try-catch

Validate before calling

const rel = path.posix.relative(root, requested);
if (!rel || rel === ".." || rel.startsWith("../") || path.isAbsolute(requested)) throw new Error("path outside handoff root");

Try / catch

try {
  const data = await readRemoteFile(root, file);
} catch (e) {
  if (String(e.message).startsWith("paperclip_runner_file_handoff_")) {
    const code = e.message.slice("paperclip_runner_file_handoff_".length);
    console.error(`file handoff rejected: ${code}`);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Remote file read where the requested path resolves outside the allowed root (path traversal), crosses a symlink, the stat identity (dev/ino/size/mtimeNs/ctimeNs) changes between check and read, or hash/identity verification fails.

Common situations: Requesting a file via a symlink pointing outside the workspace; a path like '../../etc/passwd' blocked by the within(root, file) check; the file being modified concurrently between stat and read so the identity comparison fails.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/feb18beb85a02dcd. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/native-runtime/remote-deliverable-file.ts:18

import { createHash } from "node:crypto";
import { posix } from "node:path";
import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/command-managed-runtime";

import { MAX_ATTACHMENT_BYTES } from "../../attachment-types.js";

export const MAX_REMOTE_DELIVERABLE_BYTES = MAX_ATTACHMENT_BYTES;
const PREFIX = "paperclip_runner_file_handoff_";
const READ_TIMEOUT_MS = 10_000;

// Runs only in the server-bound remote workspace. No file is opened on the
// controller and no bytes are emitted until confinement and identity pass.
const READ_REMOTE_FILE = String.raw`
const fs = require('node:fs/promises');
const { constants } = require('node:fs');
const path = require('node:path');
const { createHash } = require('node:crypto');
const fail = code => { throw new Error('paperclip_runner_file_handoff_' + code); };
const same = (a, b) => ['dev', 'ino', 'size', 'mtimeNs', 'ctimeNs'].every(key => a[key] === b[key]);
const within = (root, file) => {
  const relative = path.relative(root, file);
  return relative && relative !== '..' && !relative.startsWith('../') && !path.isAbsolute(relative);
};
async function noSymlinks(root, relative) {
  let current = root;
  for (const segment of relative.split('/')) {
    current = path.join(current, segment);
    if ((await fs.lstat(current)).isSymbolicLink()) fail('symlink_denied');
  }
}
(async () => {
  const input = JSON.parse(process.argv[1]);
  const root = await fs.realpath(input.workspaceRoot);
  if (!(await fs.stat(root)).isDirectory()) fail('path_denied');
  const relative = path.normalize(input.contentRef);
  const candidate = path.resolve(root, relative);

View on GitHub (pinned to 3f1d897a7c)