gohugoio/hugo · error
%s: %q
Error message
%s: %q
What it means
normalizeMounts has a carve-out: if a mount source resolves to the project's PublishDir, Hugo will MkdirAll it (since it's typically gitignored). If that MkdirAll fails, the underlying fs error is wrapped. So the error is really 'could not create the publish/public directory for a mount that targets it.'
Source
Thrown at modules/collect.go:835
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) {
// A common pattern for Tailwind 3 is to mount that file to get it on the server watch list.
// A common pattern is also to add hugo_stats.json to .gitignore.
// Create an empty file.
f, err := c.fs.Create(sourceDir)
if err != nil {
return nil, fmt.Errorf("%s: %q", errMsg, err)
}
f.Close()
} else {
if isModuleNodeModulesImport {
// A module imported a path inside node_modules, but it didn't exist.
// Make this a special case and also try relative to the project root.
sourceDir = filepath.Join(c.ccfg.WorkingDir, mnt.Source)
_, err := c.fs.Stat(sourceDir)View on GitHub (pinned to 52c9bd7908)
Solutions
- Grant write access to the publishDir path (and parents) for the Hugo process user.
- If read-only by design, do not mount the publishDir, or pre-create it with correct perms.
- Check the publishDir setting in config isn't pointing somewhere unwritable.
- In containers, mount a writable volume at the publishDir.
Example fix
// before: read-only publishDir mount fails // after: pre-create with perms or use a writable volume mkdir -p public && chmod u+w public hugo
Defensive patterns
Strategy: validation
Validate before calling
// Ensure the publishDir is writable before build
import "os"
func ensurePublishDirWritable(publishDir string) error {
if err := os.MkdirAll(publishDir, 0o755); err != nil {
return fmt.Errorf("publishDir not writable: %w", err)
}
probe := filepath.Join(publishDir, ".hugowrite")
if err := os.WriteFile(probe, []byte("x"), 0o644); err != nil { return err }
return os.Remove(probe)
} Type guard
null
Try / catch
// On mount-MkdirAll failure, check publishDir perms or remove the publishDir mount.
Prevention
- Grant write access to publishDir for the Hugo process user.
- Avoid read-only filesystems when mounting publishDir.
- Pre-create publishDir in setup scripts.
When it happens
Trigger: A mount whose source equals PublishDir (e.g. mounting `public/` to observe rendered output) on a filesystem where MkdirAll fails — read-only FS, permission denied, disk full, or a parent path that exists as a file.
Common situations: Hugo running in a read-only container layer, CI workspace with restricted write permissions, or a misconfigured publishDir whose parent is a file.
Related errors
- failed to open dir %q: %q
- could not determine content directory for %q
- failed to vendor module: %w
- failed to copy module to vendor dir: %w
- failed to make target dir: %w
AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09).
Data as JSON: /api/errors/b3ee99a08c70f84d.
Report an issue: GitHub.