{"record":{"id":"5b3fd8eea6b9e8b0","repo":"jdx/mise","slug":"package-plugin-state-parent","errorCode":null,"errorMessage":"package plugin state parent","messagePattern":"package plugin state parent","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/plugin.rs","lineNumber":167,"sourceCode":"                crate::file::display_path(path)\n            );\n        }\n        if state.manager != manager {\n            bail!(\n                \"package plugin ownership state {} belongs to manager '{}', not '{manager}'\",\n                crate::file::display_path(path),\n                state.manager\n            );\n        }\n        Ok(state)\n    }\n\n    fn load_state(&self) -> Result<PackagePluginState> {\n        Self::load_state_at(&self.state_path(), &self.name)\n    }\n\n    fn save_state_at(path: &std::path::Path, state: &PackagePluginState) -> Result<()> {\n        crate::file::create_dir_all(path.parent().expect(\"package plugin state parent\"))?;\n        let mut contents = serde_json::to_vec_pretty(state)?;\n        contents.push(b'\\n');\n        crate::file::write_atomic(path, contents)\n    }\n\n    fn save_state(&self, state: &PackagePluginState) -> Result<()> {\n        Self::save_state_at(&self.state_path(), state)\n    }\n\n    pub(crate) fn supports_uninstall(&self) -> bool {\n        self.plugin\n            .plugin_path\n            .join(\"hooks/package_uninstall.lua\")\n            .exists()\n    }\n\n    fn sync_lookup_path() -> Vec<PathBuf> {\n        let mut paths: Vec<PathBuf> = std::env::var_os(\"PATH\")","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/packages/plugin.rs#L149-L185","documentation":"`save_state_at` calls `path.parent().expect(\"package plugin state parent\")` before creating directories for the plugin state file. `Path::parent()` returns `None` only for root paths (`/`) or empty paths, so the panic means the plugin's state path has no parent directory — typically because the configured plugin state location is a filesystem root or empty.","triggerScenarios":"Calling save_state (src/system/packages/plugin.rs:167) when `state_path()` resolves to `/` or an empty/relative-single-component path with no directory prefix, e.g. a misconfigured state directory like `MISE_..._STATE_PATH=/` or an unset variable producing `\"\"`.","commonSituations":"Environment variable or config pointing the plugin state file at a mount root; empty path templating in config; running from a chroot with a root-level state path.","solutions":["Configure the plugin state path to a real nested file (e.g. /path/to/state/plugin.json), not a root or bare name","Log/print `state_path()` before saving to see what it resolves to","Change the code to map a missing parent to a descriptive error rather than a panic"],"exampleFix":"// before\nlet path = env_state_dir.unwrap_or_default().join(\"\"); // resolves to \"\"\n// after\nlet path = env_state_dir.unwrap_or(default_dir).join(\"plugin.json\");","handlingStrategy":"validation","validationCode":"let p = state_path();\nif p.parent().is_none() {\n    return Err(anyhow!(\"plugin state path {:?} has no parent directory\", p));\n}","typeGuard":"fn has_parent(p: &Path) -> bool { p.parent().is_some() }","tryCatchPattern":null,"preventionTips":["Configure state paths as full nested file paths, never a root or bare name","Check env vars that template the state directory for empty values","Log the resolved state path at debug level"],"tags":["panic","path","plugin-state","config"],"backgroundTag":"internal-invariant-violation","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"}