twpayne/chezmoi · error
%s: not a directory
Error message
%s: not a directory
What it means
When removeTargetEntry processes each target path it expects targetAbsPath to be a directory (it first Stats it and removes it as a directory). If the stat succeeds but the path is a regular file or symlink, the operation aborts with this error because removing requires the path to be a directory entry in the source state.
Source
Thrown at internal/chezmoi/sourcestate.go:967
) error {
// Remove empty directories with the remove_ attribute. This assumes that
// targetRelPaths is already sorted and iterates in reverse order so that
// children are removed before their parents.
TARGET:
for _, targetRelPath := range slices.Backward(targetRelPaths) {
if !s.removeDirs.Contains(targetRelPath) {
continue
}
// Ensure that we are attempting to remove a directory, not any other entry type.
targetAbsPath := targetDirAbsPath.Join(targetRelPath)
switch fileInfo, err := targetSystem.Stat(targetAbsPath); {
case errors.Is(err, fs.ErrNotExist):
continue TARGET
case err != nil:
return err
case !fileInfo.IsDir():
return fmt.Errorf("%s: not a directory", targetAbsPath)
}
// Attempt to remove the directory, but ignore any "not exist" or "not
// empty" errors.
switch err := targetSystem.Remove(targetAbsPath); {
case err == nil:
// Do nothing.
case errors.Is(err, fs.ErrExist):
continue TARGET
case errors.Is(err, fs.ErrNotExist):
continue TARGET
default:
return err
}
entryState := &EntryState{
Type: EntryStateTypeRemove,
}
if err := PersistentStateSet(persistentState, EntryStateBucket, targetAbsPath.Bytes(), entryState); err != nil {View on GitHub (pinned to f901167e46)
Solutions
- Reconcile the target: if it should be a directory, remove the stray file and recreate the directory ('rm path && mkdir path'), or if it should be a file, remove the stale directory entry from the source state.
- Run 'chezmoi apply --dry-run' or 'chezmoi diff' to inspect the mismatch before fixing.
- Correct the source entry type in the source directory (e.g. rename file to hello/ prefix or remove the dir entry).
Example fix
// before: source has a dir entry but target is a file $ file ~/.local/share/chezmoi/dot_config # entry thinks dir, target is file // after $ rm ~/.config/.../stale_file && mkdir -p matching/dir || adjust source entry type
Defensive patterns
Strategy: validation
Validate before calling
p="$1"
[ -d "$p" ] || { echo "$p is not a directory" >&2; exit 1; } Prevention
- Keep target filesystem types in sync with source entries
- Run 'chezmoi diff' before bulk operations
- Avoid manually swapping files for directories under management
When it happens
Trigger: Calling the remove/unmanage path (e.g. 'chezmoi forget' / removing source entries) where a target path recorded as a directory in the source state is now a plain file on the target filesystem, or vice versa mismatch.
Common situations: A file replaced a directory (or the reverse) outside of chezmoi; partial state after an interrupted apply; symlinks pointing at files where directories were expected.
Related errors
- %s: not found
- %s: not a directory
- expected a string, got a %T
- open %s: failed to acquire lock: %w
- expected a []string, got a %T
AI-assisted analysis of twpayne/chezmoi@f901167e46 (2026-09-01).
Data as JSON: /api/errors/ece5170bfa3c40e8.
Report an issue: GitHub.