can1357/oh-my-pi · error

The managed-skills root is a symlink; refusing to operate ou

Error message

The managed-skills root is a symlink; refusing to operate outside the managed directory.

What it means

Before any managed-skill write or delete, assertManagedRootSafe lstats the managed-skills root and refuses to operate if it is a symbolic link. This is a security guard so writes stay confined to the real managed directory and cannot be redirected elsewhere via a symlink.

Source

Thrown at packages/coding-agent/src/autolearn/managed-skills.ts:123

	void guarded.finally(() => {
		if (skillMutationChains.get(name) === guarded) skillMutationChains.delete(name);
	});
	return run;
}

/**
 * Reject when the managed-skills root itself is a symlink. lstat on a child
 * follows intermediate components, so a symlinked root would let an otherwise
 * valid name write/delete outside the isolated directory (e.g. onto authored
 * skills). Checked before composing any child path.
 */
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 {

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove the symlink and let omp recreate managed-skills as a real directory (rm the link, run the operation again)
  2. If you need skills elsewhere, put them in the user-authored skills dir (~/.omp/agent/skills), not a symlinked managed root
  3. Check `ls -la ~/.omp/agent/` to confirm the root is a real directory

Example fix

// before (shell)
ln -s ~/sync/managed-skills ~/.omp/agent/managed-skills
// after (shell)
rm ~/.omp/agent/managed-skills && mkdir ~/.omp/agent/managed-skills
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from "node:fs/promises";
const st = await lstat("~/.omp/agent/managed-skills");
if (st.isSymbolicLink()) throw new Error("managed-skills root must not be a symlink");

Type guard

function isRealDirectoryStat(st: { isSymbolicLink(): boolean; isDirectory(): boolean }): boolean {
  return st.isDirectory() && !st.isSymbolicLink();
}

Try / catch

try {
  await writeManagedSkill(input);
} catch (err) {
  if (String((err as Error).message).includes("managed-skills root is a symlink")) {
    // remove the symlink and recreate as a real dir, or abort
  } else throw err;
}

Prevention

When it happens

Trigger: writeManagedSkill or deleteManagedSkill runs while ~/.omp/agent/managed-skills is a symlink (to another directory or an attacker-controlled location).

Common situations: A user replaced the managed-skills dir with a symlink to share skills across machines or sync tools (Dropbox, dotfiles repo) created it as a link; setup scripts reorganized ~/.omp with symlinks.

Related errors


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