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
- Inspect the suffix after 'paperclip_runner_file_handoff_' to identify which check failed (confinement, symlink, identity, hash).
- 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.
- Remove symlinks from the requested path or resolve them within the root before the handoff.
- 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
- Only request paths inside the handoff root; resolve and normalize before requesting.
- Avoid symlinks in handoff targets or resolve them within the root.
- Do not mutate files between stat and read during a handoff.
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
- Codex working directory cannot contain the host HOME
- A trusted viewer build is required for public chat reports
- ACPX runtime executable changed while it was verified
- ACPX runtime executable could not be opened as a no-follow…
- ACPX runtime executable must be a bounded executable file
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)