twpayne/chezmoi · error

%s: not a directory

Error message

%s: not a directory

What it means

While building the source-state tree node-by-node, chezmoi expects each intermediate component to be a SourceStateDir. If an existing entry at that path is not a directory (e.g. a file where a directory is needed), it errors '<path>: not a directory'.

Source

Thrown at internal/chezmoi/sourcestatetreenode.go:137

			dirAttr := DirAttr{
				TargetName: componentRelPath.String(),
			}
			targetStateDir := &TargetStateDir{
				perm: dirAttr.perm() &^ umask,
			}
			sourceRelPath = sourceRelPath.Join(NewSourceRelPath(dirAttr.SourceName()))
			sourceStateDir = &SourceStateDir{
				attr:             dirAttr,
				origin:           origin,
				sourceRelPath:    sourceRelPath,
				targetStateEntry: targetStateDir,
			}
			node.SourceStateEntry = sourceStateDir
		} else {
			var ok bool
			sourceStateDir, ok = node.SourceStateEntry.(*SourceStateDir)
			if !ok {
				return nil, fmt.Errorf("%s: not a directory", componentRelPaths[0].Join(componentRelPaths[1:i+1]...))
			}
			sourceRelPath = sourceRelPath.Join(NewSourceRelPath(sourceStateDir.attr.SourceName()))
		}
	}
	return sourceStateDir, nil
}

// Set sets the SourceStateEntry at relPath to sourceStateEntry.
func (n *SourceStateEntryTreeNode) Set(targetRelPath RelPath, sourceStateEntry SourceStateEntry) {
	if targetRelPath.IsEmpty() {
		n.SourceStateEntry = sourceStateEntry
		return
	}

	node := n
	for _, childRelPath := range targetRelPath.SplitAll() {
		if node.Children == nil {
			node.Children = make(map[RelPath]*SourceStateEntryTreeNode)

View on GitHub (pinned to f901167e46)

Solutions

  1. Remove or rename the conflicting non-directory entry at that path in the source state (chezmoi source path)
  2. Decide the correct shape: if it should be a directory, delete the stale file (chezchoi managed remove) and re-add entries
  3. Run `chezmoi doctor` / inspect the source directory to find the file occupying the component path

Example fix

// before: source has file 'dot_config' and you add dot_config/nvim/init.lua
rm ~/.local/share/chezmoi/dot_config
// after: recreate as directory then add entries
chezmoi add ~/.config/nvim/init.lua
Defensive patterns

Strategy: validation

Validate before calling

// before adding nested target a/b/c, ensure source components can be directories
// (no file already occupies a or a/b in the source state)
for _, p := range []string{"dot_config", "dot_config/nvim"} {
  fi, err := os.Lstat(filepath.Join(sourceDir, p))
  if err == nil && !fi.IsDir() {
    return fmt.Errorf("source path %s is a file but must be a directory", p)
  }
}

Type guard

func isDirAt(root, rel string) bool {
  fi, err := os.Stat(filepath.Join(root, rel))
  return err == nil && fi.IsDir()
}

Try / catch

if err := apply(); err != nil {
  if strings.HasSuffix(err.Error(), ": not a directory") {
    return fmt.Errorf("file/dir conflict in source state; inspect source tree: %w", err)
  }
  return err
}

Prevention

When it happens

Trigger: Source state contains a regular file/symlink at a path that must be traversed as a directory, e.g. adding target a/b/c when source already has a file named a or a/b; name collision between a file and required parent directory.

Common situations: A file and directory target the same nested path (e.g. ~/.config exists as a file); rename from dir to file (or vice versa) in the source repo without cleaning up; mis-typed target paths causing a component clash.

Related errors


AI-assisted analysis of twpayne/chezmoi@f901167e46 (2026-09-01). Data as JSON: /api/errors/ebe5e86a30f1a73f. Report an issue: GitHub.