can1357/oh-my-pi · error

cannot overwrite directory {0} with non-directory

Error message

cannot overwrite directory {0} with non-directory

What it means

This is MvError::DirectoryToNonDirectory in crates/pi-builtins/src/mv.rs. It is raised when the destination path already exists and is a directory, but the source is a non-directory file — so mv would have to replace a directory with a file, which the kernel rename cannot do and coreutils forbids. Triggered from `handle_two_paths` (mv.rs:609-621) when `-T/--no-target-directory` is used with a directory target, or via the equivalent branch when target exists as a dir. Note the payload is the target path, matching GNU mv's message.

Source

Thrown at crates/pi-builtins/src/mv.rs:61

use crate::host::{Host, Utility, format_usage, matches_parser, util};
#[cfg(unix)]
use self::hardlink::{
	HardlinkGroupScanner, HardlinkOptions, HardlinkTracker, create_hardlink_context,
	with_optional_hardlink_context,
};

#[derive(Debug, Error)]
enum MvError {
	#[error("cannot stat {0}: No such file or directory")]
	NoSuchFile(String),
	#[error("cannot stat {0}: Not a directory")]
	CannotStatNotADirectory(String),
	#[error("{0} and {1} are the same file")]
	SameFile(String, String),
	#[error("cannot move {0} to a subdirectory of itself, {1}")]
	SelfTargetSubdirectory(String, String),
	#[error("cannot overwrite directory {0} with non-directory")]
	DirectoryToNonDirectory(String),
	#[error("cannot overwrite non-directory {1} with directory {0}")]
	NonDirectoryToDirectory(String, String),
	#[error("target {0}: Not a directory")]
	NotADirectory(String),
	#[error("target directory {0}: Not a directory")]
	TargetNotADirectory(String),
	#[error("failed to access {0}: Not a directory")]
	FailedToAccessNotADirectory(String),
}

#[derive(Debug, Error)]
enum MvFailure {
	#[error(transparent)]
	Move(#[from] MvError),
	#[error(transparent)]
	Io(#[from] io::Error),
	#[error("{0}")]

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove or rename the existing directory first if it is truly meant to be replaced, then re-run the move.
  2. Drop the `-T` flag if the intent was to move the source INTO the directory (`mv file dir/`).
  3. Check the destination with `ls -ld dest` to confirm its type before scripting the move.
  4. If the directory should hold the file, move to `dest/basename` instead of overwriting `dest` itself.

Example fix

// before (fails: conf is a directory)
mv -T config.yaml conf

// after: move the file into the directory
mv config.yaml conf/config.yaml
// or, to truly replace the directory:
rm -rf conf && mv config.yaml conf
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from "node:fs/promises";

export async function safeMoveFileOver(src: string, dest: string): Promise<void> {
  const st = await fs.lstat(dest).catch(() => null);
  if (st?.isDirectory()) {
    throw new Error(`destination ${dest} is a directory; move into it or remove it first`);
  }
  // proceed with mv src dest (-T semantics)
}

Try / catch

try {
  await runBuiltin("mv", ["-T", src, dest]);
} catch (e) {
  if (String(e).includes("cannot overwrite directory")) {
    logger.error("destination is an existing directory", { dest });
  } else throw e;
}

Prevention

When it happens

Trigger: `mv -T somefile existing_dir` (file over an existing directory); `mv --no-target-directory file dir_that_exists`; mv invoked with a symlink that resolves to a directory as the second operand while the source is a plain file and -T is set.

Common situations: Scripts that assumed the destination was a regular file but a directory of that name was created earlier (e.g. by `mkdir -p` in setup); users typing `mv -T newfile conf` where `conf/` is a config directory; build tooling that switched between file and directory outputs across versions, leaving a stale directory in place.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/b038e8dcac41eb40. Report an issue: GitHub.