{"record":{"id":"926b91b7bb0155f3","repo":"JuliusBrussee/caveman","slug":"pending-label-transaction-has-unsafe-permission","errorCode":null,"errorMessage":"pending ${label} transaction has unsafe permissions; refusing recovery","messagePattern":"pending (.+?) transaction has unsafe permissions; refusing recovery","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":12365,"sourceCode":"function readOwnedMcpPendingJournalAt(\n  path: string,\n  expected: { agent?: \"kilo\" | \"qwen\"; serverName?: string; configPath?: string; locatorPath?: string } = {},\n): ReadOwnedMcpPendingJournal | null {\n  const label = ownedMcpPendingLabel(expected.agent, expected.serverName);\n  let bytes: Buffer;\n  let value: Record<string, unknown>;\n  try {\n    bytes = readFileSync(path);\n    value = JSON.parse(bytes.toString(\"utf8\")) as Record<string, unknown>;\n  } catch (error) {\n    if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n    throw new Error(`cannot read pending ${label} transaction: ${(error as Error).message}`);\n  }\n  if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n    throw new Error(`pending ${label} transaction is malformed; refusing recovery`);\n  }\n  if (process.platform !== \"win32\" && (statSync(path).mode & 0o077) !== 0) {\n    throw new Error(`pending ${label} transaction has unsafe permissions; refusing recovery`);\n  }\n  const keys = [\n    \"action\", \"agent\", \"config_after_sha256\", \"config_before_base64\", \"config_before_mode\", \"config_path\",\n    \"config_before_sha256\", \"marker_after_base64\", \"marker_after_sha256\", \"marker_before_base64\", \"marker_before_mode\",\n    \"marker_before_sha256\", \"marker_path\", \"schema_version\", \"server_name\", \"transaction_id\",\n  ].sort();\n  const journalAgent = value.agent;\n  const journalServer = value.server_name;\n  if (Object.keys(value).sort().join(\"\\0\") !== keys.join(\"\\0\")\n    || value.schema_version !== 1\n    || typeof value.transaction_id !== \"string\"\n    || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value.transaction_id)\n    || (journalAgent !== \"kilo\" && journalAgent !== \"qwen\")\n    || typeof journalServer !== \"string\"\n    || !mcpServerToolName(journalServer)\n    || (expected.agent !== undefined && journalAgent !== expected.agent)\n    || (expected.serverName !== undefined && journalServer !== expected.serverName)\n    || (value.action !== \"install\" && value.action !== \"uninstall\")","sourceCodeStart":12347,"sourceCodeEnd":12383,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L12347-L12383","documentation":"Thrown when the pending journal file's POSIX permission bits allow group or other access (mode & 0o077 !== 0). Journals can contain configuration snapshots, so the library requires owner-only permissions (0600) and refuses recovery of world/group-readable files. Skipped on win32 where POSIX modes don't apply.","triggerScenarios":"Recovery reads a .pending journal whose stat mode is e.g. 0644 or 0664; a umask of 022 when the journal was created; copying the file with cp/scp which applied default permissive modes.","commonSituations":"Journals synced through Dropbox/git that lost 0600 mode; creating files as root then running recovery as another user; Docker volume mounts normalizing permissions.","solutions":["chmod 600 <journal.pending> on the pending journal file(s) and re-run recovery.","Set a restrictive umask (e.g. umask 077) so future journals are created owner-only.","Avoid copying pending journals through tools that reset permissions; recreate the transaction instead."],"exampleFix":"// before\n-rw-r--r-- kilo-mcp-install.pending\n// after\nchmod 600 kilo-mcp-install.pending  # -rw-------","handlingStrategy":"validation","validationCode":"import { statSync } from 'node:fs';\nif (process.platform !== 'win32' && (statSync(journalPath).mode & 0o077) !== 0) fs.chmodSync(journalPath, 0o600);","typeGuard":null,"tryCatchPattern":"try { recoverPending(); } catch (e) {\n  if (e.message.includes('unsafe permissions')) { fs.chmodSync(journalPath, 0o600); recoverPending(); }\n}","preventionTips":["Run with umask 077 in shells/scripts that create journals.","chmod 600 after any copy/sync that may reset permissions.","Avoid syncing .pending files through git/Docker volumes/cloud folders."],"tags":["permissions","security","journal-recovery"],"backgroundTag":"permission-denied","analyzedSha":"5184b3d11ac6a1acb7d44b9bfaa31698157cff97","analyzedAt":"2026-09-06T12:00:26.372Z","contentChangedAt":"2026-09-06T12:00:26.372Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}