{"record":{"id":"738ec9c87cdcf9c1","repo":"jdx/mise","slug":"relative-repo-path-path-raw-must-stay-within-t","errorCode":null,"errorMessage":"relative repo path `{path_raw}` must stay within the project root (no `..` or absolute segments)","messagePattern":"relative repo path `(.+?)` must stay within the project root \\(no `\\.\\.` or absolute segments\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/repos.rs","lineNumber":100,"sourceCode":"                \"repo path `{path_raw}` cannot start with `~`; use `~/` for a home-relative path\"\n            );\n        }\n        let path = file::replace_path(&path_raw);\n        let path = if path.is_absolute() {\n            path\n        } else {\n            let Some(root) = project_root else {\n                bail!(\n                    \"relative repo paths are only allowed in a project config; use an absolute path or a `~/` path\"\n                );\n            };\n            if path.components().any(|component| {\n                matches!(\n                    component,\n                    Component::ParentDir | Component::RootDir | Component::Prefix(_)\n                )\n            }) {\n                bail!(\n                    \"relative repo path `{path_raw}` must stay within the project root (no `..` or absolute segments)\"\n                );\n            }\n            if !path\n                .components()\n                .any(|component| matches!(component, Component::Normal(_)))\n            {\n                bail!(\n                    \"relative repo path `{path_raw}` must name a directory inside the project root\"\n                );\n            }\n            // Join only the Normal segments so `./foobar` resolves to\n            // `<root>/foobar` rather than `<root>/./foobar` — a `.` component\n            // survives `Path::join` and leaks into every displayed path.\n            let mut resolved = root.to_path_buf();\n            for component in path.components() {\n                if let Component::Normal(segment) = component {\n                    resolved.push(segment);","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/repos.rs#L82-L118","documentation":"RepoRequest::from_toml validates `[bootstrap.repos]` entries. When a repo key is a relative path, the expanded path must resolve inside the project root: any `..`, root, or platform-prefix component is rejected before the path is joined to the root. This blocks entries from pointing outside the project via traversal.","triggerScenarios":"Defining a bootstrap repo key like `\"../shared/repo\" = { url = ... }` in a project mise.toml; the relative path expands to a path containing a ParentDir component.","commonSituations":"Copying a repo entry from a global config into a project config without making it `~/`-relative or absolute; typos like `..\\tools` on Windows; intentionally trying to check out a repo that lives in a sibling directory of the project.","solutions":["Change the key to an absolute path or a `~/`-relative path (e.g. `\"~/src/dotfiles\"`), which bypasses the project-root containment check","Move the target repo inside the project directory and use a plain relative key like `\"tools/repo\"`","Remove `..` segments from the key so it stays within the project root"],"exampleFix":"// before\n[bootstrap.repos]\n\"../dotfiles\" = { url = \"https://github.com/jdx/dotfiles.git\" }\n// after\n[bootstrap.repos]\n\"~/src/dotfiles\" = { url = \"https://github.com/jdx/dotfiles.git\" }","handlingStrategy":"validation","validationCode":"fn is_safe_relative_repo_path(p: &str) -> bool {\n    let path = std::path::Path::new(p);\n    !(path.is_absolute())\n        && !path.components().any(|c| matches!(c, std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_)))\n}\n// assert!(is_safe_relative_repo_path(\"tools/repo\"))","typeGuard":"fn is_project_relative(p: &str) -> Option<&str> {\n    let path = std::path::Path::new(p);\n    if path.is_absolute() || p.starts_with('~') { return None; }\n    if path.components().any(|c| !matches!(c, std::path::Component::Normal(_) | std::path::Component::CurDir)) { return None; }\n    Some(p)\n}","tryCatchPattern":null,"preventionTips":["Use `~/`-relative keys for repos outside the project; keep relative keys strictly inside the project tree","Never use `..` in repo path keys; restructure the layout instead","Test mise.toml parsing after moving entries between global and project configs"],"tags":["config","path-traversal","git","bootstrap"],"backgroundTag":"path-traversal-blocked","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}