<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: OpsKit</title>
    <description>The latest articles on DEV Community by OpsKit (@opskit).</description>
    <link>https://dev.to/opskit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4168243%2F1856e768-1f1c-43fd-8698-f26894362515.png</url>
      <title>DEV Community: OpsKit</title>
      <link>https://dev.to/opskit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/opskit"/>
    <language>en</language>
    <item>
      <title>How to get an email when a Windows scheduled task fails (without babysitting Task Scheduler)</title>
      <dc:creator>OpsKit</dc:creator>
      <pubDate>Wed, 07 Oct 2026 10:46:33 +0000</pubDate>
      <link>https://dev.to/opskit/how-to-get-an-email-when-a-windows-scheduled-task-fails-without-babysitting-task-scheduler-3986</link>
      <guid>https://dev.to/opskit/how-to-get-an-email-when-a-windows-scheduled-task-fails-without-babysitting-task-scheduler-3986</guid>
      <description>&lt;p&gt;Your backup job runs at 3 a.m. At 9 a.m. your manager asks why last night's export is missing.&lt;br&gt;
You open Task Scheduler and see it: &lt;em&gt;Last Run Result: 0x1&lt;/em&gt;. The task failed hours ago and&lt;br&gt;
nothing told you.&lt;/p&gt;

&lt;p&gt;Windows used to tell you. Task Scheduler had built-in email alerts - then they were removed&lt;br&gt;
in Server 2016 and never came back. Everyone reinvents the same broken workaround, so here is&lt;br&gt;
the version that actually works in production.&lt;/p&gt;


&lt;h2&gt;
  
  
  What people usually paste (and why it breaks)
&lt;/h2&gt;

&lt;p&gt;The typical Google result is a script that loops over tasks, calls&lt;br&gt;
&lt;code&gt;Get-ScheduledTaskInfo&lt;/code&gt;, and mails you when &lt;code&gt;LastTaskResult -ne 0&lt;/code&gt;. Roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TaskPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-notlike&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\Microsoft\Windows*'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ScheduledTaskInfo&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastRunTime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;-24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastTaskResult&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Send-MailMessage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-To&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;example.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-From&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;taskwatch&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;example.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-Subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task failed: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TaskName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-SmtpServer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;smtp.example.com&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works once. Then it burns you in one of three ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: the &lt;code&gt;LastRunTime&lt;/code&gt; seconds quirk
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Get-ScheduledTaskInfo.LastRunTime&lt;/code&gt; has a long-standing bug where the seconds value can&lt;br&gt;
mirror the minutes value. It has been reported through Microsoft Feedback Hub more than once&lt;br&gt;
and it is still there. If you compute "how old is this run" with full timestamp precision,&lt;br&gt;
a task that ran 5 minutes and 5 seconds ago can look like it ran 5 minutes and 55 seconds&lt;br&gt;
ago - and anything with a tight window starts flapping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is boring and reliable: do age math at minute granularity&lt;/strong&gt; and give yourself a&lt;br&gt;
two-minute tolerance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ageMin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastRunTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalMinutes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ageMin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$maxAgeHours&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c"&gt;# stale&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rounding quirk can never wake you up at 3 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: the email flood
&lt;/h2&gt;

&lt;p&gt;The naive script has no memory. It runs every 5 minutes, the task is still broken, and you&lt;br&gt;
get 12 identical emails before your coffee. Worse, when the task recovers and fails again&lt;br&gt;
next week, the "already alerted" logic you bolted on suppresses the &lt;em&gt;new&lt;/em&gt; incident.&lt;/p&gt;

&lt;p&gt;The correct model is a tiny state file keyed per watched item:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;new problem&lt;/strong&gt; -&amp;gt; alert, store a signature of the problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;same problem, within the re-alert window (24h)&lt;/strong&gt; -&amp;gt; stay quiet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;same problem, past the window&lt;/strong&gt; -&amp;gt; one reminder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;problem gone&lt;/strong&gt; -&amp;gt; clear the state, so the &lt;em&gt;next&lt;/em&gt; incident alerts like new&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Signatures must describe the &lt;em&gt;problem&lt;/em&gt;, not the &lt;em&gt;moment&lt;/em&gt;. Do not include the timestamp of&lt;br&gt;
the failure in the signature or every failed run becomes a fresh alert while the task keeps&lt;br&gt;
failing every cycle.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 3: watching only Task Scheduler
&lt;/h2&gt;

