-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
dockutil can be a single-step tool because macOS gives it a writable, documented store:
edit com.apple.dock.plist, tell the Dock to reload, done. Windows 11 gives none of that.
The shell COM verbs that used to pin and unpin taskbar items were removed, and the live
pin state lives in two undocumented binary caches — the Favorites value in the
Taskband registry key, and start2.bin in the Start Menu package's local state. There
is no supported way to author either.
What Windows does still honour is the layout policy: a LayoutModification.xml pointed at
by StartLayoutFile, activated by LockedStartLayout, which Explorer reads when it
builds the taskbar. So TaskbarUtil splits into two halves that meet at a file:
-
Authoring.
add,remove,move,replacebuild a pin list and serialise it to%LOCALAPPDATA%\TaskbarUtil\LayoutModification.xml. Nothing on screen changes. -
Deployment.
applyinstalls that file as policy and forces Explorer to rebuild against it. Everything changes at once.
Every awkwardness in the tool follows from that split, including the fact that you cannot change one pin without rewriting the whole taskbar.
flowchart TD
CLI[Program.cs / System.CommandLine] --> ADD[add / remove / move / replace]
CLI --> APPLY[apply]
CLI --> RESET[reset]
CLI --> READ[list / find / show]
CLI --> SET[settings]
ADD --> RES[AppResolver]
RES --> KNOWN[KnownApps table]
RES --> SM[Start Menu .lnk scan]
RES --> APPX[Get-AppxPackage via powershell.exe]
SM --> RANK[ShortcutRanker]
ADD --> MODEL[TaskbarLayout / TaskbarPin]
MODEL --> GEN[LayoutXmlGenerator]
GEN --> CFG[(%LOCALAPPDATA%\TaskbarUtil\LayoutModification.xml)]
CFG --> PARSE[LayoutXmlParser]
PARSE --> MODEL
APPLY --> POL[PolicyManager]
POL --> REG[(HKCU Policies\...\Explorer)]
POL --> SHELL[(%LOCALAPPDATA%\...\Shell\LayoutModification.xml)]
POL --> HKU[(HKEY_USERS per SID, --allhomes)]
APPLY --> EXP[ExplorerHelper]
EXP --> CACHE[(start2.bin + Taskband)]
RESET --> POL
RESET --> TW[TaskbarWriter.Restore]
RESET --> EXP
READ --> PIR[PinnedItemsReader]
ADD --> LOG[FileLog]
APPLY --> LOG
RESET --> LOG
SET --> LOG
| Component | File | Responsibility |
|---|---|---|
Program |
src/Program.cs |
Builds the command tree, installs a crash handler that logs before printing |
| Command classes | src/Commands/*.cs |
One file per verb; each owns its own flags, exit codes and log lines |
AppResolver |
src/Core/AppResolver.cs |
Turns a friendly name into a pinnable identifier |
KnownApps |
src/Core/KnownApps.cs |
Built-in table of common apps and their identifiers |
ShortcutRanker |
src/Core/ShortcutRanker.cs |
Scores Start Menu shortcuts so the real launcher wins |
TaskbarLayout / TaskbarPin
|
src/Core/ |
The in-memory pin list and its ordering operations |
LayoutXmlGenerator / LayoutXmlParser
|
src/Core/ |
Serialise and deserialise the layout XML |
PolicyManager |
src/Core/PolicyManager.cs |
Writes and clears the policy, enumerates profiles for --allhomes
|
ExplorerHelper |
src/Core/ExplorerHelper.cs |
Cache clearing and the shell restart, with a recovery path |
TaskbarWriter |
src/Core/TaskbarWriter.cs |
Backup and restore of the legacy pin shortcuts and Taskband blobs |
PinnedItemsReader |
src/Core/PinnedItemsReader.cs |
Reads the legacy User Pinned\TaskBar folder for list
|
FileLog / Log
|
src/Core/ |
The rolled file log, shared verbatim with sibling utilities |
add "Google Chrome" has to become an attribute value that Explorer will accept. The
resolver runs four steps and returns the results ordered by confidence, highest first;
add takes the top one.
-
Known apps. A built-in table is matched against the query by display name, AppID,
AUMID or alias. Entries that resolve straight to an identifier score 100. This is
what makes
File Explorer,Microsoft Edge,Windows Terminal,Notepad,Calculator,Microsoft Store,Settings,Photos,Snipping Tool,Clock,OutlookandMicrosoft Teamswork without touching the disk. - Known app needing a shortcut. Some table entries carry only aliases — Chrome, Firefox, VS Code, Slack, Zoom, 7-Zip, Command Prompt, PowerShell, Remote Desktop, Docker Desktop. For these, every Start Menu shortcut is scored against the display name and every alias, and the best-scoring one is taken. The caller's requested display name is kept rather than the shortcut's, so asking for "Mozilla Firefox" gives you a pin named that regardless of what the shortcut on this machine is called.
- Start Menu scan. A broader scan of both Start Menu trees, scored against the raw query. Scores top out at 95.
-
Appx packages.
powershell.exe -NoProfile -NonInteractiverunsGet-AppxPackageplusGet-AppxPackageManifestto build real AUMIDs as<PackageFamilyName>!<ApplicationId>, scoring 60. This step is whyfindon a Store app can take a couple of seconds; the call is given a 15-second budget and its failure is non-fatal.
Both Start Menu roots are scanned recursively:
%ProgramData%\Microsoft\Windows\Start Menu\Programs
%APPDATA%\Microsoft\Windows\Start Menu\Programs
Reading a shortcut's target requires a COM call into WScript.Shell, which is expensive
across a few hundred .lnk files, so the ranker scores each candidate once on name alone
and only resolves the target for those that could plausibly win.
Taking the first alphabetical match is wrong more often than it sounds, and the code carries the evidence:
| Wrong winner | Should have won | Why it won |
|---|---|---|
Uninstall <app>.lnk |
<app>.lnk |
U sorts before the app's own initial |
<app> Documentation.lnk |
<app>.lnk |
Space (0x20) sorts before the dot of .lnk
|
Firefox Private Browsing.lnk |
Firefox.lnk |
Same comparison |
| Adobe Media Encoder | Visual Studio Code | Bare substring match on the Code alias |
<app> 2024.lnk |
<app> 2026.lnk |
Equal score, equal length, ordinal tie-break |
The scoring that fixes this:
| Rule | Score |
|---|---|
| Shortcut name equals the search term | 95 |
| Term matches at a word boundary, at position 0 | 80 |
| Term matches at a word boundary, later in the name | 70 |
| No word-boundary match | discarded |
Two penalties then apply. A secondary marker in the name costs 40 — the vocabulary
covers uninstallers, installers, repair and modify entries, documentation, help, manuals,
readmes, release notes, user guides, tutorials, samples, websites, changelogs, licences,
safe mode, troubleshooting, private browsing, and the apprentice / non-commercial / indie
/ demo / trial edition markers. A shortcut whose target is not executable — .html,
.htm, .chm, .url, .pdf, .txt, .md, .rtf, .doc, .docx — costs 25. The
numbers are sized so a demoted shortcut (best case 80 − 40 = 40) can never outrank a
primary one (worst case 70).
An exact name match is honoured before any penalty, so asking for a marked edition by its full name gets you exactly that.
Ties break by: newer version first when both names carry a trailing version (dots or
v accepted, so 17.1v1 beats 17.0v3 and 2026 beats 2024), then shortest name,
then ordinal. The version rule is conditional on both sides having one on purpose —
treating "no version" as zero would rank Firefox 115 ESR above plain Firefox, which is
the opposite of what the shortest-name rule is there to do.
Word-boundary matching allows a digit to follow the term but not a letter, so Photoshop
still matches Photoshop9 while Code no longer matches Encoder.
sequenceDiagram
participant U as taskbarutil apply
participant P as PolicyManager
participant R as Registry
participant E as ExplorerHelper
participant S as Shell
U->>U: load + validate config (exit 1 if missing/empty)
U->>P: Apply(configPath)
P->>R: HKCU\...\Policies\Explorer StartLayoutFile + LockedStartLayout
alt key not writable (GPO-managed)
P->>P: copy XML to %LOCALAPPDATA%\Microsoft\Windows\Shell
end
U->>E: RestartExplorer (unless --no-restart)
E->>S: kill StartMenuExperienceHost, ShellExperienceHost
E->>E: wait 2s
E->>E: delete start2.bin
E->>R: delete Taskband subtree
E->>S: kill explorer.exe
E->>E: poll for Shell_TrayWnd, up to 15s
alt taskbar did not return
E->>S: start explorer.exe
E->>E: poll again, up to 15s
end
The order is not arbitrary. start2.bin is locked while StartMenuExperienceHost is
running, so the host has to die first, and the two-second pause is there to let the file
handle actually close. Taskband has to go too, or the old pin order survives the
restart.
The most consequential piece of defensive code in the tool. Windows relaunches the shell
on its own only when Winlogon's AutoRestartShell watchdog is armed and owns the process
that was killed. Neither reliably holds when this runs from a login script: the shell may
still be coming up, the watchdog may not be watching that instance, and policy can disable
the restart outright. When the relaunch does not happen the session is left with a desktop,
a cursor and no shell at all, which is unrecoverable without a reboot.
So the code waits for the taskbar window (Shell_TrayWnd, polled twice a second for 15
seconds) and, if it has not appeared, starts explorer.exe itself and waits again. Both
the timeout and the recovery are logged. The trade-off it encodes: a taskbar missing its
pins is cosmetic; a session with no shell is not.
reset unwinds in the reverse order: policy values deleted (and the key too if it is then
empty), fallback layout file deleted, config deleted, then TaskbarWriter.Restore puts
back any .lnk files and the Favorites / FavoritesResolve binary values saved under
%LOCALAPPDATA%\TaskbarUtil\backup, deletes the backup directory, and Explorer restarts.
Note the asymmetry: the backup is written by TaskbarWriter.Apply, which no command
currently calls — the direct shortcut-writing path exists in the code but is not wired
to a verb. In practice, on a machine that has only ever used apply, there is no backup
to restore, and reset works by removing the policy and clearing the caches so Explorer
rebuilds its own defaults. The restore path is there for state written by that unused
direct-write route.
| State | Location | Lifetime |
|---|---|---|
| Desired pin list | %LOCALAPPDATA%\TaskbarUtil\LayoutModification.xml |
Until reset
|
| Active policy | HKCU\Software\Policies\Microsoft\Windows\Explorer |
Until reset
|
| Fallback policy | %LOCALAPPDATA%\Microsoft\Windows\Shell\LayoutModification.xml |
Until reset
|
| Shared layout | C:\ProgramData\TaskbarUtil\LayoutModification.xml |
Until removed by hand |
| New-profile seed | C:\Users\Default\AppData\Local\Microsoft\Windows\Shell\ |
Until removed by hand |
| Pin backup | %LOCALAPPDATA%\TaskbarUtil\backup\ |
Deleted by reset
|
| Audit trail | C:\ProgramData\ManagedUtilities\logs\taskbarutil.log |
Rolls at 5 MB, 5 generations |
The tool holds no service, no scheduled task, no daemon and no background state. Every run is a complete transaction against files and registry values.
TaskbarUtil — Apache-2.0 — github.com/windowsadmins/taskbarutil
Getting started
Reference
Operating it
Internals