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

  1. In the theme/module config, make every mount source a path relative to the module root with no `..` or leading slash.
  2. If you genuinely need an external path, declare that mount in the project's own config (project modules may use absolute paths).
  3. Move the shared asset into the theme and reference it relatively.
  4. 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

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


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)