jdx/mise · error · eyre::Report

[dotfiles]: duplicate OCI path {path:?} as both file and dir

Error message

[dotfiles]: duplicate OCI path {path:?} as both file and directory

What it means

While assembling the dotfiles layer, mise tracks paths as either files or directories (DotfilesLayerEntries). A path previously registered as a directory cannot later be registered as a file — this error fires when the exact same OCI path is added both ways, e.g. one entry contributes a directory at path X and another entry contributes a file at path X. Building a tar layer with a path as both types would be ambiguous and invalid.

Source

Thrown at src/oci/builder.rs:986

        )?;
    }
    Ok(())
}

#[derive(Default)]
struct DotfilesLayerEntries {
    files: IndexMap<String, (Vec<u8>, u32)>,
    dirs: IndexSet<String>,
}

type DotfilesLayerFile = (String, Vec<u8>, u32);
type DotfilesLayerFiles = Vec<DotfilesLayerFile>;
type DotfilesLayerDirs = Vec<String>;

impl DotfilesLayerEntries {
    fn add_file(&mut self, path: String, contents: Vec<u8>, mode: u32) -> Result<()> {
        if self.dirs.contains(&path) {
            bail!("[dotfiles]: duplicate OCI path {path:?} as both file and directory");
        }
        if let Some((existing_contents, existing_mode)) = self.files.get(&path) {
            if existing_contents != &contents || *existing_mode != mode {
                bail!("[dotfiles]: duplicate OCI file path {path:?}");
            }
            return Ok(());
        }
        self.files.insert(path, (contents, mode));
        Ok(())
    }

    fn add_dir(&mut self, path: String) -> Result<()> {
        if self.files.contains_key(&path) {
            bail!("[dotfiles]: duplicate OCI path {path:?} as both file and directory");
        }
        self.dirs.insert(path);
        Ok(())
    }

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Find the two entries whose target paths normalize to the same value and rename one of them
  2. If a directory and file genuinely share a name, move the file under the directory (e.g. `~/.config/x` vs `~/.config`)
  3. Remove the stale entry left over from restructuring your dotfiles config

Example fix

# before (mise.toml)
[oci.dotfiles]
"~/.config" = { source = "./config", mode = "copy" }       # directory
"~/.config" = { mode = "content", content = "key=1\n" }  # file — collision

# after (mise.toml)
[oci.dotfiles]
"~/.config" = { source = "./config", mode = "copy" }
"~/.config/override.conf" = { mode = "content", content = "key=1\n" }
Defensive patterns

Strategy: validation

Validate before calling

python3 - <<'EOF'
import tomllib, sys
cfg = tomllib.load(open('mise.toml', 'rb'))
paths = {}
for target, spec in (cfg.get('oci', {}).get('dotfiles', {}) or {}).items():
    norm = '/' + target.strip('/').replace('~', 'root', 1) if target.startswith('~') else target
    kind = 'file' if isinstance(spec, dict) and spec.get('mode') == 'content' else 'dir-or-file'
    if paths.setdefault(norm, kind) != kind:
        sys.exit(f'duplicate target {norm!r} with conflicting kinds')
print('dotfile targets OK')
EOF

Prevention

When it happens

Trigger: Two `[oci.dotfiles]` entries whose normalized targets collide: e.g. a symlink-each/copy entry creating directory `~/config` while another entry writes file `~/config`; or a directory-valued entry (its dir path registered via add_dir) colliding with a file entry of the same name.

Common situations: Overlapping dotfile globs or targets like `"~/.config" = {source = "./config", mode = "copy"}` and `"~/.config" = {mode = "content", content = "..."}`; restructuring dotfiles so a former file target becomes a directory target in a stale entry.

Related errors


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/0288da38f2f29dde. Report an issue: GitHub.