{"record":{"id":"d9871ee9644641eb","repo":"paperclipai/paperclip","slug":"invalid-credential-file","errorCode":null,"errorMessage":"Invalid credential file","messagePattern":"Invalid credential file","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/local-ai-credential-file.ts","lineNumber":11,"sourceCode":"import { openRunnerApiWorkspaceFile } from \"./native-runtime/runner-api-files.js\";\n\nconst MAX_CREDENTIAL_BYTES = 64 * 1024;\n\n/** Bounded descriptor read; never follows symlinks or reopens a checked path. */\nexport async function readLocalAiCredentialFile(filename: string): Promise<string> {\n  const file = await openRunnerApiWorkspaceFile(filename);\n  try {\n    const stat = await file.stat();\n    if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o600 || stat.size > MAX_CREDENTIAL_BYTES) {\n      throw new Error(\"Invalid credential file\");\n    }\n    const bytes = Buffer.alloc(MAX_CREDENTIAL_BYTES + 1);\n    let size = 0;\n    while (size < bytes.length) {\n      const read = await file.read(bytes, size, bytes.length - size, size);\n      if (!read.bytesRead) break;\n      size += read.bytesRead;\n    }\n    if (size > MAX_CREDENTIAL_BYTES) throw new Error(\"Invalid credential file\");\n    return bytes.subarray(0, size).toString(\"utf8\");\n  } finally {\n    await file.close();\n  }\n}\n","sourceCodeStart":1,"sourceCodeEnd":26,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/local-ai-credential-file.ts#L1-L26","documentation":"readLocalAiCredentialFile opens a workspace credential file without following symlinks and validates it strictly: it must be a regular file, owned by the current process uid, mode exactly 0600, and at most MAX_CREDENTIAL_BYTES. Any violation throws 'Invalid credential file' before reading content.","triggerScenarios":"File is a symlink or device; owner is a different user than the server process; permissions are not exactly 0600 (e.g., 0644); file larger than MAX_CREDENTIAL_BYTES.","commonSituations":"Editing the credential file with an editor that resets permissions to 0644; copying the file (cp changes mode/owner); running the server as a different user than the one that created the credential; container running as non-root with root-owned file.","solutions":["chmod 600 the credential file","chown the file to the user running the Paperclip server process","Replace the file with a regular file (not a symlink) containing only the credential","Trim/rotate the credential if it exceeds MAX_CREDENTIAL_BYTES"],"exampleFix":"// before\n-rw-r--r-- 1 alice alice credential.txt\n// after\nchmod 600 credential.txt && chown $(id -u) credential.txt\n# or regenerate: claude auth login (writes 0600 file)","handlingStrategy":"validation","validationCode":"import { statSync } from \"node:fs\";\nconst st = statSync(file);\nif (!st.isFile() || st.uid !== process.getuid?.() || (st.mode & 0o777) !== 0o600 || st.size > MAX_CREDENTIAL_BYTES)\n  throw new Error(\"credential file must be a 0600 file owned by the server user and under the size limit\");","typeGuard":null,"tryCatchPattern":"try {\n  const token = await readLocalAiCredentialFile(filename);\n} catch (e) {\n  if (e instanceof Error && e.message === \"Invalid credential file\") {\n    // prompt re-login / fix permissions instead of retrying\n  } else throw e;\n}","preventionTips":["Always create credential files with mode 0600 owned by the server OS user","Never symlink credential files; write them in place","Run the server under the same user that performs provider logins","Use editors/tools that preserve file mode, or re-chmod after editing","Keep credential files minimal — do not point the setting at large bundles"],"tags":["filesystem","security","permissions"],"backgroundTag":"permission-denied","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"}