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

  1. Replace the hard-linked file with an independent copy (cp --remove-destination or rm then recreate), then retry
  2. Find all links with `find ~/.omp -samefile <path>` and remove the ones you don't want
  3. 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

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


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