gohugoio/hugo · error

failed to copy to

Error message

failed to copy to %q: %w

What it means

Thrown when copying a non-resource output file (e.g. one emitted via the copy/file loader) to the publish directory fails. Either helpers.OpenFilesForWriting or io.Copy returned an error, and the printed targetFilenames show where the write failed.

Solutions

  1. Check write permissions on the publish directory (often public/).
  2. Free disk space on the volume holding the publish dir.
  3. Inspect the target path printed for invalid characters or traversal; verify each multihost base path is correct.
  4. On Windows, exclude the Hugo publish dir from anti-virus scanning.
Defensive patterns

Strategy: try-catch

Validate before calling

// Before the build, write and remove a sentinel file in the publish dir to
// confirm the process has write access.
sentinel := filepath.Join(publishDir, ".hugo-write-probe")
if err := os.WriteFile(sentinel, []byte("x"), 0o644); err != nil {
    return fmt.Errorf("publish dir not writable: %w", err)
}
_ = os.Remove(sentinel)

Try / catch

On copy failure, inspect the wrapped error for syscall.EACCES or ENOSPC and surface a targeted message (permissions vs. disk-full) instead of the generic copy error.

Prevention

When it happens

Trigger: Esbuild emits an output file that isn't recognized as a resource (no media type or not in a path group), so Hugo copies it directly to each multihost-aware target. The copy fails because the publish directory is not writable, is out of space, or the joined target path is invalid.

Common situations: publishDir (often public/) is read-only or owned by another user; CI runs Hugo as a non-privileged user; out of disk; misconfigured multihost producing invalid joined paths; on Windows an anti-virus locks the destination file.

Related errors


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

Appendix: source

Thrown at internal/js/esbuild/batch.go:716

				targetFilenames = append(targetFilenames, targetFilename)
			}

			fs := b.client.d.BaseFs.PublishFs

			if err := func() error {
				fw, err := helpers.OpenFilesForWriting(fs, targetFilenames...)
				if err != nil {
					return err
				}
				defer fw.Close()

				fr := bytes.NewReader(o.Contents)

				_, err = io.Copy(fw, fr)

				return err
			}(); err != nil {
				return nil, fmt.Errorf("failed to copy to %q: %w", targetFilenames, err)
			}
		}
	}

	p := &Package{
		id:     path.Join(NsBatch, b.id),
		b:      b,
		groups: groups,
	}

	return p, nil
}

func (b *batcher) removeNotSet() bool {
	// We already have the lock.
	var removed bool
	currentBuildID := b.buildCount
	for k, v := range b.scriptGroups {

View on GitHub (pinned to 52c9bd7908)