paperclipai/paperclip · error

native_runner_authority_rotation_state_unsafe

native_runner_authority_rotation_state_unsafe

Error message

native_runner_authority_rotation_state_unsafe

What it means

readRunnerState() applies the same safety validation as the control-plane reader but to runner-state.json, with a tighter 16 MiB cap. It throws native_runner_authority_rotation_state_unsafe when the file is a symlink, not a regular file, or oversized, because the runner state file backs authority epoch rotation and must not come from an untrusted or anomalous path.

Source

Thrown at packages/paperclip-runner/src/live/runnerd-codex-transport.ts:170

  const metadata = lstatSync(path);
  if (
    metadata.isSymbolicLink() ||
    !metadata.isFile() ||
    metadata.size > 64 * 1024 * 1024
  ) {
    throw new Error("native_runner_control_plane_state_unsafe");
  }
  return record(JSON.parse(readFileSync(path, "utf8")));
}

function readRunnerState(path: string): Record<string, unknown> {
  const metadata = lstatSync(path);
  if (
    metadata.isSymbolicLink() ||
    !metadata.isFile() ||
    metadata.size > 16 * 1024 * 1024
  ) {
    throw new Error("native_runner_authority_rotation_state_unsafe");
  }
  return record(JSON.parse(readFileSync(path, "utf8")));
}

function controlPlaneIdentity(
  state: Record<string, unknown>,
): DurableRecoveryIdentity {
  return structuredClone(
    record(state.identity) as unknown as DurableRecoveryIdentity,
  );
}

function recoveryIdentityMatches(
  value: DurableRecoveryIdentity | Record<string, unknown>,
  expected: DurableRecoveryIdentity,
): boolean {
  return (
    value.runnerInstanceId === expected.runnerInstanceId &&

View on GitHub (pinned to 01ad858492)

Solutions

  1. Check `ls -la runner-state.json`; remove symlinks and restore a plain regular file
  2. Verify size is under 16 MiB; truncate/regenerate the state file from a known-good copy
  3. Exclude the runner state directory from symlink-creating sync/backup tools
  4. If unrecoverable, archive the whole state root and let the runner re-initialize it

Example fix

// before
const st = transport.runnerState();
// after
const st = fs.lstatSync(runnerStatePath);
if (!st.isSymbolicLink() && fs.statSync(runnerStatePath).isFile() && st.size <= 16 * 1024 * 1024) {
  const state = transport.runnerState();
}
Defensive patterns

Strategy: validation

Validate before calling

import { lstatSync, statSync } from "node:fs";
function isSafeRunnerState(p: string): boolean {
  const m = lstatSync(p);
  return !m.isSymbolicLink() && statSync(p).isFile() && m.size <= 16 * 1024 * 1024;
}

Try / catch

try {
  const state = transport.runnerState();
} catch (err) {
  if (err.code === "native_runner_authority_rotation_state_unsafe") {
    logger.error("runner-state.json unsafe (symlink/special/oversized)");
    restoreRunnerStateFromBackup();
  } else throw err;
}

Prevention

When it happens

Trigger: Calling runnerState() or candidateRunnerState() (or triggering rotateExternalAuthorityEpoch/#closeOnce flows) while runner-state.json is a symlink, special file, or larger than 16 MiB.

Common situations: User symlinked runner-state.json to a shared config; a previous crash left a corrupted multi-megabyte state file; tests running against tmpdirs with odd file types; antivirus or sync tools (Dropbox) replacing the file with links.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/03997d25cabb2613. Report an issue: GitHub.