multica-ai/multica · error

clear hermes skills dir: %w

Error message

clear hermes skills dir: %w

What it means

writeHermesBoundSkills could not os.RemoveAll the task-local skills/ directory before rebuilding it from scratch. The rebuild-from-empty design guarantees a skill removed since the last run cannot linger; a failed wipe therefore fails the overlay instead of risking a stale skill winning a name collision.

Source

Thrown at server/internal/daemon/execenv/hermes_home.go:787

		}
		add(filepath.Clean(entry))
	}
	// The shared skills dir, referenced (not copied) so the user's global and
	// builtin skills stay visible. It differs from the task-local skills dir,
	// so Hermes won't fold it into the local root, and being last it yields to
	// the bound skills on a name collision.
	add(filepath.Join(sharedHome, "skills"))
	return out
}

// writeHermesBoundSkills rebuilds the task-local skills/ dir from scratch so a
// skill removed since the last run can't linger, then writes only the
// Multica-bound skills. They keep their natural slug (no user skills share this
// dir) and therefore take precedence over any same-named external skill.
func writeHermesBoundSkills(hermesHome string, workspaceSkills []SkillContextForEnv, logger *slog.Logger) error {
	skillsDir := filepath.Join(hermesHome, "skills")
	if err := os.RemoveAll(skillsDir); err != nil {
		return fmt.Errorf("clear hermes skills dir: %w", err)
	}
	if len(workspaceSkills) == 0 {
		// Defensive: callers gate on a non-empty set, but stay correct if that
		// ever changes — an empty local dir just means the external root is the
		// only source, matching un-redirected behavior.
		return os.MkdirAll(skillsDir, 0o700)
	}
	// Skills live under env.RootDir/hermes-home, which the GC loop (cloud) or
	// env teardown (local_directory) wipes wholesale — no sidecar manifest.
	return writeSkillFiles(skillsDir, workspaceSkills, nil)
}

// existingHermesExternalDirs reads the raw skills.external_dirs entries from a
// parsed config document, accepting either a single string or a list (both of
// which Hermes accepts). Returns nil when absent.
func existingHermesExternalDirs(doc *yaml.Node) []string {
	top := yamlDocumentRoot(doc)
	if top == nil {

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Inspect <hermesHome>/skills ownership and permissions; fix so the daemon UID can remove it.
  2. On Windows, exclude the env RootDir from AV/indexing/sync tools, or stop the tool holding handles, then retry.
  3. Delete the skills dir manually (it is fully regenerated) or wipe the whole overlay.
  4. Ensure a single daemon instance owns a given env RootDir at a time.
Defensive patterns

Strategy: try-catch

Try / catch

if err := writeHermesBoundSkills(hermesHome, skills, logger); err != nil {
	if strings.Contains(err.Error(), "clear hermes skills dir") {
		// wipe retry once: skills dir is fully rebuilt from the skill set
		if os.RemoveAll(skillsDir) == nil {
			return writeHermesBoundSkills(hermesHome, skills, logger)
		}
	}
	return err
}

Prevention

When it happens

Trigger: skills/ (or something inside it) is undeletable: read-only or foreign-owned files, a Windows handle held open by a watcher/indexer/AV scanning skill files, or an immutable flag.

Common situations: Windows file-watching tools (editors, Dropbox, AV) pinning skill files while a task starts; overlay dirs touched by root during debugging; concurrent tasks sharing one env RootDir.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/947fc0fc0a1e358e. Report an issue: GitHub.