gohugoio/hugo · error

%s: %q: 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

Hugo's module collector rejects a mount whose `source` escapes the module's own directory when the mount is declared by a theme or module (not the main project). Only the project module may use absolute or non-local (e.g. `../`) source paths; modules/themes are sandboxed to their own tree (with a special carve-out for node_modules imports). This is enforced in normalizeMounts via filepath.IsLocal.

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 8a468df065)

Solutions

  1. Change the mount source in the theme/module config to a path relative to and inside the module directory (e.g. `source = "assets"`).
  2. If the files live outside the module, move the mount declaration into the main project's config, where absolute paths are allowed.
  3. For node_modules packages, use a `node_modules/<pkg>/...` source, which Hugo special-cases for modules (see issue #14089).
  4. Vendor or copy the external files into the module so the source becomes local.

Example fix

# theme/hugo.toml — before
[[module.mounts]]
source = "../shared/assets"
target = "assets"

# after (declare in the project's hugo.toml instead)
[[module.mounts]]
source = "/abs/or/relative/shared/assets"
target = "assets"
Defensive patterns

Strategy: validation

Validate before calling

// before calling modules.Collect / hugo build
for _, m := range cfg.Mounts {
    if strings.HasPrefix(m.Source, "/") || filepath.IsAbs(m.Source) || strings.Contains(m.Source, "://") {
        return fmt.Errorf("mount source %q must be a relative local path", m.Source)
    }
}

Prevention

When it happens

Trigger: A theme's or imported module's hugo.toml/config declares `[[module.mounts]]` with `source` set to an absolute path (`/var/data`) or a path traversing upward (`../shared/content`), then Hugo builds and collects module configs. The main project itself is exempt (`owner.projectMod`).

Common situations: Copying a project-level mounts config into a theme's config file; a theme author trying to mount files from outside the theme repo; Windows drive-letter absolute paths in a theme mount; older configs that relied on looser validation before filepath.IsLocal enforcement.

Related errors


AI-assisted analysis of gohugoio/hugo@8a468df065 (2026-07-31). Data as JSON: /data/errors/2cb025081b61fd50.json. Report an issue: GitHub ↗.