&lt;p&gt;Half the jobs that "run themselves" are not scheduled tasks - they are SQL Agent jobs,&lt;br&gt;
SSIS packages, cron-style loops inside services, or a script on another box. Task Scheduler&lt;br&gt;
monitoring gives you a green light while the real job rots.&lt;/p&gt;

&lt;p&gt;The trick that covers everything is a &lt;strong&gt;heartbeat file&lt;/strong&gt;: one line appended by the job on&lt;br&gt;
success, one configured maximum age by the watcher.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# at the end of ANY job, scheduled or not:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Scripts\hb.txt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the file is missing or older than N minutes, alert. That converts "is my job alive?"&lt;br&gt;
into a filesystem check that works for anything that can write a file.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I run in production
&lt;/h2&gt;

&lt;p&gt;After getting tired of rebuilding this per server, I packaged it as &lt;strong&gt;TaskWatch&lt;/strong&gt; - a local,&lt;br&gt;
subscription-free watchdog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FAIL&lt;/strong&gt; - watched task exited non-zero, exit code in the alert&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MISSED RUN&lt;/strong&gt; - "must run every 25 hours" exceeded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DISABLED / MISSING&lt;/strong&gt; - someone turned your backup task off or deleted it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HEARTBEAT STALE&lt;/strong&gt; - the heartbeat trick, automated&lt;/li&gt;
&lt;li&gt;alerts via SMTP email, a Teams/Slack-style webhook, or the local log&lt;/li&gt;
&lt;li&gt;an HTML status page per machine, one bookmark&lt;/li&gt;
&lt;li&gt;one alert per problem, re-reminder after 24h, auto-clear on recovery&lt;/li&gt;
&lt;li&gt;nothing leaves your machine: no cloud, no account, no telemetry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One-command setup registers it as its own scheduled task every 5 minutes, and it can send&lt;br&gt;
you a test email during install so you know alerts work &lt;em&gt;before&lt;/em&gt; you need them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Install-TaskWatch.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-WatchList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'NightlyBackup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'SqlMaintenance'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-SmtpServer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;smtp.example.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-From&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tw&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;example.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-To&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;you&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;example.com&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The full version is $15, one-time, no subscription&lt;/strong&gt;&lt;br&gt;
(30-day refund): &lt;a href="https://xennedelan.gumroad.com/l/taskwatch" rel="noopener noreferrer"&gt;xennedelan.gumroad.com/l/taskwatch&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Free sample
&lt;/h2&gt;

