can1357/oh-my-pi · error · Error

Refusing to download onto a file with ${stat.nlink} hard lin

Error message

Refusing to download onto a file with ${stat.nlink} hard links, which would overwrite its other names: ${absolutePath}

What it means

The writer refuses to truncate and overwrite a file that has more than one hard link, because doing so would silently change the content seen under all of the file's other names. It surfaces the actual link count and path so the user can decide which name they truly mean.

Source

Thrown at packages/coding-agent/src/cursor.ts:196

		.open(
			absolutePath,
			fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK,
		)
		.catch((error: NodeJS.ErrnoException) => {
			// A readerless FIFO. Reported as the refusal it is, rather than the
			// bare "no such device or address" the errno spells out.
			if (error.code === "ENXIO") {
				throw new Error(`Refusing to download onto a special file: ${absolutePath}`);
			}
			throw error;
		});
	try {
		const stat = await handle.stat();
		if (!stat.isFile()) {
			throw new Error(`Refusing to download onto a non-regular file: ${absolutePath}`);
		}
		if (stat.nlink > 1) {
			throw new Error(
				`Refusing to download onto a file with ${stat.nlink} hard links, which would overwrite its other names: ${absolutePath}`,
			);
		}
		await handle.truncate(0);
		await handle.writeFile(payload);
	} finally {
		await handle.close();
	}
}

function createToolResultMessage(
	toolCallId: string,
	toolName: string,
	result: AgentToolResult<unknown>,
	isError: boolean,
): ToolResultMessage {
	return {
		role: "toolResult",

View on GitHub (pinned to 9690622007)

Solutions

  1. Delete the intended path first (`rm <path>`) so the download creates a fresh inode instead of overwriting the shared one.
  2. Find and remove or re-link the other names (`find -samefile <path>`) if they are stale.
  3. Copy the file (`cp <path> <path>.tmp && mv`) to break the link before re-running the download.
  4. Pick a different destination filename.

Example fix

// before: alias.bin is hard-linked to data.bin
$ ln data.bin alias.bin  # download to data.bin -> nlink=2 refusal
// after
$ rm alias.bin  # then retry the download to data.bin
Defensive patterns

Strategy: validation

Validate before calling

const s = await stat(downloadPath).catch(() => null);
if (s && s.isFile() && s.nlink > 1) {
  throw new Error(`${downloadPath} has ${s.nlink} hard links; remove the alias first`);
}

Type guard

function hasSingleLink(s: import("node:fs").Stats | undefined): boolean {
  return !!s && s.isFile() && s.nlink <= 1;
}

Try / catch

try {
  await downloadResource(res, downloadPath);
} catch (e) {
  if (String(e.message).includes("hard links")) {
    await rm(downloadPath); // unlink this name, then retry to get a fresh inode
    await downloadResource(res, downloadPath);
  } else throw e;
}

Prevention

When it happens

Trigger: downloadPath points at a regular file whose stat.nlink > 1, i.e. the same inode is reachable via another hard link (ln-created aliases, git checkout artifacts, backup tools that hard-link).

Common situations: rsync/rsnapshot or Time-machine-style hard-link snapshots sharing the inode; a user manually hard-linked a config into two places; docker layer dedup artifacts.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/cc11b4965851ac9e. Report an issue: GitHub.