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
- Point `source` at the directory containing the files to link
- For single files, use `mode = "symlink"` or `mode = "copy"` instead
- 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
- Reserve `symlink-each` for directories; use `symlink` for single files
- Lint mise.toml in CI after editing dotfile entries
- Test cross-platform configs on both OSes before committing
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
- {raw}: multiple [dotfiles] edit entries match; choose one of
- target is not the managed directory, use --force to remove i
- cannot verify {}; use --force to remove it
- trimSuffix requires exactly 2 arguments
- {raw}: target must be absolute or start with ~/
AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22).
Data as JSON: /api/errors/40369e65825d9dcd.
Report an issue: GitHub.