Skip to content

Add Disable folder thumbnails mod - #5425

Open
Anixx wants to merge 4 commits into
ramensoftware:mainfrom
Anixx:patch-804941
Open

Add Disable folder thumbnails mod#5425
Anixx wants to merge 4 commits into
ramensoftware:mainfrom
Anixx:patch-804941

Conversation

@Anixx

@Anixx Anixx commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Changelog

If this pull request updates an existing mod, describe the changes below:

  • Changelog item 1...
  • Changelog item 2...

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • The submitter, without AI assistance
    • The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • Other (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Sep 9, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Thanks for the pull request! This repository uses a two-stage review: an AI review that you run yourself, followed by a human review.

To get started, comment /ai-review. Once you're happy with the result, comment /ready-for-reviewer to hand it over to a human reviewer.

See the pull request review process for the full details.

@Anixx

Anixx commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 9, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Something went wrong while preparing the AI review, so there is nothing to post this time. This is a problem on the reviewer's side, not with this pull request.

Comment /ai-review to try again.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Sep 9, 2026
@Anixx

Anixx commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 9, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The classification logic itself is sound (the FAILED(hr) check correctly tolerates S_FALSE, the COM method signature and the self-as-first-parameter STDMETHODCALLTYPE form are right, and out-params are nulled before returning failure). The problems are all in how the hook target is obtained.

1. Don't activate a COM object in Wh_ModInit — resolve the target with HookSymbols instead

When the mod is loaded at process startup, Wh_ModInit runs on Explorer's initial thread, before the process begins executing (Mod lifetime). Doing CoInitializeEx(MTA)CoCreateInstance(CLSID_LocalThumbnailCache)CoUninitialize() there means running COM activation and in-proc class-factory initialization on Explorer's main thread before the process has set up its own apartment. In a critical system process that's a stability risk, and it creates a silent failure mode: if CoCreateInstance fails that early, Wh_ModInit returns FALSE and the mod does nothing at all until the next settings change — the mod looks enabled but has no effect.

It also creates the exact hazard the pin in item 2 was added to paper over: the trailing CoUninitialize(), when it's the last uninitialize for the apartment, runs CoFreeAllLibraries, which can unload in-proc servers — and the hook is applied only after Wh_ModInit returns, so target would already point at unmapped memory.

All of this disappears with a plain symbol hook against the module that implements the thumbnail cache (windows.storage.dll on current Windows). Use the Windhawk Symbol Helper to get the exact decorated name, then:

// windows.storage.dll
static const WindhawkUtils::SYMBOL_HOOK hooks[] = {
    {
        {L"public: virtual long __cdecl CLocalThumbnailCache::GetThumbnail(...)"},
        &g_originalGetThumbnail,
        GetThumbnail_Hook,
    },
};

BOOL Wh_ModInit() {
    HMODULE storage = GetModuleHandleW(L"windows.storage.dll");
    if (!storage) {
        return FALSE;
    }
    return WindhawkUtils::HookSymbols(storage, hooks, ARRAYSIZE(hooks));
}

folder-thumbnail-style-switcher.wh.cpp#L978-L1035 is the same target module and shows the pattern. Note that mod hooks CFolderThumbnail — the folder-specific thumbnail extractor. That is very likely a better hook point for you than IThumbnailCache::GetThumbnail: it is reached only for folders, so you would not need to intercept every file thumbnail request and classify it with IShellItem::GetAttributes, and file thumbnails stay untouched by construction. Worth evaluating before settling on the cache-level hook.

2. GET_MODULE_HANDLE_EX_FLAG_PIN is an irreversible process change

BOOL pinned = GetModuleHandleExW(
    GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
        GET_MODULE_HANDLE_EX_FLAG_PIN, ...);

A pin can never be undone. Once the mod has run, the module stays loaded for the lifetime of the process even after the user disables or uninstalls the mod — a mod's effects have to disappear when it's disabled. Pinning also makes the mod's own success depend on it: if GetModuleHandleExW fails, hooked stays FALSE and Wh_ModInit returns FALSE, so the mod does nothing.

With item 1 applied there is nothing to pin — GetModuleHandleW doesn't take a reference and windows.storage.dll is a hard dependency of explorer.exe, so it is never unloaded. Drop the pin. If you ever do need to keep a module alive, take a normal reference (GetModuleHandleExW without PIN, or LoadLibraryExW(L"...", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)) and FreeLibrary it in Wh_ModUninit, so it's reversible.

3. Substantial overlap with folder-thumbnail-style-switcher

folder-thumbnail-style-switcher is about Explorer folder thumbnails, targets the same module, uses the same @include explorer.exe, and already exposes a thumbnailStyle dropdown (win7 / win10 / win11). "No folder thumbnails" reads naturally as a fourth option in that dropdown rather than a separate mod. The maintainer's stated preference is to extend an existing mod over merging a near-neighbour, since duplicates fragment the catalog.

The functional difference is real (suppressing vs. restyling), so this isn't automatically a blocker — but please state in the PR why it should be standalone, and consider opening an issue/PR on that mod proposing a "None" style instead.

4. Verify the mod takes effect (and reverts) without an Explorer restart

The README instructs users to enable the mod and then restart Explorer via Task Manager. That shouldn't be necessary: Wh_SetFunctionHook patches the implementation code, not a vtable slot on one instance, so the hook is live for every caller the moment Wh_ModInit returns — including when the mod is injected into a running Explorer. If a restart really is required, that points at something else:

  • Explorer may be serving already-cached folder thumbnails via IThumbnailCache::GetThumbnailByID (vtable slot 4), which this mod doesn't hook, so the hook is bypassed entirely for warm entries; or
  • the mod isn't hooking Explorer's actual path.

Please check which it is. Note the restart instruction also has a reversibility angle — if enabling needs a restart, disabling presumably does too, and a mod's effect should come and go with the toggle.

5. Add a screenshot to the README

The effect is purely visual (folder thumbnails replaced by plain folder icons). A before/after screenshot makes the mod's purpose immediately clear on windhawk.net. Only i.imgur.com and raw.githubusercontent.com are allowed as image hosts.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Use the type-safe helper instead of raw Wh_SetFunctionHook with void* casts, so a signature or calling-convention mismatch becomes a compile error:

    #include <windhawk_utils.h>
    
    auto target = reinterpret_cast<GetThumbnail_t>(vtable[3]);
    WindhawkUtils::SetFunctionHook(target, GetThumbnail_Hook, &g_originalGetThumbnail);

    (Moot if you switch to HookSymbols, which is type-safe already.)

  • If the COM path stays, use CLSID_LocalThumbnailCache from <thumbcache.h> (with <initguid.h>) rather than the hand-written GUID literal.

  • If the COM path goes away, drop -lole32 -luuid from @compilerOptions and the now-unused <shobjidl.h> include along with it.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • GetThumbnailByID is not covered. Only slot 3 (GetThumbnail) is hooked. Callers that already hold a WTS_THUMBNAILID can fetch a cached folder thumbnail through slot 4 and never reach your hook. Worth checking whether Explorer does this in practice — see item 4 above.

  • Every thumbnail request pays an extra shell round-trip. IsFilesystemFolder runs IShellItem::GetAttributes on all items, including files. For plain filesystem items that's served from the PIDL and is cheap, but for namespace extensions and cloud-file providers it can be a real call. Hooking CFolderThumbnail instead avoids the classification step entirely.

  • Failure results aren't cached. Returning WTS_E_FAILEDEXTRACTION before reaching the real implementation means no "no thumbnail" record is ever written to the thumbnail cache, so Explorer re-enters the hook on every request. The hook is cheap so this is probably fine, but it's worth confirming the shell view falls back to the icon cleanly and doesn't visibly retry.

  • Scope is explorer.exe only, so folder thumbnails still appear in other apps' file open/save dialogs, which host the same shell views. That matches what folder-thumbnail-style-switcher does, but it's worth a line in the README's Limitations section.

  • Check the edge cases of the classification mask. SFGAO_FOLDER | SFGAO_FILESYSTEM without SFGAO_STREAM also matches drive roots and, depending on the shell version, library items (.library-ms). Confirm that's the intended behavior for those.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Sep 9, 2026
@Anixx

Anixx commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

/ready-for-reviewer

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-reviewer Ready for a human reviewer, and in the queue for one. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 9, 2026
@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-reviewer Ready for a human reviewer, and in the queue for one. labels Sep 10, 2026
@windhawk-reviewer

Copy link
Copy Markdown

New commits were pushed, so this pull request left the human review queue and is back to waiting-for-author.

Comment /ai-review to get an AI review of the updated code, then /ready-for-reviewer to hand it over to a human reviewer again. See the pull request review process for details.

Updated author information and added GitHub link.
@Anixx

Anixx commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 10, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The overall approach (intercept IThumbnailCache::GetThumbnail and reject folder items) is sound and the COM plumbing is leak-free, but the apartment choice at init and the per-call shortcut resolution both need work.

1. CoInitializeEx(nullptr, COINIT_MULTITHREADED) in Wh_ModInit can make the mod hook a COM proxy instead of the real implementation.

Wh_ModInit runs on the target process's main thread when the mod is loaded before the process starts (Mod lifetime). Two problems with using MTA there:

  • Most in-proc shell classes are registered with ThreadingModel=Apartment. When such a class is created from an MTA thread, COM hosts the object in a separate STA and hands back a marshalled proxy. *reinterpret_cast<void***>(cache) would then be the proxy's vtable, so vtable[3] is a stub inside combase.dll — the mod would pin combase.dll, hook the proxy stub, return TRUE, and silently do nothing. (This may well be what the "restart Explorer" step in the README is papering over.)
  • It creates and tears down the process-wide MTA on Explorer's main thread before Explorer has executed any of its own startup code, which spins up MTA/RPC infrastructure very early for no reason.

Use STA, as the merged mods that do the same vtable trick do — explorer-rename-overwrite.wh.cpp#L451 initializes in Wh_ModInit directly:

HRESULT initHr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

or, to leave the host's main-thread apartment completely untouched, resolve the vtable on a short-lived dedicated thread as copy-queue.wh.cpp#L98-L125 does.

Either way, it's worth sanity-checking that the resolved address really belongs to the expected module before hooking, so a proxy/interception by something else fails loudly instead of silently:

WCHAR moduleName[MAX_PATH];
if (!GetModuleFileNameW(implementationModule, moduleName, ARRAYSIZE(moduleName)) ||
    !PathMatchSpecW(moduleName, L"*\\windows.storage.dll")) {
    Wh_Log(L"Unexpected GetThumbnail implementation module: %s", moduleName);
    return FALSE;
}

2. The shortcut branch does a full .lnk disk load on every single thumbnail request, and can block on unreachable targets.

For every IShellItem that carries SFGAO_LINK, IsFilesystemFolder does CoCreateInstance(CLSID_ShellLink)IPersistFile::Load (reads and parses the .lnk from disk) → IShellLink::GetIDListSHCreateItemFromIDListGetAttributes. Nothing is cached, so browsing a shortcut-heavy folder (Start Menu, Desktop, Recent) repeats all of that per item, per request. Worse, SHCreateItemFromIDList/GetAttributes on a target that lives on a disconnected network share can block for many seconds. There's also a silent-failure path: if the calling thread happens not to have COM initialized, CoCreateInstance returns CO_E_NOTINITIALIZED and shortcuts are simply never suppressed.

The shell already has a one-call API for this — BHID_LinkTargetItem binds a link item to its target and replaces the entire IShellLink/IPersistFile block:

IShellItem* targetItem = nullptr;
if (SUCCEEDED(item->BindToHandler(nullptr, BHID_LinkTargetItem, IID_PPV_ARGS(&targetItem)))) {
    bool targetIsFolder = IsPlainFilesystemFolder(targetItem);
    targetItem->Release();
    return targetIsFolder;
}
return false;

Better still, check whether target resolution is needed at all: the shell reports SFGAO_FOLDER on the link item itself for shortcuts that point at folders, so (attributes & SFGAO_FOLDER) && (attributes & SFGAO_LINK) may already be sufficient and costs nothing beyond the GetAttributes call you're making anyway. Worth testing before keeping the expensive path.

3. The README has no screenshot.

This is a mod with a purely visual effect, so a before/after screenshot of a folder view (thumbnails vs. plain icons) makes it much easier for users to tell what they're getting. Only i.imgur.com and raw.githubusercontent.com are allowed image hosts.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Pinning the implementation module is probably unnecessary, and shouldn't gate the hook. CLSID_LocalThumbnailCache lives in windows.storage.dll, which explorer.exe loads permanently and never unloads, so the "COM could unload it" scenario the comment describes can't happen here. (Compare explorer-folder-hover-menu.wh.cpp#L2490-L2506, which pins because it hooks genuinely unloadable shell-extension DLLs like zipfldr.dll.) GET_MODULE_HANDLE_EX_FLAG_PIN is also permanent — it isn't undone when the mod is disabled, which is a small dent in Windhawk's reversibility principle. If you keep it, at least don't make Wh_SetFunctionHook conditional on it succeeding; a failed pin currently makes the whole mod a no-op.

  • Use WindhawkUtils::SetFunctionHook instead of raw Wh_SetFunctionHook — it's type-checked and drops the reinterpret_cast<void*> pairs. Add #include <windhawk_utils.h> and:

    auto target = reinterpret_cast<GetThumbnail_t>(vtable[3]);
    BOOL hooked = WindhawkUtils::SetFunctionHook(target, GetThumbnail_Hook, &g_originalGetThumbnail);
  • Two GetAttributes round-trips per link item. IsPlainFilesystemFolder already queries SFGAO_LINK as part of its mask, then IsFilesystemFolder queries it again. Query once with the full mask in IsFilesystemFolder and pass the result down.

  • Hand-rolled CLSID. If mingw's thumbcache.h declares the class (it does in the Windows SDK), __uuidof(LocalThumbnailCache) or CLSID_LocalThumbnailCache (you're already linking -luuid) removes the need for the manual GUID literal.

  • Stray double blank line before return hooked;.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • The "restart Explorer" instruction deserves an explanation, or may not be needed. Windhawk injects into a running explorer.exe and the hook takes effect immediately, so a restart shouldn't be required for the hook itself — only to refresh folder views that are already showing thumbnails. If a restart genuinely is required for the mod to work at all, that's a symptom worth chasing (see item 1). Either way, saying why in the README helps.

  • A more targeted hook point exists. CFolderThumbnail in windows.storage.dll is the folder-specific thumbnail generator — the existing folder-thumbnail-style-switcher.wh.cpp#L959-L990 hooks CFolderThumbnail::Extract / CFolderThumbnail::_GetThumbnails by symbol. Hooking there would remove the need to inspect every IShellItem (files included) and the whole shortcut-resolution problem, since only folders reach that code. The tradeoff is that it wouldn't reject already cached folder thumbnails, which your current interception point does — so it's a genuine design choice, just worth being aware of.

  • Scope is explorer.exe only. Open/Save dialogs and other processes that host shell views will still show folder thumbnails. That's the right default (broad injection is a real cost), but it's worth listing under Limitations in the README.

  • Shortcut handling was added mid-PR; since it's a distinct behavior (a shortcut to a folder is still a .lnk file), a setting to turn it off separately might be worth offering.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Sep 10, 2026
