HMCL-dev/HMCL · error · IOException

Theme-pack file is outside the managed directory:

Error message

Theme-pack file is outside the managed directory: 

What it means

uninstall() validates that the pack's absolute normalized file lives under THEME_PACKS_DIRECTORY or USER_THEME_PACKS_DIRECTORY. If it resides anywhere else, the manager refuses to delete it and throws this IOException with the offending path, as a safety measure against removing arbitrary user files.

Solutions

  1. Install the pack first with ThemePackManager.install(Path) so it is copied into a managed directory, then uninstall that installed copy.
  2. If deleting an unmanaged pack, delete the file yourself (Files.deleteIfExists) instead of going through uninstall().
  3. Check that THEME_PACKS_DIRECTORY/USER_THEME_PACKS_DIRECTORY are where you expect (they may change between HMCL versions or portable-mode configs).

Example fix

// before
ThemePackManager.uninstall(ThemePackManager.loadInstalled(downloadedZip));
// after
InstalledThemePack installed = ThemePackManager.install(downloadedZip);
ThemePackManager.uninstall(installed);
Defensive patterns

Strategy: validation

Validate before calling

Path target = pack.file().toAbsolutePath().normalize();
Path local = ThemePackManager.THEME_PACKS_DIRECTORY.toAbsolutePath().normalize();
Path user = ThemePackManager.USER_THEME_PACKS_DIRECTORY.toAbsolutePath().normalize();
boolean managed = (target.startsWith(local) && !target.equals(local))
               || (target.startsWith(user) && !target.equals(user));
if (!managed) {
    // install the pack into a managed directory first, then uninstall that copy
    InstalledThemePack inst = ThemePackManager.install(pack.file());
}

Try / catch

try {
    ThemePackManager.uninstall(pack);
} catch (IOException e) {
    if (e.getMessage().startsWith("Theme-pack file is outside the managed directory")) {
    // fall back to deleting the file directly or install-then-uninstall
    }
}

Prevention

When it happens

Trigger: Calling uninstall() on an InstalledThemePack whose file path points outside both managed theme-pack directories — e.g. a pack in the user's home or Downloads folder that was loaded via load() without ever being installed.

Common situations: Double-clicking a theme pack downloaded from the web and attempting to uninstall it directly; loading packs from a custom directory and then calling uninstall on them; symlinked or relocated install directories making the normalized path differ from expectations.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10). Data as JSON: /api/errors/27788d673bceec19. Report an issue: GitHub.

Appendix: source

Thrown at HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManager.java:552

    public static void uninstall(InstalledThemePack themePack) throws IOException {
        Objects.requireNonNull(themePack);

        if (themePack.builtin()) {
            throw new IOException("Cannot delete a built-in theme pack: " + themePack.manifest().id());
        }

        @Nullable Path file = themePack.file();
        if (file == null) {
            throw new IOException("Theme pack does not have a local file: " + themePack.manifest().id());
        }

        Path targetFile = file.toAbsolutePath().normalize();
        Path localDirectory = THEME_PACKS_DIRECTORY.toAbsolutePath().normalize();
        Path userDirectory = USER_THEME_PACKS_DIRECTORY.toAbsolutePath().normalize();
        boolean localThemePack = targetFile.startsWith(localDirectory) && !targetFile.equals(localDirectory);
        boolean userThemePack = targetFile.startsWith(userDirectory) && !targetFile.equals(userDirectory);
        if (!localThemePack && !userThemePack) {
            throw new IOException("Theme-pack file is outside the managed directory: " + targetFile);
        }

        deleteIfExists(targetFile);

        ThemeReference reference = settings().getSelectedThemeOrDefault();
        ThemePackManifest manifest = themePack.manifest();
        if (reference.packId().equals(manifest.id())) {
            @Nullable InstalledThemePack replacementThemePack = findInstalled(reference);
            @Nullable Theme replacementTheme = replacementThemePack == null
                    ? null
                    : replacementThemePack.manifest().findTheme(reference.themeId());
            if (replacementTheme == null) {
                settings().selectedThemeProperty().set(BUILTIN_DEFAULT_THEME_REFERENCE);
            }
        }
    }

    /// Applies one theme from a loaded theme pack to current launcher settings.

View on GitHub (pinned to 24702dc5a0)