paperclipai/paperclip · error · Error

Worktree seed lock ${lockPath} is stale or malformed. Verify

Error message

Worktree seed lock ${lockPath} is stale or malformed. Verify that no seed is running, then remove the stale lock and retry.

What it means

Thrown by acquireWorktreeSeedLock when the lock file exists but its content cannot be parsed into a valid owner AND the file's mtime is older than WORKTREE_SEED_LOCK_MALFORMED_STALE_MS (60s). This treats a corrupt/unrecognizable lock as stale only after the grace window, to avoid racing a live process that just hasn't finished writing.

Source

Thrown at cli/src/commands/worktree.ts:1540

      if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
    }

    const [rawOwner, lockStat] = await Promise.all([
      fsPromises.readFile(lockPath, "utf8").catch(() => null),
      fsPromises.stat(lockPath).catch(() => null),
    ]);
    const currentOwner = rawOwner ? parseWorktreeSeedLockOwner(rawOwner) : null;
    const malformedLockIsStale = Boolean(
      lockStat && Date.now() - lockStat.mtimeMs >= WORKTREE_SEED_LOCK_MALFORMED_STALE_MS,
    );
    if (currentOwner && !processIsAlive(currentOwner.pid)) {
      throw new Error(
        `Worktree seed lock ${lockPath} belongs to exited process ${currentOwner.pid}. `
        + "Verify that no seed is running, then remove the stale lock and retry.",
      );
    }
    if (!currentOwner && malformedLockIsStale) {
      throw new Error(
        `Worktree seed lock ${lockPath} is stale or malformed. `
        + "Verify that no seed is running, then remove the stale lock and retry.",
      );
    }
    await new Promise((resolve) => setTimeout(resolve, WORKTREE_SEED_LOCK_POLL_MS));
  }
}

export async function ensureWorktreeSeeded(
  opts: WorktreeEnsureSeededOptions = {},
  dependencies: { seedDatabase?: SeedWorktreeDatabase } = {},
): Promise<EnsureWorktreeSeededResult> {
  const configPath = resolveConfigPath(opts.config);
  const markers = resolveWorktreeSeedMarkerPaths(configPath);
  mkdirSync(path.dirname(markers.lock), { recursive: true });
  const releaseLock = await acquireWorktreeSeedLock(markers.lock);
  try {
    // These checks deliberately happen under the cross-process lock. A second

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Confirm no seed is currently running.
  2. Delete the malformed/stale lock file at the given path.
  3. Retry the seed command.
  4. If this recurs, check for processes or tools writing to the lock path, and filesystem sync/clock issues.

Example fix

# before: malformed lock older than 60s
# after
ps aux | grep paperclip
rm <lockPath-from-error>
paperclipai worktree reseed ...
Defensive patterns

Strategy: validation

Validate before calling

function lockIsStaleOrMalformed(lockPath: string): boolean {
  const raw = fs.readFileSync(lockPath, 'utf8').catch?.(() => null);
  const owner = raw ? parseWorktreeSeedLockOwner(raw) : null;
  const stat = fs.statSync(lockPath);
  return !owner && Date.now() - stat.mtimeMs >= WORKTREE_SEED_LOCK_MALFORMED_STALE_MS;
}

Try / catch

try {
  await acquireWorktreeSeedLock(lockPath);
} catch (e) {
  if (/stale or malformed/.test((e as Error).message) && !anySeedRunning()) {
    fs.rmSync(lockPath, { force: true }); // retry
  } else throw e;
}

Prevention

When it happens

Trigger: Lock file exists but is empty, partially written, or contains non-JSON/invalid JSON; more than 60 seconds have passed since its mtime; no current owner could be extracted to check liveness.

Common situations: Crash during lock write leaving a truncated file; another tool overwrote the lock with garbage; filesystem clock skew making mtime appear old; manual creation of an empty lock file.

Understand the failure class

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/a3b31f0310923c0d. Report an issue: GitHub.