@Anixx

Anixx commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

/ready-for-reviewer

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Sep 10, 2026
@windhawk-reviewer

Copy link
Copy Markdown

New commits were pushed, so this pull request left the human review queue and is back to waiting-for-author.

Comment /ai-review to get an AI review of the updated code, then /ready-for-reviewer to hand it over to a human reviewer again. See the pull request review process for details.

@windhawk-reviewer windhawk-reviewer Bot removed the waiting-for-reviewer Ready for a human reviewer, and in the queue for one. label Sep 10, 2026
@Anixx

Anixx commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 10, 2026
@windhawk-reviewer

Copy link
Copy Markdown

This pull request has already had 3 AI reviews in the last 24 hours, which is the limit, so no review was posted this time.

Comment /ai-review again after 2026-09-10 18:38 UTC (in 8 hours) to get another one.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Sep 10, 2026
@Anixx

Anixx commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

@m417z I only had 2 reviews. Why it says I had 3?

@m417z

m417z commented Sep 10, 2026

Copy link
Copy Markdown
Member

Because of the error 16 hours ago. I started a review for you.

@m417z m417z added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 10, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The idea is useful and the filtering logic (excluding SFGAO_STREAM/SFGAO_LINK, avoiding IShellLink::Resolve) is thoughtful. The main concerns are the way the hook target is obtained — COM activation plus a pinned module in every process on the system — and an existing mod that already offers this feature.

