{"record":{"id":"cc11b4965851ac9e","repo":"can1357/oh-my-pi","slug":"refusing-to-download-onto-a-file-with-stat-nlink","errorCode":null,"errorMessage":"Refusing to download onto a file with ${stat.nlink} hard links, which would overwrite its other names: ${absolutePath}","messagePattern":"Refusing to download onto a file with (.+?) hard links, which would overwrite its other names: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/cursor.ts","lineNumber":196,"sourceCode":"\t\t.open(\n\t\t\tabsolutePath,\n\t\t\tfs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK,\n\t\t)\n\t\t.catch((error: NodeJS.ErrnoException) => {\n\t\t\t// A readerless FIFO. Reported as the refusal it is, rather than the\n\t\t\t// bare \"no such device or address\" the errno spells out.\n\t\t\tif (error.code === \"ENXIO\") {\n\t\t\t\tthrow new Error(`Refusing to download onto a special file: ${absolutePath}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t});\n\ttry {\n\t\tconst stat = await handle.stat();\n\t\tif (!stat.isFile()) {\n\t\t\tthrow new Error(`Refusing to download onto a non-regular file: ${absolutePath}`);\n\t\t}\n\t\tif (stat.nlink > 1) {\n\t\t\tthrow new Error(\n\t\t\t\t`Refusing to download onto a file with ${stat.nlink} hard links, which would overwrite its other names: ${absolutePath}`,\n\t\t\t);\n\t\t}\n\t\tawait handle.truncate(0);\n\t\tawait handle.writeFile(payload);\n\t} finally {\n\t\tawait handle.close();\n\t}\n}\n\nfunction createToolResultMessage(\n\ttoolCallId: string,\n\ttoolName: string,\n\tresult: AgentToolResult<unknown>,\n\tisError: boolean,\n): ToolResultMessage {\n\treturn {\n\t\trole: \"toolResult\",","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/cursor.ts#L178-L214","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: alias.bin is hard-linked to data.bin\n$ ln data.bin alias.bin  # download to data.bin -> nlink=2 refusal\n// after\n$ rm alias.bin  # then retry the download to data.bin","handlingStrategy":"validation","validationCode":"const s = await stat(downloadPath).catch(() => null);\nif (s && s.isFile() && s.nlink > 1) {\n  throw new Error(`${downloadPath} has ${s.nlink} hard links; remove the alias first`);\n}","typeGuard":"function hasSingleLink(s: import(\"node:fs\").Stats | undefined): boolean {\n  return !!s && s.isFile() && s.nlink <= 1;\n}","tryCatchPattern":"try {\n  await downloadResource(res, downloadPath);\n} catch (e) {\n  if (String(e.message).includes(\"hard links\")) {\n    await rm(downloadPath); // unlink this name, then retry to get a fresh inode\n    await downloadResource(res, downloadPath);\n  } else throw e;\n}","preventionTips":["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."],"tags":["filesystem","security","mcp"],"backgroundTag":"hardlink-overwrite-refused","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}