jdx/mise · error · eyre::Report

mode symlink-each requires the source to be a directory

Error message

mode symlink-each requires the source to be a directory

What it means

`symlink-each` only supports directory sources: mise recreates the source tree under the target and symlinks each contained file individually. This terminal match arm fires when a symlink-each entry is planned and its source is not a directory — in practice on Windows (the earlier non-Windows arm intercepts first) or when `source` names a regular file or is missing.

Source

Thrown at src/system/files.rs:1599

                bail!("source directory is missing, so managed children cannot be identified");
            }
            plan_single_file(req, opts, &mut paths)?;
        }
        FileMode::Content => {
            if !req.target.exists() && !req.target.is_symlink() {
                return Ok(None);
            }
            if opts.force {
                paths.insert(req.target.clone(), ());
            } else {
                plan_inline_file(req, &mut paths)?;
            }
        }
        FileMode::Symlink => {
            plan_single_file(req, opts, &mut paths)?;
        }
        FileMode::SymlinkEach => {
            bail!("mode symlink-each requires the source to be a directory");
        }
        FileMode::Template => {
            if !req.target.exists() && !req.target.is_symlink() {
                return Ok(None);
            }
            if opts.force {
                paths.insert(req.target.clone(), ());
            } else if opts.dry_run {
                // Rendering may execute commands. Keep dry-run inert, matching
                // apply/status policy, and describe the removal as conditional.
                paths.insert(req.target.clone(), ());
                conditional = true;
            } else {
                if !req.source.exists() {
                    bail!("source is missing; use --force to remove the target");
                }
                // Rendering may execute user-authored commands. Defer it until
                // after the complete unapply plan has been confirmed.

View on GitHub (pinned to 6f52dcdf99)

Solutions

  1. Point `source` at the directory containing the files to link
  2. For single files, use `mode = "symlink"` or `mode = "copy"` instead
  3. Remove the entry if it is obsolete

Example fix

# mise.toml - before: symlink-each on a single file
[[dotfiles]]
source = "~/dotfiles/ssh/config"
target = "~/.ssh/config"
mode = "symlink-each"

# after: link the single file directly
[[dotfiles]]
source = "~/dotfiles/ssh/config"
target = "~/.ssh/config"
mode = "symlink"

# or link every file in the directory:
# source = "~/dotfiles/ssh", target = "~/.ssh", mode = "symlink-each"
Defensive patterns

Strategy: validation

Validate before calling

# Lint: every symlink-each entry's source must be a directory.
python3 - <<'EOF'
import tomllib, pathlib
for f in pathlib.Path('.').rglob('mise.toml'):
    for e in tomllib.loads(f.read_text()).get('dotfiles', []):
        if e.get('mode') == 'symlink-each':
            s = pathlib.Path(e['source']).expanduser()
            assert s.is_dir(), f"{f}: symlink-each source not a dir: {s}"
EOF

Type guard

fn is_symlink_each_bad_source(err: &miette::Report) -> bool {
    err.to_string().contains(
        "mode symlink-each requires the source to be a directory",
    )
}

Prevention

When it happens

Trigger: A `[dotfiles]` entry with `mode = "symlink-each"` whose `source` is a file or does not exist, planned on Windows (`cfg!(windows)`), because on Unix the `FileMode::SymlinkEach if !cfg!(windows)` arm matches first.

Common situations: Copy-pasted entry pointing at a single file instead of its parent directory; source dir renamed; a cross-platform mise.toml used on Windows where the guard sends planning to this arm.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22). Data as JSON: /api/errors/40369e65825d9dcd. Report an issue: GitHub.