gohugoio/hugo · error
: : mount source must be a local path for modules/themes
Error message
%s: %q: mount source must be a local path for modules/themes
What it means
normalizeMounts enforces that mounts defined inside a theme or module (owner.projectMod false) must use a local (relative, in-tree) source — it calls filepath.IsLocal which rejects absolute paths and any path containing `..`. A non-local source from a theme/module would let it reach arbitrary filesystem locations, so it's rejected.
Solutions
- In the theme/module config, make every mount source a path relative to the module root with no `..` or leading slash.
- If you genuinely need an external path, declare that mount in the project's own config (project modules may use absolute paths).
- Move the shared asset into the theme and reference it relatively.
- Publish a corrected version of the theme with local-only mount sources.
Example fix
// before (inside theme config): not local [[module.mounts]] source = "/home/me/shared/assets" target = "assets" // after: keep assets inside the theme [[module.mounts]] source = "shared-assets" target = "assets"
Defensive patterns
Strategy: validation
Validate before calling
// Reject non-local mount sources in any non-project module config
import "path/filepath"
func validateMountSources(ownerIsProject bool, mounts []Mount) error {
if ownerIsProject { return nil }
for _, m := range mounts {
if !filepath.IsLocal(m.Source) {
return fmt.Errorf("theme/module mount source must be local: %s", m.Source)
}
}
return nil
} Type guard
null
Try / catch
// On 'mount source must be a local path', move the asset in-tree or relocate the mount to the project config.
Prevention
- In theme/module configs use only relative, in-tree mount sources.
- Keep absolute/external mounts in the project's own config.
- Lint theme configs in CI for escaping mount sources.
When it happens
Trigger: A theme/module's config defines `[[module.mounts]] source = "/abs/path"` or `source = "../sibling"`. Since the owner is not the project, Hugo refuses to honour escapes; only the project itself may mount absolute/external paths.
Common situations: Theme author copies a project-level mount (with an absolute or `..` source) into the theme config; theme developed as a project then published without sanitising mounts; cross-theme shared assets referenced via `..`.
Related errors
- failed to apply mounts for project
- cannot vendor module
- could not determine content directory for
- invalid module config for
- invalid module path ; must be relative to themesDir when…
AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09).
Data as JSON: /api/errors/2cb025081b61fd50.
Report an issue: GitHub.
Appendix: source
Thrown at modules/collect.go:819
}
// Special case for node_modules imports in themes/modules.
// See #14089.
var isModuleNodeModulesImport bool
if !owner.projectMod {
nodeModulesImportSource := c.nodeModulesRoot(mnt.Source)
if nodeModulesImportSource != "" {
isModuleNodeModulesImport = true
mnt.Source = nodeModulesImportSource
}
}
mnt.Source = filepath.Clean(mnt.Source)
mnt.Target = filepath.Clean(mnt.Target)
var sourceDir string
if !owner.projectMod && !filepath.IsLocal(mnt.Source) {
return nil, fmt.Errorf("%s: %q: mount source must be a local path for modules/themes", errMsg, mnt.Source)
}
if filepath.IsAbs(mnt.Source) {
// Abs paths in the main project is allowed.
sourceDir = mnt.Source
} else {
sourceDir = filepath.Join(dir, mnt.Source)
}
// Verify that Source exists
_, err := c.fs.Stat(sourceDir)
if err != nil {
if paths.IsSameFilePath(sourceDir, c.ccfg.PublishDir) {
// This is a little exotic, but there are use cases for mounting the public folder.
// This will typically also be in .gitingore, so create it.
if err := c.fs.MkdirAll(sourceDir, 0o755); err != nil {
return nil, fmt.Errorf("%s: %q", errMsg, err)
}
} else if strings.HasSuffix(sourceDir, files.FilenameHugoStatsJSON) {View on GitHub (pinned to 52c9bd7908)