1. This feature already exists as an option in Resource Redirect

icon-resource-redirect ships a disableThumbnails / "Disable folder thumbnails" option that does exactly this — Explorer folders show a generic icon instead of a content preview. It's implemented by hooking RegQueryValueExW and supplying a Logo = imageres.dll,-3 value for the AllFolders\Shell shell bag key (implementation), i.e. the shell's own documented mechanism — no COM activation, no vtable hooking, no pinned module.

The maintainer strongly prefers extending an existing mod over merging a near-duplicate. Please state in the PR description how this mod differs and why it should exist separately. The differences I can see are that yours also covers folder shortcuts and that it isn't tied to a large theming mod — those may well be enough, but it needs to be spelled out. If the delta really is just "the same thing, standalone", extending Resource Redirect (or the Logo-value technique in a small mod of its own) is the lighter path.

2. Don't activate the COM object to find the hook target — hook the implementation by symbol

Wh_ModInit currently does CoInitializeExCoCreateInstance(CLSID_LocalThumbnailCache) → read vtable[3] → pin the owning module → CoUninitialize. Each step has a cost, and there's an established pattern in the repo that avoids all of them.

faster-virtual-desktop-switching hooks the very same function via symbols — its comment says it directly: "The implementation moved between windows.storage.dll and thumbcache.dll. Hook each loaded copy independently, avoiding COM activation and a hard-coded vtable slot."

