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
- Delete the intended path first (`rm <path>`) so the download creates a fresh inode instead of overwriting the shared one.
- Find and remove or re-link the other names (`find -samefile <path>`) if they are stale.
- Copy the file (`cp <path> <path>.tmp && mv`) to break the link before re-running the download.
- 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
- Avoid hard-linking files that tools may overwrite (snapshots, rsync --link-dest).
- Run `find -samefile <path>` before overwriting files with unknown aliases.
- Prefer symlinks over hard links when an alias is needed.
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
- ENXIO
- Refusing to download onto a non-regular file: ${absolutePath
- The managed-skills root is a symlink; refusing to operate ou
- Managed skill "${name}" SKILL.md is a symlink; refusing to o
- Managed skill "${name}" resolves through a symlink; refusing
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/cc11b4965851ac9e.
Report an issue: GitHub.