{"record":{"id":"72a96cd48ce594b5","repo":"multica-ai/multica","slug":"stat-s-w-72a96c","errorCode":null,"errorMessage":"stat %s: %w","messagePattern":"stat (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/internal/daemon/execenv/hermes_home.go","lineNumber":624,"sourceCode":"// entry kind.\nfunc linkSharedHermesEntry(src, dst string) error {\n\tif fi, err := os.Lstat(dst); err == nil {\n\t\tif fi.Mode()&os.ModeSymlink != 0 {\n\t\t\tif target, err := os.Readlink(dst); err == nil && target == src {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tif err := os.RemoveAll(dst); err != nil {\n\t\t\treturn fmt.Errorf(\"remove stale %s: %w\", dst, err)\n\t\t}\n\t}\n\n\tinfo, err := os.Stat(src) // follow the link to decide dir vs file\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil // dangling source in the user's home — nothing to link\n\t\t}\n\t\treturn fmt.Errorf(\"stat %s: %w\", src, err)\n\t}\n\tif info.IsDir() {\n\t\treturn createDirLink(src, dst)\n\t}\n\treturn createFileLink(src, dst)\n}\n\n// writeDerivedHermesConfig writes the task-local config.yaml: the user's config\n// with `skills.external_dirs` set to their existing external dirs plus the shared\n// ~/.hermes/skills, all as absolute paths. When the user has no config we still\n// write a minimal one so their global skills stay reachable via the external\n// root. If the config can't be parsed we copy it verbatim so auth/model settings\n// survive — the bound skills still load from the task-local skills/ dir, which is\n// the point of the fix; only the user's global skills would be missing. The file\n// is written 0600 (it can hold inline api_key secrets) via atomic replace, so\n// reuse also repairs a prior file's permissions.\nfunc writeDerivedHermesConfig(sharedHome, hermesHome string, env map[string]string, logger *slog.Logger) error {\n\tsrcConfig := filepath.Join(sharedHome, \"config.yaml\")","sourceCodeStart":606,"sourceCodeEnd":642,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/daemon/execenv/hermes_home.go#L606-L642","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if _, err := os.Stat(src); err != nil && !errors.Is(err, fs.ErrNotExist) {\n\treturn fmt.Errorf(\"shared entry unstatable, skipping mirror of %s: %w\", src, err)\n}","typeGuard":null,"tryCatchPattern":"if err := linkSharedHermesEntry(src, dst); err != nil {\n\tvar pe *os.PathError\n\tif errors.As(err, &pe) {\n\t\tswitch {\n\t\tcase errors.Is(pe.Err, syscall.ELOOP):\n\t\t\treturn nil // symlink cycle in user's home: skip, do not fail the overlay\n\t\tcase errors.Is(pe.Err, syscall.EACCES):\n\t\t\tlog.Printf(\"no permission to stat %s — fix shared home perms\", pe.Path)\n\t\t}\n\t}\n\treturn err\n}","preventionTips":["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."],"tags":["filesystem","permissions","symlink","hermes","execenv"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}