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
- 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
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
- 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
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
- ACPX runtime executable must be a bounded executable file
- Materialized OpenCode executable has unsafe permissions
- 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…
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)