{"record":{"id":"c59bdb7c9b28f7dc","repo":"jdx/mise","slug":"managed-file-paths-previous-and-path-norma","errorCode":null,"errorMessage":"managed file paths '{previous}' and '{path}' normalize to the same target '{}'","messagePattern":"managed file paths '(.+?)' and '(.+?)' normalize to the same target '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/managed_files.rs","lineNumber":349,"sourceCode":"fn merged_files_from_config(\n    config: &Config,\n) -> Result<IndexMap<PathBuf, (ManagedFileTomlConfig, PathBuf)>> {\n    let mut merged: IndexMap<PathBuf, (ManagedFileTomlConfig, PathBuf)> = IndexMap::new();\n    // Config files are ordered from highest to lowest precedence. Preserve the\n    // first declaration of a target so a parent or global layer cannot replace\n    // the nearer project declaration.\n    for cf in config.config_files.values() {\n        if let Some(bootstrap) = cf.bootstrap_config() {\n            let mut layer_paths = IndexMap::new();\n            let base = cf\n                .get_path()\n                .parent()\n                .unwrap_or_else(|| Path::new(\".\"))\n                .to_path_buf();\n            for (path, file) in bootstrap.files {\n                let target = absolute_target(&path)?;\n                if let Some(previous) = layer_paths.insert(target.clone(), path.clone()) {\n                    bail!(\n                        \"managed file paths '{previous}' and '{path}' normalize to the same target '{}'\",\n                        target.display()\n                    );\n                }\n                merged.entry(target).or_insert_with(|| (file, base.clone()));\n            }\n        }\n    }\n    Ok(merged)\n}\n\nfn directories_from_config(config: &Config) -> Result<Vec<ManagedDirectoryRequest>> {\n    let mut merged: IndexMap<PathBuf, ManagedDirectoryTomlConfig> = IndexMap::new();\n    for cf in config.config_files.values() {\n        if let Some(bootstrap) = cf.bootstrap_config() {\n            let mut layer_paths = IndexMap::new();\n            for (path, directory) in bootstrap.directories {\n                let target = absolute_target(&path)?;","sourceCodeStart":331,"sourceCodeEnd":367,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/managed_files.rs#L331-L367","documentation":"While merging `[bootstrap.files]` across config layers, two entries within the SAME config layer produced the same absolute normalized target path. `absolute_target()` canonicalizes the declared path (resolving relative paths and normalization), so distinct-looking keys like \"etc/app.conf\" and \"./etc/app.conf\" — or \"etc/app.conf\" plus a trailing-slash/dot-segment variant — collapse to one target; the per-layer `layer_paths` IndexMap detects the collision (managed_files.rs:344-351). Across different layers, later declarations override earlier ones by design; only same-layer duplicates error.","triggerScenarios":"One mise.toml containing e.g. `[bootstrap.files.\"/etc/app.conf\"]` and `[bootstrap.files.\"/etc/./app.conf\"]`, or relative variants like `files.cfg` and `./files.cfg` that both resolve against the config file's parent dir to the same absolute path. TOML duplicate keys that are literally identical are rejected by the TOML parser itself, so this error is specifically about paths that differ as strings but normalize identically.","commonSituations":"Appending a file entry generated by a script that sometimes prefixes `./`; mixing absolute and relative spellings of the same path; normalizing differences introduced by copying config between projects; symlinky path spellings like `/var/../etc/x`.","solutions":["Deduplicate the two entries — delete one; they manage the same file, so one fully-specified entry is enough","Pick a single spelling convention (absolute paths, or relative to the config file) for the whole table","If both entries were intentional because you wanted different states per layer, move them into different config layers (e.g. project mise.toml vs ~/.config/mise/mise.toml) where merge-by-override applies instead of erroring"],"exampleFix":"# before (same file, two spellings, one layer)\n[bootstrap.files.\"/etc/app.conf\"]\ncontent = \"a\"\n[bootstrap.files.\"/etc/./app.conf\"]\ncontent = \"b\"\n\n# after\n[bootstrap.files.\"/etc/app.conf\"]\ncontent = \"a\"","handlingStrategy":"validation","validationCode":"// Same-layer duplicate check before mise sees it (per config file)\nuse std::collections::HashSet;\nuse std::path::{Path, Component};\nfn norm(p: &Path) -> Path { p.components().collect::<Vec<Component>>().into_iter().collect() }\nlet mut seen = HashSet::new();\nfor key in bootstrap_files_keys(config_file) {\n    let t = absolute(base_dir, norm(&key));\n    assert!(seen.insert(t), \"duplicate file target in this config file\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use absolute paths consistently in [bootstrap.files] so duplicates are visually obvious","Never mix './x' and 'x' spellings in the same table","Lint generated config with a dedupe-on-normalized-path script before committing"],"tags":["bootstrap","config-validation","path-normalization","duplicate-key","managed-files"],"backgroundTag":"path-normalization-collision","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}