jdx/mise · error
package plugin ' ' does not support uninstall; add…
Error message
package plugin '{}' does not support uninstall; add hooks/package_uninstall.lua What it means
A package plugin supports uninstalling packages only if it ships a `hooks/package_uninstall.lua` hook. During `apply_prune_plan`, after computing which packages to remove, mise checks `supports_uninstall()`; if the plugin lacks that hook it aborts rather than silently leaving packages installed while claiming to prune them. The ownership state has already been updated with missing-ownership reconciliation at this point.
Solutions
- Add a `hooks/package_uninstall.lua` hook to the plugin implementing package removal for its manager.
- If the plugin is intentionally install-only, remove/uninstall the packages with the underlying package manager directly instead of via mise.
- Check the plugin's directory layout — the hook must be exactly at `hooks/package_uninstall.lua` relative to the plugin root.
- Switch to a plugin variant that supports uninstall if you cannot modify the plugin.
Example fix
# plugin layout — before
myplugin/
hooks/
package_install.lua
# after
myplugin/
hooks/
package_install.lua
package_uninstall.lua # required for uninstall/prune support Defensive patterns
Strategy: validation
Validate before calling
// Check that the plugin supports uninstall before running prune/uninstall
import { existsSync } from 'node:fs';
import { join } from 'node:path';
const pluginRoot = resolvePluginRoot(pluginName); // e.g. from mise plugin list
if (!existsSync(join(pluginRoot, 'hooks', 'package_uninstall.lua'))) {
console.warn(`Plugin '${pluginName}' is install-only; uninstall its packages with the underlying manager directly`);
} Type guard
const supportsUninstall = (root) => existsSync(join(root, 'hooks', 'package_uninstall.lua'));
Try / catch
try { await misePkgPrune(plugin) } catch (e) { if (String(e).includes('does not support uninstall')) { console.error('Add hooks/package_uninstall.lua to the plugin or remove packages with the manager directly'); } else throw e; } Prevention
- When authoring a package plugin, always ship hooks/package_uninstall.lua alongside package_install.lua.
- Treat install-only plugins as one-way; document that in the plugin README before teams adopt it.
- Verify plugin directory layout after upgrades/renames — hook filenames are exact.
- Prefer plugins from the mise plugin registry that advertise uninstall support if you need prune flows.
When it happens
Trigger: Running an operation that prunes packages (`apply_prune_plan` reached with a non-empty `present` list) against a plugin that has no `hooks/package_uninstall.lua` file — e.g. `mise uninstall`/prune flows on an install-only plugin.
Common situations: Using a community or hand-written plugin that implements only package installation; an upgraded/renamed plugin directory where the uninstall hook file is absent or misnamed; calling prune/uninstall on a plugin intended to be install-only.
Related errors
- package hook did not return state for
- package hook returned invalid state ' ' for
- brew-cask: refusing to remove generic artifact outside
- cloned request builder should remain cloneable
- Consider enabling 'keep_danger_placeholders' option (to…
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/e487fba224a57868.
Report an issue: GitHub.
Appendix: source
Thrown at src/system/packages/plugin.rs:444
let before = self.installed(&requests).await?;
let present = before
.iter()
.filter_map(|status| {
Self::status_version(status).map(|version| PackageRequest {
name: status.request.name.clone(),
version: Some(version),
tap_url: None,
desired: crate::system::packages::PackageDesiredState::Present,
})
})
.collect::<Vec<_>>();
Self::reconcile_missing_ownership(&mut state, &before);
self.save_state(&state)?;
if present.is_empty() {
return Ok(0);
}
if !self.supports_uninstall() {
bail!(
"package plugin '{}' does not support uninstall; add hooks/package_uninstall.lua",
self.name
);
}
let action_result = self.uninstall_action(&present).await;
let after = self.installed(&present).await;
let after = match after {
Ok(after) => after,
Err(status_err) => {
if let Err(action_err) = action_result {
warn!(
"{}: failed to verify uninstall after error: {status_err:#}",
self.name
);
return Err(action_err);
}
return Err(status_err);View on GitHub (pinned to 533346cc37)