can1357/oh-my-pi · error
Managed skill "${name}" SKILL.md has ${fileStat.nlink} hard
Error message
Managed skill "${name}" SKILL.md has ${fileStat.nlink} hard links; refusing to overwrite a file that may be user-authored elsewhere. What it means
Managed skills are machine-generated, so overwriting a SKILL.md with more than one hard link could destroy a file the user authored elsewhere (same inode under another path). assertManagedSkillFileSafeForUpdate throws when fileStat.nlink > 1.
Source
Thrown at packages/coding-agent/src/autolearn/managed-skills.ts:134
*/
async function assertManagedRootSafe(): Promise<void> {
const rootStat = await fs.lstat(getManagedSkillsDir()).catch(err => {
if (isEnoent(err)) return null;
throw err;
});
if (rootStat?.isSymbolicLink()) {
throw new Error("The managed-skills root is a symlink; refusing to operate outside the managed directory.");
}
}
const UPDATE_FILE_OPEN_FLAGS = fsConstants.O_WRONLY | fsConstants.O_NOFOLLOW;
function assertManagedSkillFileSafeForUpdate(name: string, fileStat: Stats): void {
if (!fileStat.isFile()) {
throw new Error(`Managed skill "${name}" SKILL.md is not a regular file; refusing to overwrite it.`);
}
if (fileStat.nlink > 1) {
throw new Error(
`Managed skill "${name}" SKILL.md has ${fileStat.nlink} hard links; refusing to overwrite a file that may be user-authored elsewhere.`,
);
}
}
async function openManagedSkillFileForUpdate(name: string, file: string) {
try {
return await fs.open(file, UPDATE_FILE_OPEN_FLAGS);
} catch (err) {
if ((err as { code?: string }).code === "ELOOP") {
throw new Error(`Managed skill "${name}" SKILL.md is a symlink; refusing to overwrite it.`);
}
throw err;
}
}
/** Create or update a managed `SKILL.md`. Returns the resolved file path. */
export async function writeManagedSkill(input: WriteManagedSkillInput): Promise<{ path: string }> {View on GitHub (pinned to 9690622007)
Solutions
- Replace the hard-linked file with an independent copy (cp --remove-destination or rm then recreate), then retry
- Find all links with `find ~/.omp -samefile <path>` and remove the ones you don't want
- If you want a user-authored copy, move it out of managed-skills and into ~/.omp/agent/skills as a normal file
Example fix
// before (shell) ln ~/.omp/agent/skills/foo/SKILL.md ~/.omp/agent/managed-skills/foo/SKILL.md // after (shell) rm ~/.omp/agent/managed-skills/foo/SKILL.md && cp ~/.omp/agent/skills/foo/SKILL.md ~/.omp/agent/managed-skills/foo/SKILL.md
Defensive patterns
Strategy: validation
Validate before calling
import { stat } from "node:fs/promises";
const st = await stat(skillMdPath).catch(() => null);
if (st && st.nlink > 1) throw new Error("SKILL.md has multiple hard links"); Type guard
function isSingleLinkedFile(st: { isFile(): boolean; nlink: number }): boolean {
return st.isFile() && st.nlink === 1;
} Try / catch
try {
await writeManagedSkill({ name, action: "update", ... });
} catch (err) {
if (String((err as Error).message).includes("hard links")) {
// break the link: rm + cp the content back, then retry
} else throw err;
} Prevention
- Use `cp`, not `ln`, when copying skills into managed-skills
- Find existing links with `find ~/.omp -samefile <path>` before editing
- Avoid dedup/backup tools that hard-link files under ~/.omp
When it happens
Trigger: writeManagedSkill targets an existing SKILL.md whose inode has multiple hard links — typically because someone hard-linked the file into (or out of) the managed directory.
Common situations: A backup or dotfiles tool hard-linked skills into the managed dir; a user ran `ln` to alias a managed skill to their own skills directory; deduplicating filesystems creating links.
Related errors
- {} and {} are the same file
- missing destination file operand after {}
- extra operand {} Try '{} --help' for more information.
- {}: hard link not allowed for directory
- {0}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/48a30e3959c42a44.
Report an issue: GitHub.