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
- Remove or rename the conflicting non-directory entry at that path in the source state (chezmoi source path)
- Decide the correct shape: if it should be a directory, delete the stale file (chezchoi managed remove) and re-add entries
- 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
- Avoid managing both a file and children under the same target path
- After renames dir<->file, clean stale entries from the chezmoi source directory
- Inspect the source tree (chezmoi source-path) when nesting changes
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
- %s: parent directory not in source state
- %s: not a directory
- %s: not found
- empty directory name
- empty filename
AI-assisted analysis of twpayne/chezmoi@f901167e46 (2026-09-01).
Data as JSON: /api/errors/ebe5e86a30f1a73f.
Report an issue: GitHub.