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
- Use a proper module path (github.com/owner/repo) for cross-tree imports, or a path relative to themesDir without `..` escapes.
- 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.
- Confirm config.toml's themesDir points where you expect, then express the import relative to it.
- 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
- In theme configs, only use plain names or proper module paths.
- Never put absolute or `..` paths in a sub-module's imports.
- Keep themesDir explicit in config so relative resolution is predictable.
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
- : : mount source must be a local path for modules/themes
- cannot vendor module
- could not determine content directory for
- failed to apply mounts for project
- failed to compile whitelist pattern
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)