// thumbcache.dll, windows.storage.dll
WindhawkUtils::SYMBOL_HOOK symbolHooks[] = {
    {{L"public: virtual long __cdecl CThumbnailCache::GetThumbnail(struct IShellItem *,unsigned int,enum WTS_FLAGS,struct ISharedBitmap * *,enum WTS_CACHEFLAGS *,struct WTS_THUMBNAILID *)"},
     &g_getThumbnailOriginal, GetThumbnailHook, true},
    {{L"public: virtual long __cdecl CThumbnailCacheAPI::GetThumbnail(struct IShellItem *,unsigned int,enum WTS_FLAGS,struct ISharedBitmap * *,enum WTS_CACHEFLAGS *,struct WTS_THUMBNAILID *)"},
     &g_apiGetThumbnailOriginal, ApiGetThumbnailHook, true},
};

Concrete gains over the current approach:

  • Nothing is force-loaded. In a process that never shows a thumbnail, the mod does no work at all — which matters a lot given @include * (see item 4).
  • No CoInitializeEx/CoUninitialize on the target's main thread before its own code runs.
  • Both implementation classes are covered. CLSID_LocalThumbnailCache hands you one class's vtable; the reference mod hooks both CThumbnailCache::GetThumbnail and CThumbnailCacheAPI::GetThumbnail, in both thumbcache.dll and windows.storage.dll. If you find folder thumbnails still appearing on some shell surface, an unhooked second class is the likely reason.
  • Late loads are handled. The reference mod hooks kernelbase!LoadLibraryExW and applies the hooks when the DLL shows up (Wh_ModInit / LoadLibraryExW hook). Note it resolves LoadLibraryExW out of kernelbase.dll explicitly rather than hooking the kernel32 import — internal callers go straight to kernelbase, so a kernel32-level hook misses them.

