{"record":{"id":"feb18beb85a02dcd","repo":"paperclipai/paperclip","slug":"paperclip-runner-file-handoff-code","errorCode":"paperclip_runner_file_handoff_<code>","errorMessage":"'paperclip_runner_file_handoff_' + code","messagePattern":"'paperclip_runner_file_handoff_' \\+ code","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/remote-deliverable-file.ts","lineNumber":18,"sourceCode":"import { createHash } from \"node:crypto\";\nimport { posix } from \"node:path\";\nimport type { CommandManagedRuntimeRunner } from \"@paperclipai/adapter-utils/command-managed-runtime\";\n\nimport { MAX_ATTACHMENT_BYTES } from \"../../attachment-types.js\";\n\nexport const MAX_REMOTE_DELIVERABLE_BYTES = MAX_ATTACHMENT_BYTES;\nconst PREFIX = \"paperclip_runner_file_handoff_\";\nconst READ_TIMEOUT_MS = 10_000;\n\n// Runs only in the server-bound remote workspace. No file is opened on the\n// controller and no bytes are emitted until confinement and identity pass.\nconst READ_REMOTE_FILE = String.raw`\nconst fs = require('node:fs/promises');\nconst { constants } = require('node:fs');\nconst path = require('node:path');\nconst { createHash } = require('node:crypto');\nconst fail = code => { throw new Error('paperclip_runner_file_handoff_' + code); };\nconst same = (a, b) => ['dev', 'ino', 'size', 'mtimeNs', 'ctimeNs'].every(key => a[key] === b[key]);\nconst within = (root, file) => {\n  const relative = path.relative(root, file);\n  return relative && relative !== '..' && !relative.startsWith('../') && !path.isAbsolute(relative);\n};\nasync function noSymlinks(root, relative) {\n  let current = root;\n  for (const segment of relative.split('/')) {\n    current = path.join(current, segment);\n    if ((await fs.lstat(current)).isSymbolicLink()) fail('symlink_denied');\n  }\n}\n(async () => {\n  const input = JSON.parse(process.argv[1]);\n  const root = await fs.realpath(input.workspaceRoot);\n  if (!(await fs.stat(root)).isDirectory()) fail('path_denied');\n  const relative = path.normalize(input.contentRef);\n  const candidate = path.resolve(root, relative);","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/remote-deliverable-file.ts#L1-L36","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const rel = path.posix.relative(root, requested);\nif (!rel || rel === \"..\" || rel.startsWith(\"../\") || path.isAbsolute(requested)) throw new Error(\"path outside handoff root\");","typeGuard":null,"tryCatchPattern":"try {\n  const data = await readRemoteFile(root, file);\n} catch (e) {\n  if (String(e.message).startsWith(\"paperclip_runner_file_handoff_\")) {\n    const code = e.message.slice(\"paperclip_runner_file_handoff_\".length);\n    console.error(`file handoff rejected: ${code}`);\n    return;\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["security","filesystem","sandbox"],"backgroundTag":"path-traversal-blocked","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}