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

  1. Grant write access to the publishDir path (and parents) for the Hugo process user.
  2. If read-only by design, do not mount the publishDir, or pre-create it with correct perms.
  3. Check the publishDir setting in config isn't pointing somewhere unwritable.
  4. 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

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


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