&lt;p&gt;I kept a free, MIT-licensed version for people who just want the report. It prints every&lt;br&gt;
task that failed on the machine in the last 24 hours with exit codes, and exits 1 if&lt;br&gt;
anything failed - so you can drop it into any existing alerting that understands exit codes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/OpsKit-Dev/powershell-admin-scripts&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;powershell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExecutionPolicy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Bypass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\powershell-admin-scripts\TaskWatch\TaskWatch-mini.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/OpsKit-Dev/powershell-admin-scripts/blob/main/TaskWatch/TaskWatch-mini.ps1" rel="noopener noreferrer"&gt;&lt;code&gt;TaskWatch-mini.ps1&lt;/code&gt; on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/xenne_46bf4f39eb9488be6ef/10-powershell-scripts-that-catch-windows-server-problems-before-your-users-do-5f6p"&gt;10 PowerShell scripts that catch Windows server problems before your users do&lt;/a&gt; - the scripts TaskWatch tells you to go run: disk cleanup, backup rotation, service watchdog, TLS expiry checks and more (&lt;a href="https://xennedelan.gumroad.com/l/opskit" rel="noopener noreferrer"&gt;OpsKit, $7&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/xenne_46bf4f39eb9488be6ef/offboarding-in-powershell-the-checklist-i-run-when-someone-leaves-1ap4"&gt;Offboarding in PowerShell - the checklist I run when someone leaves&lt;/a&gt; - AD user offboarding (&lt;a href="https://xennedelan.gumroad.com/l/offboard" rel="noopener noreferrer"&gt;$2&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If TaskWatch saves you one "why is the export missing" conversation, it has paid for itself&lt;br&gt;
several times over.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Disk cleanup with PowerShell: an age-gated script that shows its work before deleting anything</title>
      <dc:creator>OpsKit</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:01:51 +0000</pubDate>
      <link>https://dev.to/opskit/disk-cleanup-with-powershell-an-age-gated-script-that-shows-its-work-before-deleting-anything-4g0n</link>
      <guid>https://dev.to/opskit/disk-cleanup-with-powershell-an-age-gated-script-that-shows-its-work-before-deleting-anything-4g0n</guid>
      <description>&lt;p&gt;Temp folder cleanup is the kind of task everybody does by hand once, gets uncomfortable with, and then automates badly. The uncomfortable part is the three-week-old &lt;code&gt;.part&lt;/code&gt; download sitting in someone's temp folder, or the log file that's still being written to — scripts that just do &lt;code&gt;Remove-Item $env:TEMP\*&lt;/code&gt; don't care. They'll eat it.&lt;/p&gt;

&lt;p&gt;Here's the cleanup script I actually run, and the three rules that make it safe enough to schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: never delete by age alone, always by last-touched age
&lt;/h2&gt;

&lt;p&gt;The failure mode of naive cleanup isn't deleting too much — it's deleting something &lt;em&gt;live&lt;/em&gt;. A file's creation date tells you nothing; a file being written to right now still has a creation date from three weeks ago if the folder was restored from backup.&lt;/p&gt;

&lt;p&gt;What matters is &lt;strong&gt;last write time&lt;/strong&gt;: if a file hasn't been touched in 3+ days, nothing has it open, nothing is appending to it, and nobody is actively using it. The script's &lt;code&gt;-MinFileAgeDays&lt;/code&gt; parameter (default 3) filters everything through that check before a single file is considered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$MinFileAge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$MinFileAgeDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$targets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastWriteTime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$MinFileAge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raise it to 7 or 14 on busy servers. On a terminal server with users in session all day, that one number is the difference between a maintenance window and an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: -WhatIf has to mean something
&lt;/h2&gt;

&lt;p&gt;PowerShell's &lt;code&gt;SupportsShouldProcess&lt;/code&gt; attribute gives you &lt;code&gt;-WhatIf&lt;/code&gt; and &lt;code&gt;-Confirm&lt;/code&gt; for free, but only if you route every destructive call through &lt;code&gt;ShouldProcess&lt;/code&gt;. The script does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$PSCmdlet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldProcess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Remove (age &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$age&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;) days)"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$freed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it once with &lt;code&gt;-WhatIf&lt;/code&gt; and you get a complete preview — full paths, ages, counts — with zero deletions. It's a dry run you can paste into a change ticket as evidence of what &lt;em&gt;would&lt;/em&gt; happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: the script reports, it doesn't just delete
&lt;/h2&gt;

&lt;p&gt;A cleanup that leaves no trace is a cleanup nobody can audit. Each run appends a line to &lt;code&gt;%ProgramData%\OpsKit\disk-cleanup.csv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;FreedMB&lt;/span&gt;
&lt;span class="ld"&gt;2026-10-08&lt;/span&gt; &lt;span class="ld"&gt;03:14:02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;WEB&lt;/span&gt;&lt;span class="mf"&gt;01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;812.4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five weeks later, when someone asks why disk space jumped on Tuesday, the answer is one &lt;code&gt;Import-Csv&lt;/code&gt; away. It also gives the script a natural heartbeat — an empty CSV section means the scheduled task stopped running, which you notice &lt;em&gt;before&lt;/em&gt; the disk fills up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually cleans
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User temp folders&lt;/strong&gt; (&lt;code&gt;$env:LOCALAPPDATA\Temp&lt;/code&gt;) for every profile on the box — not just the currently logged-in one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System temp&lt;/strong&gt; (&lt;code&gt;$env:windir\Temp&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows Update download cache&lt;/strong&gt; (&lt;code&gt;$env:windir\SoftwareDistribution\Download&lt;/code&gt;) — usually the biggest single win after a patch cycle, and safe to clear when the update service isn't mid-install&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old logs&lt;/strong&gt; in paths you configure, age-gated the same way
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\disk-cleanup.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-WhatIf&lt;/span&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="c"&gt;# preview, delete nothing&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\disk-cleanup.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MinFileAgeDays&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;7&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c"&gt;# only files idle a week&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\disk-cleanup.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MinFileAgeDays&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-KeepDays&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;30&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# logs: age-gate 3, retention 30&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it before you trust it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/OpsKit-Dev/powershell-admin-scripts/blob/main/disk-cleanup.ps1" rel="noopener noreferrer"&gt;disk-cleanup.ps1&lt;/a&gt; is one of two scripts I keep MIT-licensed and free in the &lt;a href="https://github.com/OpsKit-Dev/powershell-admin-scripts" rel="noopener noreferrer"&gt;powershell-admin-scripts repo&lt;/a&gt; — source visible, no account, no telemetry. The companion there is &lt;code&gt;ssl-expiry-check.ps1&lt;/code&gt;, which catches expiring certificates before your users do.&lt;/p&gt;

&lt;p&gt;The full kit — backup rotation, service watchdog, event-log alerting, inventory export, share and password audits, patch reporting, and the user-offboarding checklist — is &lt;a href="https://xennedelan.gumroad.com/l/opskit" rel="noopener noreferrer"&gt;$7 on Gumroad&lt;/a&gt; with a 30-day refund. If offboarding is the only thing you need, &lt;a href="https://xennedelan.gumroad.com/l/offboard" rel="noopener noreferrer"&gt;the standalone script is $2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href="https://dev.to/opskit/how-to-get-an-email-when-a-windows-scheduled-task-fails-without-babysitting-task-scheduler-3986"&gt;How to get an email when a Windows scheduled task fails&lt;/a&gt; - the watchdog I run over exactly these kinds of jobs (&lt;a href="https://xennedelan.gumroad.com/l/taskwatch" rel="noopener noreferrer"&gt;TaskWatch&lt;/a&gt;, free &lt;a href="https://github.com/OpsKit-Dev/powershell-admin-scripts/blob/main/TaskWatch/TaskWatch-mini.ps1" rel="noopener noreferrer"&gt;mini sample&lt;/a&gt;).&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>windows</category>
      <category>sysadmin</category>
      <category>automation</category>
    </item>
    <item>
      <title>Offboarding in PowerShell: the checklist I run when someone leaves</title>
      <dc:creator>OpsKit</dc:creator>
      <pubDate>Wed, 07 Oct 2026 08:39:13 +0000</pubDate>
      <link>https://dev.to/opskit/offboarding-in-powershell-the-checklist-i-run-when-someone-leaves-1ap4</link>
      <guid>https://dev.to/opskit/offboarding-in-powershell-the-checklist-i-run-when-someone-leaves-1ap4</guid>
      <description>&lt;p&gt;Someone hands in their notice and IT gets one chance to get the offboarding right. Miss a step and you're explaining to security why a terminated employee's account still resolved Kerberos tickets three weeks later.&lt;/p&gt;

&lt;p&gt;Here's the checklist I run, with the PowerShell for every step that can be automated — and an honest note about the steps that shouldn't be.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Disable the account (not delete)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Disable-ADAccount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deleting breaks audit history and exchange artifacts you still need. Disable, and let your 30-day cleanup process delete later.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Kill the password &lt;em&gt;now&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Disabling stops interactive logon, but you also want the credential itself dead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Set-ADUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-Replace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pwdLastSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pwdLastSet = 0&lt;/code&gt; marks the password as needing a change at next logon — which effectively means the old password is done. Services still running as that user will fail loudly instead of silently continuing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Terminate active sessions
&lt;/h2&gt;

&lt;p&gt;Leaving a disconnected RDP session alive means a profile hive stays locked and scheduled tasks may keep running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ComputerName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;COMPUTERNAME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UserName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*\&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SamAccountName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Stop-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this &lt;em&gt;after&lt;/em&gt; disabling the account, and do it on every box your jump-host inventory knows about — not just the one you remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Archive the home directory before anyone empties it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$dest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"E:\offboarding\&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SamAccountName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;yyyyMMdd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;robocopy.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HomeDirectory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/E&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/R:1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/W:1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/NP&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;robocopy&lt;/code&gt; exit codes 0–7 are success — check for &lt;code&gt;&amp;gt;= 8&lt;/code&gt; before you let anything claim the copy worked. Archive first, remove access second. The order matters exactly once, and it matters a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Write the audit trail while you do it
&lt;/h2&gt;

&lt;p&gt;Every action above appends one row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;Timestamp&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Ticket&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Operator&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;User&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;LastLogon&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Actions&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;HomeBackup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When security asks "when exactly did we cut off jsmith?", the answer should be a &lt;code&gt;Select-String&lt;/code&gt;, not archaeology across four consoles.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The parts that must stay manual (on purpose)
&lt;/h2&gt;

&lt;p&gt;Mailbox delegation and forwarding, distribution-group membership, VPN certificates, SaaS SSO apps, badge access — these live in systems with different owners and different blast radii. My script deliberately prints them as a to-do list instead of silently automating them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Manual follow-ups: mailboxes/distribution groups, VPN, SaaS SSO, badge - not automatable here.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automating revocation in a system you can't verify is how you get a "why did the CEO lose email" ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;Steps 1–5 are one command with &lt;code&gt;-WhatIf&lt;/code&gt; support and &lt;code&gt;ConfirmImpact = High&lt;/code&gt; (you get prompted before anything changes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\user-offboarding.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-SamAccountName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jsmith&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Ticket&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;INC-1042&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-HomeDirBackup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;E:\offboarding&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free first:&lt;/strong&gt; two scripts from the kit (disk cleanup + TLS expiry, MIT) in the &lt;a href="https://github.com/OpsKit-Dev/powershell-admin-scripts" rel="noopener noreferrer"&gt;powershell-admin-scripts repo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Just the offboarding script:&lt;/strong&gt; &lt;a href="https://xennedelan.gumroad.com/l/offboard" rel="noopener noreferrer"&gt;$2 — OpsKit Offboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All ten admin scripts&lt;/strong&gt; (cleanup, backups, watchdog, certs, alerts, inventory, share/password audits, patch report, offboarding): &lt;a href="https://xennedelan.gumroad.com/l/opskit" rel="noopener noreferrer"&gt;$7 — OpsKit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are instant download with a 30-day refund. &lt;em&gt;(Disclosure: those are my products — the snippets above work standalone if you'd rather assemble it yourself.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What's step 7 on your offboarding checklist? Mine's usually "find out which service account was using their laptop."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href="https://dev.to/opskit/how-to-get-an-email-when-a-windows-scheduled-task-fails-without-babysitting-task-scheduler-3986"&gt;How to get an email when a Windows scheduled task fails&lt;/a&gt; - the watchdog I run over exactly these kinds of jobs (&lt;a href="https://xennedelan.gumroad.com/l/taskwatch" rel="noopener noreferrer"&gt;TaskWatch&lt;/a&gt;, free &lt;a href="https://github.com/OpsKit-Dev/powershell-admin-scripts/blob/main/TaskWatch/TaskWatch-mini.ps1" rel="noopener noreferrer"&gt;mini sample&lt;/a&gt;).&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>sysadmin</category>
      <category>windows</category>
      <category>activedirectory</category>
    </item>
    <item>
      <title>10 PowerShell scripts that catch Windows server problems before your users do</title>
      <dc:creator>OpsKit</dc:creator>
      <pubDate>Wed, 07 Oct 2026 08:09:34 +0000</pubDate>
      <link>https://dev.to/opskit/10-powershell-scripts-that-catch-windows-server-problems-before-your-users-do-5f6p</link>
      <guid>https://dev.to/opskit/10-powershell-scripts-that-catch-windows-server-problems-before-your-users-do-5f6p</guid>
      <description>&lt;p&gt;Every Windows admin eventually writes the same five scripts from scratch: clean the temp folders, rotate the backups, watch the services, check the certificates, and offboard someone who left. Then the server gets rebuilt, and you write them again.&lt;/p&gt;

&lt;p&gt;I got tired of that loop and packaged the ten scripts I kept re-writing into one small kit. Here are five of them with the part that actually matters — the logic, not the boilerplate — plus what each one has caught in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Disk cleanup that tells you what it freed
&lt;/h2&gt;

&lt;p&gt;The usual trap: &lt;code&gt;Remove-Item -Recurse&lt;/code&gt; on &lt;code&gt;C:\Windows\Temp&lt;/code&gt; while Windows Update is mid-download. Age-gate the files instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$cutoff&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$MinFileAgeDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# default 3 days&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastWriteTime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cutoff&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;-WhatIf&lt;/code&gt; first, then check the CSV: every run appends &lt;code&gt;Timestamp, Host, FreedMB&lt;/code&gt;. On a terminal-server farm this is usually 4–10 GB per box on the first pass, then a few hundred MB a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Certificate expiry before the 3 a.m. page
&lt;/h2&gt;

&lt;p&gt;Nobody notices the TLS cert until the webhook stops firing. A raw check needs no modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ssl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Net.Security.SslStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStream&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ssl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AuthenticateAsClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$ssl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RemoteCertificate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotAfter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Days&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it to a host list, make it &lt;code&gt;exit 1&lt;/code&gt; when anything is under 30 days, and drop it in Task Scheduler. It has never been the interesting part of anyone's job, and it is always the thing that pages you.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A watchdog with a restart cap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Restart-Service&lt;/code&gt; in a loop is how you turn a 5-minute outage into a restart storm. The cap is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$restartsToday&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Import-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LogPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Restarted'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timestamp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$restartsToday&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$MaxRestartsPerDay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Write-Log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Suppressed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;continue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three restarts and then it &lt;em&gt;stops touching the service&lt;/em&gt; and writes &lt;code&gt;Suppressed&lt;/code&gt; to the log. A dead service that stays dead is a signal; a service in a crash loop that you keep poking is noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Share permissions: find the &lt;code&gt;Everyone&lt;/code&gt; group
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$leaf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Everyone'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FileSystemRights&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'FullControl|Modify'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$flag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RISK: &lt;/span&gt;&lt;span class="nv"&gt;$identity&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$perms&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against &lt;code&gt;Get-SmbShare&lt;/code&gt; on every file server and you will, statistically, find at least one share where someone granted &lt;code&gt;Everyone&lt;/code&gt; FullControl "temporarily" in 2019. The audit CSV gives you the share path and the exact ACE, so the remediation conversation is short.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Offboarding that leaves a trail
&lt;/h2&gt;

&lt;p&gt;Disabling the account is the easy 10%. The parts people forget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Set-ADUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-Replace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pwdLastSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# password dies with the session&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# kill the user's processes, robocopy the home dir to an archive, append one audit line:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Timestamp | Ticket | Operator | User | LastLogon | Actions | HomeBackup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every action writes one CSV row with the ticket number, so when someone asks "when exactly did we cut off jsmith?", the answer is a &lt;code&gt;Select-String&lt;/code&gt;, not archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ties them together
&lt;/h2&gt;

&lt;p&gt;Three rules, applied everywhere:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-WhatIf&lt;/code&gt; before destructive.&lt;/strong&gt; Every script that deletes, disables, or restarts supports &lt;code&gt;SupportsShouldProcess&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV audit for every mutation.&lt;/strong&gt; If it changed state, it logged a timestamped row to &lt;code&gt;%ProgramData%\OpsKit\&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No obfuscation, no agents, no telemetry.&lt;/strong&gt; Plain commented PowerShell you can read before you run it — which is the only kind of script worth putting on a production box.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest pitch
&lt;/h2&gt;

&lt;p&gt;These ten — cleanup, backup rotation, service watchdog, cert expiry, event-log alerting, inventory export, share-permission audit, password-age audit, patch reporting, and offboarding — are the pack I'd have bought years ago instead of re-writing. Two ways in: the &lt;strong&gt;whole kit is $7&lt;/strong&gt; (&lt;a href="https://xennedelan.gumroad.com/l/opskit" rel="noopener noreferrer"&gt;OpsKit — 10 Windows Server Admin PowerShell Scripts&lt;/a&gt;), or if offboarding is your only pain point, &lt;strong&gt;the offboarding script alone is $2&lt;/strong&gt; (&lt;a href="https://xennedelan.gumroad.com/l/offboard" rel="noopener noreferrer"&gt;OpsKit Offboard&lt;/a&gt;). &lt;strong&gt;Prefer to try first?&lt;/strong&gt; disk-cleanup and ssl-expiry-check are MIT-licensed and free in the &lt;a href="https://github.com/OpsKit-Dev/powershell-admin-scripts" rel="noopener noreferrer"&gt;powershell-admin-scripts repo&lt;/a&gt;. Everything is tested by dry-run and live execution, 30-day refund if it's not for you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Full disclosure: yes, that's my product. The five scripts above work standalone if you'd rather copy them — no hard feelings.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speaking of failing quietly:&lt;/strong&gt; knowing &lt;em&gt;when&lt;/em&gt; one of these jobs dies at 3 a.m. is the other half of the problem. I wrote &lt;a href="https://dev.to/opskit/how-to-get-an-email-when-a-windows-scheduled-task-fails-without-babysitting-task-scheduler-3986"&gt;How to get an email when a Windows scheduled task fails&lt;/a&gt; (including the &lt;code&gt;LastRunTime&lt;/code&gt; PowerShell bug that will page you for nothing), packaged it as &lt;a href="https://xennedelan.gumroad.com/l/taskwatch" rel="noopener noreferrer"&gt;TaskWatch - $15, local, no subscription&lt;/a&gt;, and kept a free failure-report sample in the repo (&lt;a href="https://github.com/OpsKit-Dev/powershell-admin-scripts/blob/main/TaskWatch/TaskWatch-mini.ps1" rel="noopener noreferrer"&gt;TaskWatch-mini.ps1&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;What would you add? The one I'm still missing is a DNS stale-record sweeper.&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>sysadmin</category>
      <category>automation</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