The one thing the current approach genuinely buys you is version-agnosticism (whatever the CLSID returns is what gets hooked). Hooking both symbols in both DLLs gives you the same coverage without the side effects.

3. GET_MODULE_HANDLE_EX_FLAG_PIN is an irreversible change to the process

BOOL pinned = GetModuleHandleExW(
    GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, ...);

A pin can never be undone. The thumbnail-cache DLL stays mapped for the lifetime of the process even after the mod is disabled or uninstalled, in every process the mod injected into — and the mod has no Wh_ModUninit at all, so nothing is released. That conflicts with Windhawk's principle that a mod's effects disappear when it's disabled.

Keeping the module alive while the hook is installed is a legitimate need (Windhawk restores the original bytes at unload, so the code must still be mapped). Take an ordinary reference and drop it in Wh_ModUninit instead — see control-panel-in-modern-file-explorer, which spells out the reasoning:

// An ordinary reference, not a pin: released in Wh_ModUninit, once hooks
// are already removed and the module is safe to let go.
HMODULE ref = nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
                   reinterpret_cast<LPCWSTR>(module), &ref);

and releases it in Wh_ModUninit. Note that mod also handles the Wh_ModInit-returns-FALSE case by releasing the reference before returning, since Wh_ModUninit won't run then.

4. @include * combined with eager initialization

