paperclipai/paperclip · error

Invalid credential file

Error message

Invalid credential file

What it means

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.

Solutions

  1. chmod 600 the credential file
  2. chown the file to the user running the Paperclip server process
  3. Replace the file with a regular file (not a symlink) containing only the credential
  4. Trim/rotate the credential if it exceeds MAX_CREDENTIAL_BYTES

Example fix

// before
-rw-r--r-- 1 alice alice credential.txt
// after
chmod 600 credential.txt && chown $(id -u) credential.txt
# or regenerate: claude auth login (writes 0600 file)
Defensive patterns

Strategy: validation

Validate before calling

import { statSync } from "node:fs";
const st = statSync(file);
if (!st.isFile() || st.uid !== process.getuid?.() || (st.mode & 0o777) !== 0o600 || st.size > MAX_CREDENTIAL_BYTES)
  throw new Error("credential file must be a 0600 file owned by the server user and under the size limit");

Try / catch

try {
  const token = await readLocalAiCredentialFile(filename);
} catch (e) {
  if (e instanceof Error && e.message === "Invalid credential file") {
    // prompt re-login / fix permissions instead of retrying
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

Thrown at server/src/services/local-ai-credential-file.ts:11

import { openRunnerApiWorkspaceFile } from "./native-runtime/runner-api-files.js";

const MAX_CREDENTIAL_BYTES = 64 * 1024;

/** Bounded descriptor read; never follows symlinks or reopens a checked path. */
export async function readLocalAiCredentialFile(filename: string): Promise<string> {
  const file = await openRunnerApiWorkspaceFile(filename);
  try {
    const stat = await file.stat();
    if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o600 || stat.size > MAX_CREDENTIAL_BYTES) {
      throw new Error("Invalid credential file");
    }
    const bytes = Buffer.alloc(MAX_CREDENTIAL_BYTES + 1);
    let size = 0;
    while (size < bytes.length) {
      const read = await file.read(bytes, size, bytes.length - size, size);
      if (!read.bytesRead) break;
      size += read.bytesRead;
    }
    if (size > MAX_CREDENTIAL_BYTES) throw new Error("Invalid credential file");
    return bytes.subarray(0, size).toString("utf8");
  } finally {
    await file.close();
  }
}

View on GitHub (pinned to 3f1d897a7c)