JuliusBrussee/caveman · error · Error
pending ${label} transaction has unsafe permissions; refusin
Error message
pending ${label} transaction has unsafe permissions; refusing recovery What it means
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.
Source
Thrown at packages/cli/src/index.ts:12365
function readOwnedMcpPendingJournalAt(
path: string,
expected: { agent?: "kilo" | "qwen"; serverName?: string; configPath?: string; locatorPath?: string } = {},
): ReadOwnedMcpPendingJournal | null {
const label = ownedMcpPendingLabel(expected.agent, expected.serverName);
let bytes: Buffer;
let value: Record<string, unknown>;
try {
bytes = readFileSync(path);
value = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw new Error(`cannot read pending ${label} transaction: ${(error as Error).message}`);
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`pending ${label} transaction is malformed; refusing recovery`);
}
if (process.platform !== "win32" && (statSync(path).mode & 0o077) !== 0) {
throw new Error(`pending ${label} transaction has unsafe permissions; refusing recovery`);
}
const keys = [
"action", "agent", "config_after_sha256", "config_before_base64", "config_before_mode", "config_path",
"config_before_sha256", "marker_after_base64", "marker_after_sha256", "marker_before_base64", "marker_before_mode",
"marker_before_sha256", "marker_path", "schema_version", "server_name", "transaction_id",
].sort();
const journalAgent = value.agent;
const journalServer = value.server_name;
if (Object.keys(value).sort().join("\0") !== keys.join("\0")
|| value.schema_version !== 1
|| typeof value.transaction_id !== "string"
|| !/^[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)
|| (journalAgent !== "kilo" && journalAgent !== "qwen")
|| typeof journalServer !== "string"
|| !mcpServerToolName(journalServer)
|| (expected.agent !== undefined && journalAgent !== expected.agent)
|| (expected.serverName !== undefined && journalServer !== expected.serverName)
|| (value.action !== "install" && value.action !== "uninstall")View on GitHub (pinned to 5184b3d11a)
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.
Example fix
// before -rw-r--r-- kilo-mcp-install.pending // after chmod 600 kilo-mcp-install.pending # -rw-------
Defensive patterns
Strategy: validation
Validate before calling
import { statSync } from 'node:fs';
if (process.platform !== 'win32' && (statSync(journalPath).mode & 0o077) !== 0) fs.chmodSync(journalPath, 0o600); Try / catch
try { recoverPending(); } catch (e) {
if (e.message.includes('unsafe permissions')) { fs.chmodSync(journalPath, 0o600); recoverPending(); }
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: Journals synced through Dropbox/git that lost 0600 mode; creating files as root then running recovery as another user; Docker volume mounts normalizing permissions.
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
- cave_live_eval_sandbox_profile_escapes_root
- caveman-code: path escapes the workspace: ${candidate}
- caveman agent: file source escapes project root
- cave_host_sandbox_nested_under_required
- cave_untrusted_tool_definition
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-09-06).
Data as JSON: /api/errors/926b91b7bb0155f3.
Report an issue: GitHub.