Per the mod lifetime flow chart, Wh_ModInit runs on the target's main thread, before the process begins executing. So as written, every process on the machine — console tools, services, background helpers that will never display a thumbnail — pays, at startup: a COM init, an in-proc COM activation, a forced load of windows.storage.dll/thumbcache.dll and its dependency chain (running their DllMains very early in the process), a permanent pin, and a COM teardown.

Two things to do here:

  • Making initialization lazy (item 2) removes almost all of that cost — a process that never loads the thumbnail cache would do nothing beyond a LoadLibraryExW hook.
  • Please still justify or narrow @include *. If Explorer is the main target, explorer.exe is the natural scope — compare folder-thumbnail-style-switcher, which targets explorer.exe only and explicitly notes in its README that @include * "may cause problems". If file dialogs really do need it, listing the specific hosts you care about is still preferable to *.

5. The shortcut path can block for a long time on unreachable targets

hr = SHCreateItemFromIDList(targetIdList, IID_PPV_ARGS(&targetItem));
if (SUCCEEDED(hr)) {
    targetIsFolder = IsPlainFilesystemFolder(targetItem);  // GetAttributes()

Creating a shell item from the target IDList and querying SFGAO_* on it can bind into the target's namespace. For a shortcut pointing at a UNC share that's offline, a disconnected network drive, or removed media, that can stall for many seconds inside every thumbnail request for that shortcut — and it happens for shortcuts the user isn't even asking about, just because they're in the visible folder.

You've already loaded the .lnk, and the link data itself records the target's attributes, so you can answer the question without touching the target at all:

WCHAR targetPath[MAX_PATH];
WIN32_FIND_DATAW findData{};
if (SUCCEEDED(link->GetPath(targetPath, ARRAYSIZE(targetPath), &findData,
                            SLGP_RAWPATH))) {
    targetIsFolder = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
}

That's cached data (it can be stale if the target changed type since the shortcut was made, which is a non-issue here), and it costs nothing beyond the .lnk read you're already doing. Worth verifying on your setup, but it should let you drop the SHCreateItemFromIDList call entirely.

6. Add a screenshot to the README

This is a purely visual mod and the README currently has no image. A before/after screenshot of a folder view with and without thumbnails makes the mod much easier to evaluate in the catalog. Images must be hosted on i.imgur.com or raw.githubusercontent.com.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Use the type-safe hook helper. Wh_SetFunctionHook with reinterpret_cast<void*> on both the target and the hook gives up all compile-time checking of the signature. With #include <windhawk_utils.h>:

    WindhawkUtils::SetFunctionHook(reinterpret_cast<GetThumbnail_t>(target),
                                   GetThumbnail_Hook, &g_originalGetThumbnail);

    (Moot if you switch to HookSymbols, which is typed as well.)

  • Redundant second attribute query. IsPlainFilesystemFolder already requests SFGAO_LINK in its mask, then throws the result away; IsFilesystemFolder immediately calls GetAttributes(SFGAO_LINK, ...) again. One query returning the full attribute set to both callers would do.

  • CoCreateInstance(CLSID_ShellLink) per request. A fresh IShellLinkW is created for every shortcut thumbnail request. It's cheap, but if the shortcut path stays, this is an easy thing to avoid re-doing per call.

  • README wording. "Not-thumbnail icon overlays are unaffected" reads oddly — maybe "Icon overlays are unaffected". "file choosing dialogs" → "file dialogs". There's also a trailing space at the end of the second line and a stray blank line before the final return hooked;.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Scope of what gets suppressed. SFGAO_FOLDER | SFGAO_FILESYSTEM (minus STREAM/LINK) also matches drive roots and filesystem-backed special folders, so those lose their thumbnails too. Almost certainly what you want, but worth a line in the README so users aren't surprised.

  • Failure code and repeated work. Returning WTS_E_FAILEDEXTRACTION means the caller re-asks (and the mod re-runs the attribute checks, and the .lnk load for shortcuts) each time the view is refreshed rather than getting a cached negative. Not a problem in practice, just something to be aware of if you ever see the check showing up in a profile.

  • A setting for shortcuts. Some users may want folder thumbnails gone but folder-shortcut thumbnails kept (or vice versa). Since you added shortcut handling in a follow-up commit anyway, a single boolean would make that configurable cheaply.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Sep 10, 2026
@Anixx

Anixx commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

icon-resource-redirect ships a disableThumbnails / "Disable folder thumbnails" option that does exactly this — Explorer folders show a generic icon instead of a content preview. It's implemented by hooking RegQueryValueExW and supplying a Logo = imageres.dll,-3 value for the AllFolders\Shell

This does not disable thumbnails completely. Still, an empty folder has a different icon from a folder with content. Thus, mods such as Classic Start Menu Folders Icon do not work, neither works replacement of the default folder icon with one from Windows 95/2000/XP/whatever via registry.

@Anixx

Anixx commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author
  1. Don't activate the COM object to find the hook target — hook the implementation by symbol

I don't think this is a good idea, this requires symbols download, which could be unavailable for a given build and creates a delay at the first start.

@Anixx

Anixx commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

/ready-for-reviewer

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-reviewer Ready for a human reviewer, and in the queue for one. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 10, 2026
@Anixx
Anixx marked this pull request as draft September 10, 2026 13:26
@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-reviewer Ready for a human reviewer, and in the queue for one. labels Sep 10, 2026
@windhawk-reviewer

Copy link
Copy Markdown

This pull request was converted to a draft, so it left the human review queue and is back to waiting-for-author.

Mark it as ready for review and comment /ready-for-reviewer to hand it over to a human reviewer again. See the pull request review process for details.

@Anixx
Anixx marked this pull request as ready for review September 10, 2026 13:28
@Anixx

Anixx commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

/ready-for-reviewer

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-reviewer Ready for a human reviewer, and in the queue for one. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-for-reviewer Ready for a human reviewer, and in the queue for one.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants