siyuan-note/siyuan · critical
skill storage is not a regular directory
Error message
skill storage is not a regular directory
What it means
openManagedSkillsRoot walks data/storage/ai/agent/skills creating each level and Lstats it; if any level exists but is not a real directory (a regular file, or a symlink), the error is thrown. This guards against storage layout tampering and symlink redirection of skill management operations.
Solutions
- Inspect data/storage/ai/agent/skills (and each parent) — remove or rename the file/symlink and create a real directory in its place
- Replace the symlink with a real directory (copy contents back if the symlink pointed at your data)
- If a stray file occupies a path meant to be a directory, delete it so Mkdir can recreate the level
- Restore the storage subtree from a good backup if the layout is corrupted
Example fix
// before $ ls -l data/storage/ai/agent skills -> /mnt/cloud/skills # symlink // after $ rm data/storage/ai/agent/skills $ mkdir data/storage/ai/agent/skills $ cp /mnt/cloud/skills/* data/storage/ai/agent/skills/
Defensive patterns
Strategy: try-catch
Validate before calling
const st = await fs.lstat('data/storage/ai/agent/skills');
if (!st.isDirectory() || st.isSymbolicLink()) {
throw new Error('skills storage must be a real directory');
} Try / catch
try {
await api.manageSkillFiles({action: 'list', path: '.'});
} catch (e) {
if (e.message.includes('not a regular directory')) {
// fix storage layout: replace file/symlink with a real directory
}
} Prevention
- Do not symlink data/storage subdirectories to synced locations
- Back up and verify data/storage layout after restores
- Avoid creating stray files where directory levels are expected
When it happens
Trigger: Any ManageSkillFiles action or TestSkillManagementCommitConflict when data/storage, data/storage/ai, data/storage/ai/agent, or data/storage/ai/agent/skills is a file or a symlink instead of a directory.
Common situations: A user/script replaced 'skills' with a symlink to a synced folder; a stray file named 'ai' was created inside storage; an interrupted migration left a file where a directory belongs; restoring data from a partial backup.
Related errors
- resolve assets directory
- resolve assets root [ ] failed
- accessing assets in encrypted notebook
- asset path contains an unresolved symbolic link
- asset path resolves outside notebook assets directory
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/0a505724b626b1b4.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/util/skill_manage.go:96
}
// 从工作空间目录逐层打开根目录,拒绝把管理操作重定向到链接目标。
func openManagedSkillsRoot() (*os.Root, error) {
workspace, err := os.OpenRoot(DataDir)
if err != nil {
return nil, err
}
defer workspace.Close()
for _, p := range []string{"storage", "storage/ai", "storage/ai/agent", "storage/ai/agent/skills"} {
if err = workspace.Mkdir(p, 0755); err != nil && !errors.Is(err, os.ErrExist) {
return nil, err
}
info, statErr := workspace.Lstat(p)
if statErr != nil {
return nil, statErr
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return nil, errors.New("skill storage is not a regular directory")
}
}
return workspace.OpenRoot("storage/ai/agent/skills")
}
func checkManagedSkillPath(root *os.Root, p string) error {
if err := validateManagedSkillPath(p); err != nil {
return err
}
parts := strings.Split(p, "/")
for i := range parts {
info, err := root.Lstat(strings.Join(parts[:i+1], "/"))
if errors.Is(err, os.ErrNotExist) && i == len(parts)-1 {
return nil
}
if err != nil {
return err
}View on GitHub (pinned to 9f775e8a12)