multica-ai/multica · error
stat %s: %w
Error message
stat %s: %w
What it means
linkSharedHermesEntry could not os.Stat the shared-home source entry, and the failure was not 'does not exist' (a dangling symlink in the user's home is deliberately skipped with nil). Stat follows symlinks to decide dir-vs-file linking, so errors like EACCES on a path component or ELOOP on a symlink cycle propagate.
Source
Thrown at server/internal/daemon/execenv/hermes_home.go:624
// entry kind.
func linkSharedHermesEntry(src, dst string) error {
if fi, err := os.Lstat(dst); err == nil {
if fi.Mode()&os.ModeSymlink != 0 {
if target, err := os.Readlink(dst); err == nil && target == src {
return nil
}
}
if err := os.RemoveAll(dst); err != nil {
return fmt.Errorf("remove stale %s: %w", dst, err)
}
}
info, err := os.Stat(src) // follow the link to decide dir vs file
if err != nil {
if os.IsNotExist(err) {
return nil // dangling source in the user's home — nothing to link
}
return fmt.Errorf("stat %s: %w", src, err)
}
if info.IsDir() {
return createDirLink(src, dst)
}
return createFileLink(src, dst)
}
// writeDerivedHermesConfig writes the task-local config.yaml: the user's config
// with `skills.external_dirs` set to their existing external dirs plus the shared
// ~/.hermes/skills, all as absolute paths. When the user has no config we still
// write a minimal one so their global skills stay reachable via the external
// root. If the config can't be parsed we copy it verbatim so auth/model settings
// survive — the bound skills still load from the task-local skills/ dir, which is
// the point of the fix; only the user's global skills would be missing. The file
// is written 0600 (it can hold inline api_key secrets) via atomic replace, so
// reuse also repairs a prior file's permissions.
func writeDerivedHermesConfig(sharedHome, hermesHome string, env map[string]string, logger *slog.Logger) error {
srcConfig := filepath.Join(sharedHome, "config.yaml")View on GitHub (pinned to 2c0912b6ec)
Solutions
- Reproduce as the daemon user: `stat <sharedHome>/<entry>`; fix the permission on the failing path component.
- Find and break symlink cycles: `namei -l <path>` or `readlink` chain inspection.
- Grant the daemon's account read access to the whole Hermes home, or run the daemon as the home's owner.
- Remove the offending entry from the shared home if it is not needed by Hermes.
Defensive patterns
Strategy: validation
Validate before calling
if _, err := os.Stat(src); err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("shared entry unstatable, skipping mirror of %s: %w", src, err)
} Try / catch
if err := linkSharedHermesEntry(src, dst); err != nil {
var pe *os.PathError
if errors.As(err, &pe) {
switch {
case errors.Is(pe.Err, syscall.ELOOP):
return nil // symlink cycle in user's home: skip, do not fail the overlay
case errors.Is(pe.Err, syscall.EACCES):
log.Printf("no permission to stat %s — fix shared home perms", pe.Path)
}
}
return err
} Prevention
- Audit ~/.hermes for cyclic symlinks when this recurs (namei -l helps).
- Give the daemon search (x) permission on every directory in the shared home path.
When it happens
Trigger: A component of the source path lacks execute/search permission for the daemon user; the entry is a symlink loop (a→b→a); the path exceeds NAME_MAX; I/O error on the volume holding the shared home.
Common situations: Shared Hermes home on restrictive storage (mode-600 directories from another user); manually-created cyclic symlinks inside ~/.hermes; enterprise ACL'd network shares; multi-user hosts where the daemon account is not the home owner.
Related errors
- remove stale %s: %w
- mint PAT: response missing token
- daemon profile is not resolved yet; token sync skipped
- runtime local skill discovery timed out
- runtime local skill import failed
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/72a96cd48ce594b5.
Report an issue: GitHub.