Why Windows housekeeping only works right after a reboot
The files that most need cleaning are the ones Windows is holding open. Why the minute after a reboot is the only dependable window, and how to use it.
You try to delete a folder an uninstaller left behind and Windows says:
The process cannot access the file because it is being used by another process.
Or the friendlier variant of the same fact, access denied on a path nobody appears to be
using. You close what you can think of, you try again, and it still refuses.
Most of what you find when you search this is a list: run as administrator, add -Force, take
ownership, reboot and try again. Some of it works occasionally and none of it explains why, which
is why the same problem comes back on the next machine.
Why -Force does not help here
Remove-Item -Force is the first thing everyone reaches for, and for this error it is the wrong
tool. -Force makes the cmdlet act on items it would otherwise skip — read-only files, hidden
files, system-attributed files. It changes what PowerShell is willing to delete.
It does not change what Windows is able to delete. A file with an open handle held by another
process is refused by the filesystem, below the level -Force operates at. The same goes for
takeown and icacls: ownership and permissions decide whether you are allowed to ask. The open
handle decides whether the answer can be yes.
That distinction is the whole problem. Three different failures print roughly the same message:
| What is actually wrong | What fixes it |
|---|---|
| Read-only or system attribute | -Force, attrib -r |
| No permission on the path | takeown, icacls |
| Another process holds the file open | Nothing you can pass to Remove-Item |
The third one is the one that brought you here, and it is the only one where the answer is not a flag.
Finding the process that holds it
Before working around a lock, it is worth knowing whose it is — sometimes the answer is a service you can simply stop, and the rest of this guide is unnecessary.
Sysinternals handle.exe will tell you:
handle.exe -nobanner -u "C:\Program Files\VendorName"
Resource Monitor does the same through a UI: CPU → Associated Handles, search the path.
Two answers make the search worthwhile and two do not. If the holder is a service or a background
agent, stop it and the delete succeeds. If the holder is System — meaning the kernel, a
filesystem filter driver, or an antivirus scanning engine — there is nothing to stop, and no
amount of privilege will change that while Windows is running.
The one window where the file is free
Every one of these locks is created by something that started after the machine booted. Which means there is a moment when none of them exist yet: after the operating system is up and before the desktop, the services and the third-party agents have finished starting.
That window is short and it is not glamorous, but it has a property nothing else does — the file is simply closed. No lock to break, no handle to steal, no driver to argue with. Work that fights a live system for minutes completes there in seconds.
This is why “reboot and try again” occasionally works and usually does not: by the time you have logged in and opened Explorer, the thing that held the file is running again. The window closed while you were typing your password.
Two ways to run work inside that window
A scheduled task with an “At startup” trigger, running as SYSTEM. This is the ordinary
answer and it covers most cases. It uses only components that ship with Windows — the task
scheduler, PowerShell, sc.exe — so there is no third-party binary anywhere in the path, which
matters when the security team asks what you are installing on their fleet.
A one-shot Safe Mode boot. For the stubborn remainder — files still held immediately after a normal boot, usually by a filter driver — Safe Mode starts a minimal set of drivers and services, and the file is free there when it is free nowhere else.
The second option has an obvious risk attached, and it is the reason most people do not attempt
it: a machine that boots into Safe Mode and stays there is a desk visit. The answer is bcdedit
with /bootsequence rather than the sticky safeboot flag. /bootsequence applies to the next
boot only and is discarded once used. Whatever happens during that boot — the job fails, the
machine loses power, the script crashes — the boot after it is ordinary Windows, because the
persistent boot entry was never modified.
If you take one thing from this guide into your own scripts, take that one. Never set safeboot
on the default entry to do maintenance. A one-shot sequence cannot strand a machine; a sticky flag
absolutely can, and it will do it on the machine that is furthest away.
The trap on the way there: the task that “succeeded”
Scripts that work when you run them and do nothing when the scheduler runs them are the standard second problem, and the Last Run Result column is read wrongly more often than not:
0x41303— the task has never run. It is not a failure of your script; it is the scheduler saying there was nothing to report. Usually a start date in the past or a trigger condition that was never met.0x41301— the task is running right now.0x0— the task ran and the program returned success.0x1— the task ran, and your script returned a non-zero exit code. The scheduler did its job. The problem is in the script.
For 0x1, the causes are dull and finite. Work through them in this order, because that is
roughly their order of frequency:
- Working directory. A task has none unless you set “Start in”. Every relative path in the script resolves somewhere you did not intend.
- The account. Running as SYSTEM means no mapped drives, no user profile, no per-user environment variables, and no access to anything held in your own credential store.
-NoProfile. Recommended for predictability, but if the script quietly depended on something your profile set up, it stops working the moment you add the flag.- Quoting. The arguments field is not a shell. A path with a space and a trailing backslash inside quotes will not arrive as you expect.
- Interactivity. Anything that waits for input — a confirmation prompt,
Read-Host, a-Confirmdefault — hangs forever with nobody there to answer.
The first move is not to guess between them. Put Start-Transcript at the top of the script,
write it somewhere SYSTEM can definitely write, and read what actually happened:
Start-Transcript -Path 'C:\ProgramData\maintenance\last-run.log' -Force
# ... the work ...
Stop-Transcript
Ten minutes of transcript beats an afternoon of theories.
Before you let anything delete files at boot
A script that runs as SYSTEM before anyone is watching is close to the most dangerous thing on a Windows machine. Ours ended up with four rules, and each one exists because the alternative was worse:
- Declare the work as data, not as code. A job file that lists paths, services and registry keys can be read and approved by the person who has to trust it. A script that runs “whatever is in the config” cannot be reviewed at all, because its behaviour is not in the review.
- Guard the paths. No drive letters — they differ between machines, and the same job that
cleans
D:\VendorAppon one machine finds something else entirely on another. Resolve volumes by label. Refuse... Refuse system directories outright, at the point of validation, not at the point of deletion. - Do not delete on the first pass. Move to a timestamped quarantine folder on the same volume
— it is instant and costs no space — and export registry keys and service registrations to
.regfiles before removing them. The one time you need this, you need it badly. - Leave nothing behind. The scheduled task, the boot entry and any service the job installed should remove themselves once the run is over. Maintenance tooling that outlives the maintenance becomes something nobody remembers installing.
The working version
We package all of this as OfflineTasks — MIT licensed, jobs declared in JSON, both engines implemented, quarantine on by default. The README opens with what it deliberately cannot do rather than with features, which is the section worth reading first.
Use it, read it, or take the four rules above and write your own. The rules are the part that matters.
Related
- Declare maintenance jobs as data, not as codeA maintenance tool that can run any command is one nobody should trust with admin rights. What changes when the job is declarative data instead of code.
- OfflineTasksAn open-source Windows maintenance tool that runs cleanup jobs at the one moment they reliably work: right after reboot, before anything can lock a file.
Does your version have a constraint this guide does not?
That constraint is usually the whole problem. Describe it and we will tell you whether it is the kind of thing we take on.
Start a projectWe reply within 1 business day. No sales calls unless you ask for one.