{"record":{"id":"a9934fa8f04208ae","repo":"gohugoio/hugo","slug":"invalid-module-path-q-must-be-relative-to-themes","errorCode":null,"errorMessage":"invalid module path %q; must be relative to themesDir when defined outside of the project","messagePattern":"invalid module path %q; must be relative to themesDir when defined outside of the project","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"modules/client.go","lineNumber":878,"sourceCode":"\t}\n\n\tif goModOnly {\n\t\treturn nil\n\t}\n\n\tif err := c.rewriteGoMod(goSumFilename, isGoMod); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) shouldNotVendor(path string) bool {\n\treturn c.noVendor != nil && c.noVendor.Match(path)\n}\n\nfunc (c *Client) createThemeDirname(modulePath string, isProjectMod bool) (string, error) {\n\tinvalid := fmt.Errorf(\"invalid module path %q; must be relative to themesDir when defined outside of the project\", modulePath)\n\n\tmodulePath = filepath.Clean(modulePath)\n\tif filepath.IsAbs(modulePath) {\n\t\tif isProjectMod {\n\t\t\treturn modulePath, nil\n\t\t}\n\t\treturn \"\", invalid\n\t}\n\n\tmoduleDir := filepath.Join(c.ccfg.ThemesDir, modulePath)\n\tif !isProjectMod && !strings.HasPrefix(moduleDir, c.ccfg.ThemesDir) {\n\t\treturn \"\", invalid\n\t}\n\treturn moduleDir, nil\n}\n\n// ClientConfig configures the module Client.\ntype ClientConfig struct {","sourceCodeStart":860,"sourceCodeEnd":896,"githubUrl":"https://github.com/gohugoio/hugo/blob/52c9bd7908b4d02d4d0ff8f82a888834d6ee10d2/modules/client.go#L860-L896","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (in a theme's config): escapes themesDir\n[[module.imports]]\npath = \"../../shared/assets\"\n// after: install under themesDir and import by name\n[[module.imports]]\npath = \"shared-assets\"","handlingStrategy":"validation","validationCode":"// Validate import paths in a (sub)module config before Hugo loads them\nimport \"path/filepath\"\n\nfunc validateThemeImportPath(themesDir, p string, isProjectMod bool) error {\n  p = filepath.Clean(p)\n  if !isProjectMod {\n    if filepath.IsAbs(p) { return fmt.Errorf(\"absolute path not allowed: %s\", p) }\n    joined := filepath.Join(themesDir, p)\n    if !strings.HasPrefix(joined, themesDir) { return fmt.Errorf(\"path escapes themesDir: %s\", p) }\n  }\n  return nil\n}","typeGuard":"null","tryCatchPattern":"null","preventionTips":["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."],"tags":["modules","config","path-validation","security"],"backgroundTag":null,"analyzedSha":"52c9bd7908b4d02d4d0ff8f82a888834d6ee10d2","analyzedAt":"2026-08-09T21:49:36.660Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}