gohugoio/hugo · error

invalid module path ; must be relative to themesDir when…

Error message

invalid module path %q; must be relative to themesDir when defined outside of the project

What it means

createThemeDirname validates that a non-project module's import path resolves to a directory inside the configured ThemesDir. It rejects absolute paths for non-project modules, and rejects relative paths whose cleaned/joined form escapes the ThemesDir prefix (e.g. contains `..` that climbs out). The guard prevents modules from arbitrary filesystem access via crafted import paths.

Solutions

  1. Use a proper module path (github.com/owner/repo) for cross-tree imports, or a path relative to themesDir without `..` escapes.
  2. If the module is genuinely part of the project, mark it as a project module or mount it via the project's own config rather than a sub-module.
  3. Confirm config.toml's themesDir points where you expect, then express the import relative to it.
  4. Move the target into themesDir and reference it by its name only.

Example fix

// before (in a theme's config): escapes themesDir
[[module.imports]]
path = "../../shared/assets"
// after: install under themesDir and import by name
[[module.imports]]
path = "shared-assets"
Defensive patterns

Strategy: validation

Validate before calling

// Validate import paths in a (sub)module config before Hugo loads them
import "path/filepath"

func validateThemeImportPath(themesDir, p string, isProjectMod bool) error {
  p = filepath.Clean(p)
  if !isProjectMod {
    if filepath.IsAbs(p) { return fmt.Errorf("absolute path not allowed: %s", p) }
    joined := filepath.Join(themesDir, p)
    if !strings.HasPrefix(joined, themesDir) { return fmt.Errorf("path escapes themesDir: %s", p) }
  }
  return nil
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: A module import in a theme/sub-module (isProjectMod false) whose path is absolute, or relative with `..` segments that, after filepath.Join(ThemesDir, path), no longer has ThemesDir as a prefix. Happens when a theme's own config imports a sibling via an absolute path or an escaping relative one.

Common situations: Theme author writes `path = "/home/me/theme"` or `path = "../../somewhere"` in a child module's imports; moving a project and forgetting themesDir; absolute path that worked on one OS breaks portability.

Related errors


AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09). Data as JSON: /api/errors/a9934fa8f04208ae. Report an issue: GitHub.

Appendix: source

Thrown at modules/client.go:878

	}

	if goModOnly {
		return nil
	}

	if err := c.rewriteGoMod(goSumFilename, isGoMod); err != nil {
		return err
	}

	return nil
}

func (c *Client) shouldNotVendor(path string) bool {
	return c.noVendor != nil && c.noVendor.Match(path)
}

func (c *Client) createThemeDirname(modulePath string, isProjectMod bool) (string, error) {
	invalid := fmt.Errorf("invalid module path %q; must be relative to themesDir when defined outside of the project", modulePath)

	modulePath = filepath.Clean(modulePath)
	if filepath.IsAbs(modulePath) {
		if isProjectMod {
			return modulePath, nil
		}
		return "", invalid
	}

	moduleDir := filepath.Join(c.ccfg.ThemesDir, modulePath)
	if !isProjectMod && !strings.HasPrefix(moduleDir, c.ccfg.ThemesDir) {
		return "", invalid
	}
	return moduleDir, nil
}

// ClientConfig configures the module Client.
type ClientConfig struct {

View on GitHub (pinned to 52c9bd7908)