<?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: Mikhail Predtetchenski</title>
    <description>The latest articles on DEV Community by Mikhail Predtetchenski (@mikepred).</description>
    <link>https://dev.to/mikepred</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%2F4204470%2F29ef6260-a81b-4b0b-a2d4-388db59fb9a5.jpg</url>
      <title>DEV Community: Mikhail Predtetchenski</title>
      <link>https://dev.to/mikepred</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikepred"/>
    <language>en</language>
    <item>
      <title>One markdown file, three sites: what broke the first time my crosspost ran</title>
      <dc:creator>Mikhail Predtetchenski</dc:creator>
      <pubDate>Sun, 01 Nov 2026 02:31:03 +0000</pubDate>
      <link>https://dev.to/mikepred/one-markdown-file-three-sites-what-broke-the-first-time-my-crosspost-ran-2m2i</link>
      <guid>https://dev.to/mikepred/one-markdown-file-three-sites-what-broke-the-first-time-my-crosspost-ran-2m2i</guid>
      <description>&lt;h2&gt;
  
  
  Case file
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incident:&lt;/strong&gt; Oct 30–31, 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; the publishing pipeline for this log was built on Oct 24–25 and had never sent a single post. The night the first real post went out, it broke in four separate places.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence:&lt;/strong&gt; a dry run that sent &lt;code&gt;/build-log/images/...&lt;/code&gt; paths to dev.to and Hashnode unchanged; a Cross-post run that died at &lt;code&gt;npm ci&lt;/code&gt; with &lt;code&gt;Missing: @emnapi/core@1.11.3 from lock file&lt;/code&gt;; &lt;code&gt;Hashnode FAILED: fetch failed&lt;/code&gt; in the log of a run that still finished green.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; four assumptions nobody had tested live. Relative URLs only resolve on my site. The lockfile came from a newer npm than the GitHub runner uses. Hashnode's old API host now redirects to a paid-API notice. And the script treated "a site rejected this post" as a log line, not a failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; five small PRs between 12:29 and 2:04 AM CT on Oct 31: &lt;a href="https://github.com/mikepred/build-log/pull/17" rel="noopener noreferrer"&gt;#17&lt;/a&gt; (absolute URLs), &lt;a href="https://github.com/mikepred/build-log/pull/18" rel="noopener noreferrer"&gt;#18&lt;/a&gt; (lockfile), &lt;a href="https://github.com/mikepred/build-log/pull/19" rel="noopener noreferrer"&gt;#19&lt;/a&gt; (Hashnode endpoint), &lt;a href="https://github.com/mikepred/build-log/pull/20" rel="noopener noreferrer"&gt;#20&lt;/a&gt; (re-run the post) and &lt;a href="https://github.com/mikepred/build-log/pull/21" rel="noopener noreferrer"&gt;#21&lt;/a&gt; (fail the run).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevention:&lt;/strong&gt; tests for the URL rewriting that run before every cross-post, a lockfile made with the runner's npm, and a run that goes red when any site rejects a post. The next post, the AMD boot loop one, went to all three sites on its first try.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; a pipeline that has never run isn't done. It's a draft.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fone-markdown-file-three-sites%2Fpipeline.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fone-markdown-file-three-sites%2Fpipeline.svg" alt="The pipeline: a post in content/ is merged to main, deploy.yml builds the Astro site for GitHub Pages, crosspost.yml runs npm ci and tests, crosspost.mjs rewrites URLs and sends to dev.to then Hashnode, a rejected send turns the run red, and ids land in sync-state.json with skip ci. Markers show the four breaks: relative URLs (#17), the lockfile (#18), Hashnode's endpoint (#19) and the green run (#21)" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The story
&lt;/h2&gt;

&lt;p&gt;The idea was simple. I write each post once, as a markdown file in a public GitHub repo. When I merge it, a GitHub Action builds the site on GitHub Pages, and a second job sends the same file to dev.to and Hashnode with a canonical link pointing back to the site. No copy-pasting, no "which version is the real one."&lt;/p&gt;

&lt;p&gt;That got built on Oct 24–25. The first pull request merged at 2:27 AM CT on Oct 25, and the cross-post job ran right away. Its entire output was &lt;code&gt;No content entries to cross-post.&lt;/code&gt; There weren't any posts yet. So for about a week I had a pipeline that had "run" exactly once and done nothing. My old draft of this post said it plainly: the auto-publish has never actually run.&lt;/p&gt;

&lt;p&gt;There was one detour back then that I'm keeping in the story. Hashnode's GraphQL API had started requiring a Pro plan, and my first reaction was to drop Hashnode to drafts only. Then I looked at the price, asked "Unless mistaken, $5/mo on Hashnode should work for API work?", and it did. I upgraded, added the two Hashnode secrets, and the sender went back in. I also asked whether I needed a separate "publisher bot." The answer was no: publishing is the same steps every time, and fixed steps belong in code. My merge is the approval, and the Action ships it.&lt;/p&gt;

&lt;p&gt;Then on the night of Oct 30 I finally had a real post to send: the "how it started" retro. HQ, the Grok Bot I do most of my work with, was getting it ready, and the first problem got caught before anything went live. The retro's diagrams were linked as &lt;code&gt;/build-log/images/how-it-started/...&lt;/code&gt;. That works on my site. On dev.to it would point at dev.to. The script was sending the markdown untouched. That became &lt;a href="https://github.com/mikepred/build-log/pull/17" rel="noopener noreferrer"&gt;#17&lt;/a&gt;, which parses the markdown and rewrites site-relative links and images to full URLs before sending, without touching code blocks. It went through a lot of review rounds (Copilot and Cursor Bugbot both found real edge cases) and merged at 12:29 AM.&lt;/p&gt;

&lt;p&gt;The Cross-post run for that merge &lt;a href="https://github.com/mikepred/build-log/actions/runs/41172774125" rel="noopener noreferrer"&gt;failed within seconds&lt;/a&gt;. Not on dev.to, not on Hashnode, but on &lt;code&gt;npm ci&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync.
npm error Missing: @emnapi/core@1.11.3 from lock file
npm error Missing: @emnapi/runtime@1.11.3 from lock file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixing break one had caused break two. #17 added a markdown parser, and the lockfile that came with it was written by a newer npm than the one the runner gets with Node 22. &lt;a href="https://github.com/mikepred/build-log/pull/18" rel="noopener noreferrer"&gt;#18&lt;/a&gt; regenerated it with Node 22.23.4 and npm 10.9.9, the same versions CI uses, and merged at 1:13 AM.&lt;/p&gt;

&lt;p&gt;At 1:38 AM I merged the retro itself. The run &lt;a href="https://github.com/mikepred/build-log/actions/runs/41177455644" rel="noopener noreferrer"&gt;went green&lt;/a&gt;. dev.to had the post. Hashnode didn't. Buried in the log was one line: &lt;code&gt;Hashnode FAILED: fetch failed&lt;/code&gt;. Hashnode had retired &lt;code&gt;gql.hashnode.com&lt;/code&gt;. It now answers every request with a 301 to a notice about paid API access, and my sender refuses redirects on purpose, so every call failed before it reached the API. Hashnode's own CLI uses &lt;code&gt;gql-beta.hashnode.com&lt;/code&gt;, and a read-only test query with my token worked there. &lt;a href="https://github.com/mikepred/build-log/pull/19" rel="noopener noreferrer"&gt;#19&lt;/a&gt; was a one-line endpoint change, merged at 1:53 AM.&lt;/p&gt;

&lt;p&gt;But merging a script fix doesn't re-send anything, because the job only sends posts whose files changed in that push. So &lt;a href="https://github.com/mikepred/build-log/pull/20" rel="noopener noreferrer"&gt;#20&lt;/a&gt; added one trailing newline to the retro, which doesn't change how it renders, just to make it count as changed. That merged at 1:55 AM, and &lt;a href="https://github.com/mikepred/build-log/actions/runs/41178551310" rel="noopener noreferrer"&gt;this run&lt;/a&gt; is the one I was waiting for: dev.to updated the existing article in place (no duplicate), and Hashnode published the post.&lt;/p&gt;

&lt;p&gt;That left the part that bugged me most. The 1:38 AM run had been green with a failed site in it. If I hadn't read the log, I'd never have known. The Hashnode sender returned &lt;code&gt;{ failed: true }&lt;/code&gt; instead of throwing, and the main script just moved on. &lt;a href="https://github.com/mikepred/build-log/pull/21" rel="noopener noreferrer"&gt;#21&lt;/a&gt; counts those results as failures. The other sites still get their turn, but the run ends red and GitHub emails me. Merged at 2:04 AM.&lt;/p&gt;

&lt;p&gt;The real test came a couple of hours later. At 4:21 AM I merged the AMD boot loop post, and &lt;a href="https://github.com/mikepred/build-log/actions/runs/41188357651" rel="noopener noreferrer"&gt;its run&lt;/a&gt; created it on dev.to and Hashnode on the first try, then committed both ids to &lt;code&gt;sync-state.json&lt;/code&gt;. Nobody touched anything. That's the whole point.&lt;/p&gt;

&lt;p&gt;These days the roles are simple. GitHub is the source: the posts, the code, and the PRs where the reviewers argue. Linear is the backlog of post ideas and fixes. Notion holds private session transcripts that never get published. X, LinkedIn and Facebook get drafts that I post myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do it yourself
&lt;/h2&gt;

&lt;p&gt;My code is all in the public repo, &lt;a href="https://github.com/mikepred/build-log" rel="noopener noreferrer"&gt;github.com/mikepred/build-log&lt;/a&gt;. This is the shape of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. One file per post, with a canonical URL.&lt;/strong&gt; Mine live in &lt;code&gt;content/YYYY-MM-DD-slug.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;One&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;markdown&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;file,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;three&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sites"&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-10-31&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;github-actions&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;astro&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;devto&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;hashnode&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="s"&gt;4 max, dev.to's limit&lt;/span&gt;
&lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Used&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;as&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;description,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dev.to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Hashnode&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;subtitle."&lt;/span&gt;
&lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;you&amp;gt;.github.io/&amp;lt;repo&amp;gt;/&amp;lt;slug&amp;gt;/"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick the canonical URL once and never change it. dev.to and Hashnode both use it to find the existing copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Build the site from the repo.&lt;/strong&gt; I use Astro, deployed to GitHub Pages by its own workflow (&lt;code&gt;deploy.yml&lt;/code&gt;) on every push to &lt;code&gt;main&lt;/code&gt;. That one doesn't know cross-posting exists, so a broken cross-post never blocks the site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cross-post from a second workflow, and keep it on a short leash.&lt;/strong&gt; In &lt;code&gt;crosspost.yml&lt;/code&gt;, ignore the files that shouldn't trigger it, run the tests before sending anything, and pass secrets only to the step that needs them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths-ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sync-state.json&lt;/span&gt;   &lt;span class="c1"&gt;# the state commit shouldn't start another run&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;log/**'&lt;/span&gt;          &lt;span class="c1"&gt;# running-log notes are site-only&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish changed entries&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DEVTO_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEVTO_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;HASHNODE_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HASHNODE_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;HASHNODE_PUBLICATION_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HASHNODE_PUBLICATION_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;BEFORE_SHA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.before }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/crosspost.mjs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script diffs &lt;code&gt;BEFORE_SHA&lt;/code&gt; against the pushed commit and only sends posts in &lt;code&gt;content/&lt;/code&gt; that changed. Each platform is one small file in &lt;code&gt;scripts/senders/&lt;/code&gt; with the same four exports (&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;disabledReason&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;publish&lt;/code&gt;), so adding or dropping a site is one file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Make every send safe to repeat.&lt;/strong&gt; Before creating anything, look for the existing copy. My dev.to sender checks the stored id, then looks for an article with the same &lt;code&gt;canonical_url&lt;/code&gt;, and only creates one if neither exists. Hashnode does the same with &lt;code&gt;originalArticleURL&lt;/code&gt;. The ids go into &lt;code&gt;sync-state.json&lt;/code&gt;, keyed by slug, and the workflow commits that file back with &lt;code&gt;[skip ci]&lt;/code&gt; in the message. That's why re-sending the retro updated dev.to instead of duplicating it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Rewrite site-relative URLs before sending.&lt;/strong&gt; On your site, &lt;code&gt;/build-log/images/x.svg&lt;/code&gt; is fine. Anywhere else it's broken. Rewrite links and images under your base path to absolute URLs in the copy you send, and leave code blocks alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;before: !&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Diagram&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;/build-log/images/how-it-started/team-timeline.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
after:  !&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Diagram&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://mikepred.github.io/build-log/images/how-it-started/team-timeline.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it. Mine has tests for nested HTML, inline code inside link text, lookalike paths like &lt;code&gt;/build-logs&lt;/code&gt;, and code blocks inside lists, and the workflow runs them before every cross-post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Make the lockfile with the runner's npm.&lt;/strong&gt; &lt;code&gt;npm ci&lt;/code&gt; refuses a lockfile that's out of sync with what &lt;em&gt;its&lt;/em&gt; npm expects. If your workflow uses &lt;code&gt;node-version: 22&lt;/code&gt;, regenerate the lockfile with Node 22's npm (10.x), not whatever newer npm you have locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;   &lt;span class="c"&gt;# v22.x, same major as the workflow&lt;/span&gt;
npm &lt;span class="nt"&gt;-v&lt;/span&gt;    &lt;span class="c"&gt;# 10.x&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--package-lock-only&lt;/span&gt;
npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Fail the run when any site rejects a post.&lt;/strong&gt; Keep going so the other sites still get their copy, but don't let the run finish green:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The sender already logged the details. Count it so the run ends red.&lt;/span&gt;
  &lt;span class="nx"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ...after every post and every sender:&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ScriptError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; cross-post send failed.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A missing secret is different. That sender is skipped with a warning, so a copy of the repo without those accounts still gets a clean run. If every sender is skipped, the script warns and exits 0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Dry-run before the first real merge.&lt;/strong&gt; My script has a mode that builds every request and prints it, with no network and no state written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node scripts/crosspost.mjs &lt;span class="nt"&gt;--dry-run&lt;/span&gt; content/2026-10-31-one-markdown-file-three-sites.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's how #17 got checked: the retro's diagram paths came out relative before the fix and absolute after it. A dry run can't catch the other three, though: npm on the runner, a retired endpoint and a hidden failure only show up on a real run. So plan to watch the first one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveats&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hashnode's API needs a Pro plan, and its old host &lt;code&gt;gql.hashnode.com&lt;/code&gt; now redirects. Mine uses &lt;code&gt;gql-beta.hashnode.com&lt;/code&gt;, the host its official CLI uses.&lt;/li&gt;
&lt;li&gt;Editing a copy on dev.to or Hashnode doesn't stick. The next time that post's file changes, the repo version overwrites it.&lt;/li&gt;
&lt;li&gt;dev.to strips tags down to letters and numbers, so &lt;code&gt;github-actions&lt;/code&gt; shows up there as &lt;code&gt;githubactions&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>githubactions</category>
      <category>astro</category>
      <category>devto</category>
      <category>hashnode</category>
    </item>
    <item>
      <title>Giving my Grok Bot assistant admin rights without switching off UAC</title>
      <dc:creator>Mikhail Predtetchenski</dc:creator>
      <pubDate>Sun, 01 Nov 2026 02:28:15 +0000</pubDate>
      <link>https://dev.to/mikepred/giving-my-grok-bot-assistant-admin-rights-without-switching-off-uac-4g67</link>
      <guid>https://dev.to/mikepred/giving-my-grok-bot-assistant-admin-rights-without-switching-off-uac-4g67</guid>
      <description>&lt;h2&gt;
  
  
  Case file
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incident:&lt;/strong&gt; Oct 24, 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; dr eggbot, one of my Grok Bot assistants, runs PowerShell on my PC from a normal, non-elevated terminal. Every admin step came back to me as "paste this into an admin window."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence:&lt;/strong&gt; restore points, &lt;code&gt;HKLM&lt;/code&gt; registry writes, &lt;code&gt;bcdedit&lt;/code&gt; and SYSTEM scheduled tasks all need an elevated session. A Windows rebuild is mostly those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tempting fix:&lt;/strong&gt; SSH to my own machine as an admin. On Windows, an SSH session for an admin account runs fully elevated with no UAC prompt, and the private key would sit in my profile, where anything running as me could use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; a fixed menu of five elevated scheduled tasks under &lt;code&gt;\WinSetup\&lt;/code&gt;. Each one runs a script from a folder only admins can change. My normal account can start the tasks but can't edit them, and the runner refuses any action that isn't on the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevention:&lt;/strong&gt; adding or changing an action means rerunning the installer from an admin window. That one paste is my approval step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; give the assistant verbs, not a shell, and test the boundary from the account it actually runs as. Bonus lesson from the first real run: read the tool's own log before you blame a hang on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The story
&lt;/h2&gt;

&lt;p&gt;I was rebuilding my Windows 11 setup with dr eggbot. It runs PowerShell on my PC, but its terminal isn't elevated, so every admin-only step turned into a command for me to copy into an admin window. After a few of those I asked the obvious question: can we set up an SSH key so you can run these yourself?&lt;/p&gt;

&lt;p&gt;It could, but dr eggbot explained the catch before building anything. You turn on Windows' built-in OpenSSH server, authorize a key, and the assistant runs &lt;code&gt;ssh&lt;/code&gt; to localhost. Windows gives an admin account's SSH session a fully elevated token with no UAC prompt. That part is the point. The problem is where the key lives: in my profile, so the assistant's terminal can read it. That means &lt;em&gt;anything&lt;/em&gt; running as me, whether a sketchy installer, a bad package or the assistant itself, could pick up that key and get admin without a prompt. It's UAC switched off in all but name.&lt;/p&gt;

&lt;p&gt;So I had three options: full SSH admin, a short list of admin jobs, or keep pasting. I asked whether the short-list version could handle something like &lt;code&gt;Enable-ComputerRestore&lt;/code&gt;. It could, as long as that job was on the list. What it can't do is run a brand-new admin command on the spot. Anything new means updating the protected copy of the scripts, and that takes one paste from me in an admin window. I decided that paste was a feature. It's my approval step. I went with the short list.&lt;/p&gt;

&lt;p&gt;Here's what dr eggbot built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A folder, &lt;code&gt;C:\ProgramData\WinSetupAdmin&lt;/code&gt;, that only Administrators and SYSTEM can write to. Everyone else can read and run what's in it.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;runner.ps1&lt;/code&gt; in that folder that accepts exactly one of a fixed set of actions and refuses anything else: RestorePoint, Tweaks, Apps and SaveDumps at first, plus Check, which came later.&lt;/li&gt;
&lt;li&gt;One scheduled task per action under &lt;code&gt;\WinSetup\&lt;/code&gt;. Each runs the runner elevated, as me, only while I'm signed in, and only when something starts it. No triggers, no schedule.&lt;/li&gt;
&lt;li&gt;A permission on each task that lets my normal account start it and read it, but not change it.&lt;/li&gt;
&lt;li&gt;A log for every run, written inside the protected folder, that dr eggbot can read afterward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fai-assistant-admin-without-breaking-uac%2Fverbs-not-a-shell.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fai-assistant-admin-without-breaking-uac%2Fverbs-not-a-shell.svg" alt="Two ways to give an assistant admin. Rejected: anything running as me could use an SSH key in my profile to reach localhost and get any command as admin with no prompt. Built: dr eggbot's non-elevated terminal starts a \WinSetup\ task, the task runs elevated as me, and runner.ps1 accepts only five actions and writes a log. From the normal account, writing to the folder, editing the runner and re-registering a task are all denied. Changing the menu means rerunning the installer from an admin window" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I pasted the installer once, from an admin window. Then dr eggbot started the RestorePoint task from its own non-elevated terminal, and it worked: a fresh restore point, exit 0.&lt;/p&gt;

&lt;p&gt;A working menu only proves half of it, though. The other half is whether the normal account can quietly change what the menu does. So dr eggbot tried, from the same account it runs as. Writing a new file into the folder: blocked. Appending a line to &lt;code&gt;runner.ps1&lt;/code&gt;: blocked. Re-registering a task with a changed definition: access denied. It can start the tasks. It can't change what they do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coda: the hang that wasn't winget
&lt;/h3&gt;

&lt;p&gt;The first real job was applying my tweaks script through the Tweaks task. The registry changes landed, but the task sat at "Running" for more than five minutes with nothing in its log past the header. The script's last step was &lt;code&gt;winget configure --enable&lt;/code&gt;, so we blamed winget. A hidden task has no console, and winget probably wanted one. We moved that step into its own process with a time limit, reran it, and it came back clean in two seconds. Case closed, I thought.&lt;/p&gt;

&lt;p&gt;It wasn't. When dr eggbot finally read winget's own log, winget hadn't done anything on either attempt. I'd passed &lt;code&gt;--enable&lt;/code&gt; together with &lt;code&gt;--disable-interactivity&lt;/code&gt;, and winget rejects that combination: "Option '--enable' and '--disable' cannot be used with other arguments." It quit within milliseconds, both times. On the first run, the thing actually hanging was PowerShell, still waiting on the inline &lt;code&gt;| Out-Null&lt;/code&gt; pipe after winget had exited. Why that pipe stayed open is still unproven. The "clean" rerun was only clean because &lt;code&gt;Start-Process&lt;/code&gt; doesn't pipe output, and the feature I wanted was never turned on.&lt;/p&gt;

&lt;p&gt;The real fix was the right flag (&lt;code&gt;--enable&lt;/code&gt; on its own) plus running winget as its own process. On the next run winget's log showed "Leaf command succeeded: root:configure," and the task finished in about six seconds. The time limit is now five minutes, because &lt;code&gt;--enable&lt;/code&gt; can download from the Store. It's insurance, not the fix, and it has never fired.&lt;/p&gt;

&lt;p&gt;That run turned up one more bug. The tweaks script has a check mode that reports drift, and when it ran without admin it called a SYSTEM-owned task "missing" because it couldn't see it. Now it says "not checked (needs admin)."&lt;/p&gt;

&lt;p&gt;Which raised a fair question: hadn't I just given it admin? Sort of. It could &lt;em&gt;apply&lt;/em&gt; settings through the menu, but it couldn't read admin-only state back to confirm them. So we added a fifth action, &lt;strong&gt;Check&lt;/strong&gt;. It's read-only and runs the drift check elevated. I reran the installer (one more paste, one more approval), dr eggbot started Check itself, and it came back "All settings match." with exit 0. It verified admin-only settings without getting any broader admin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do it yourself
&lt;/h2&gt;

&lt;p&gt;This assumes your everyday account is an administrator under UAC, which is the Windows default, and that you're comfortable reading PowerShell. The scripts are trimmed versions of mine. Keep your working copies in a folder such as &lt;code&gt;%USERPROFILE%\WinSetup&lt;/code&gt;. The installer copies them into the protected folder; that copy is the only one the tasks ever run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Decide on the menu.&lt;/strong&gt; Write down the admin jobs you actually want the assistant to run, as verbs. Mine are RestorePoint, Tweaks (my settings script), Apps (install from a list), SaveDumps (the crash-dump copy from my &lt;a href="https://mikepred.github.io/build-log/windows-update-amd-chipset-driver-broke-boot/" rel="noopener noreferrer"&gt;boot loop post&lt;/a&gt;) and Check (a read-only drift report). Anything that takes free-form input, like "run this command" or "install this package name," doesn't belong on the list. That's a shell with extra steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Write the runner&lt;/strong&gt; (&lt;code&gt;runner.ps1&lt;/code&gt;). It accepts one action from a fixed list and logs everything.&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;param&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mandatory&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;ValidateSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'RestorePoint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Tweaks'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Apps'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'SaveDumps'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Check'&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# The task starts a fresh -NoProfile process, so nothing upstream sets this. Without it, a failed registry write&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# is a non-terminating error, the catch below never sees it, and the log still says exit 0.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="bp"&gt;$Error&lt;/span&gt;&lt;span class="n"&gt;ActionPreference&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;'Stop'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Load modules only from system folders. The per-user module folder is writable by anything running as you.&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;PSModulePath&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;"&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;ProgramFiles&lt;/span&gt;&lt;span class="s2"&gt;\WindowsPowerShell\Modules;&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;SystemRoot&lt;/span&gt;&lt;span class="s2"&gt;\System32\WindowsPowerShell\v1.0\Modules"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$root&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;$PSScriptRoot&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$logs&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;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'logs'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$logs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ItemType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Directory&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;Start-Transcript&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$logs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{0}-{1:yyyyMMdd-HHmmss}.log'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Action&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="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="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$code&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;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;try&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="kr"&gt;switch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$Action&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="s1"&gt;'RestorePoint'&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;Enable-ComputerRestore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Drive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'C:\'&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SystemRestorePointCreationFrequency&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="nx"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DWord&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nx"&gt;Checkpoint-Computer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'WinSetup: manual restore point'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RestorePointType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MODIFY_SETTINGS&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="s1"&gt;'Tweaks'&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;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'01-tweaks.ps1'&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="s1"&gt;'Apps'&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;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'02-apps.ps1'&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;# installs only what's in apps.json, which lives in this folder too&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s1"&gt;'SaveDumps'&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;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'save-dumps.ps1'&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="s1"&gt;'Check'&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;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'01-tweaks.ps1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Check&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;# read-only drift report&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;span class="kr"&gt;catch&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;$code&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;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ERROR: &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;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&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="s2"&gt;"RESULT: &lt;/span&gt;&lt;span class="nv"&gt;$Action&lt;/span&gt;&lt;span class="s2"&gt; exit &lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Stop-Transcript&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;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$code&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;$ErrorActionPreference = 'Stop'&lt;/code&gt; turns every error into one the &lt;code&gt;catch&lt;/code&gt; sees, so a failed step can't end in a log that says &lt;code&gt;exit 0&lt;/code&gt;. The scripts the runner calls inherit it too. If one of them is meant to shrug off an error, say so on that line with &lt;code&gt;-ErrorAction Continue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every script the runner calls sits next to it in the protected folder. If an action called a script somewhere your normal account can write, that action would be "run whatever is in that file," as admin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Write the installer&lt;/strong&gt; (&lt;code&gt;install-admin-tasks.ps1&lt;/code&gt;). It locks the folder, copies the scripts in and registers one task per action. Rerun it whenever you change a script or the menu.&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="bp"&gt;$Error&lt;/span&gt;&lt;span class="n"&gt;ActionPreference&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;'Stop'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$id&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;Security.Principal.WindowsIdentity&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;GetCurrent&lt;/span&gt;&lt;span class="p"&gt;()&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="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Security.Principal.WindowsPrincipal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsInRole&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Security.Principal.WindowsBuiltInRole&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nx"&gt;Administrator&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="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Run this from an admin PowerShell.'&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;$src&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;$PSScriptRoot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dst&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;'C:\ProgramData\WinSetupAdmin'&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;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sid&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;$id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$actions&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;'RestorePoint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Tweaks'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Apps'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'SaveDumps'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Check'&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# The check the installer trusts: stop at any link, and if anyone but Administrators or SYSTEM owns, or can change,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# anything under $path. The walk is hand-rolled because Get-ChildItem -Recurse in Windows PowerShell follows junctions.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$trusted&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;'S-1-5-32-544'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'S-1-5-18'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$change&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;int&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;Security.AccessControl.FileSystemRights&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="s1"&gt;'Write, Delete, DeleteSubdirectoriesAndFiles, ChangePermissions, TakeOwnership'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-bor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x50000000&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# + generic all/write&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Assert-Locked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&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;$queue&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;Collections.Queue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;Get-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;$path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="kr"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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;$item&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;$queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dequeue&lt;/span&gt;&lt;span class="p"&gt;()&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;$item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Attributes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-band&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IO.FileAttributes&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ReparsePoint&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="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Link found: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;. Delete the folder and rerun."&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;$itemAcl&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-Acl&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;$item&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="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;$trusted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-notcontains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$itemAcl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOwner&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Security.Principal.SecurityIdentifier&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&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="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Not owned by Administrators: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;. Delete the folder and rerun."&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$itemAcl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAccessRules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$true&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="p"&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;Security.Principal.SecurityIdentifier&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="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;$r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AccessControlType&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;'Allow'&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;$trusted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-notcontains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IdentityReference&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&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;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$r&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;-band&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$change&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="kr"&gt;throw&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;$r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IdentityReference&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; can change &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;. Delete the folder and rerun."&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;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;$item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSIsContainer&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;Get-ChildItem&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;$item&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="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;$queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&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;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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Unregister the old tasks before touching the folder, so a run that stops below never leaves tasks pointing at a folder&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# that might not be locked (say, one you deleted and someone else recreated).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\WinSetup\'&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;Unregister-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# A new folder is created already locked, in one step: owner Administrators and one complete permission list&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# (Administrators + SYSTEM full control, Users read and run, nothing inherited), so nothing can be planted in it first.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# An existing folder is never re-permissioned (that would push the new list into whatever is inside). It's only reused&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# if it already passes the check; otherwise delete it and rerun.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$acl&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;Security.AccessControl.DirectorySecurity&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$acl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetSecurityDescriptorSddlForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'O:BAD:PAI(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;OICI;0x1200a9;;;BU)'&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;Test-Path&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;$dst&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;Assert-Locked&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dst&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="kr"&gt;else&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="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;IO.Directory&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;CreateDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$acl&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;# Delete and re-copy each script rather than overwrite it, so the new file inherits the list above instead of keeping old grants.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'runner.ps1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'01-tweaks.ps1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'02-apps.ps1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'apps.json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'save-dumps.ps1'&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;$target&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;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$f&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="n"&gt;Test-Path&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;$target&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;$target&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Copy-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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Destination&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;icacls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/setowner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'*S-1-5-32-544'&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;Out-Null&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;Assert-Locked&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# check the result instead of trusting it; no tasks get registered if this throws&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$ps&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;"&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;SystemRoot&lt;/span&gt;&lt;span class="s2"&gt;\System32\WindowsPowerShell\v1.0\powershell.exe"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# full path, not a PATH lookup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$svc&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="nt"&gt;-ComObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Schedule.Service'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$principal&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-ScheduledTaskPrincipal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UserId&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="nt"&gt;-LogonType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Interactive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RunLevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Highest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$settings&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-ScheduledTaskSettingsSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AllowStartIfOnBatteries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DontStopIfGoingOnBatteries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExecutionTimeLimit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;New-TimeSpan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Hours&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MultipleInstances&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IgnoreNew&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$actions&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;$act&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-ScheduledTaskAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Execute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Argument&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File &lt;/span&gt;&lt;span class="se"&gt;`"&lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="s2"&gt;\runner.ps1&lt;/span&gt;&lt;span class="se"&gt;`"&lt;/span&gt;&lt;span class="s2"&gt; -Action &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Register-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\WinSetup\'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$act&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Principal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$principal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Settings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$settings&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c"&gt;# Admins + SYSTEM full control; your account may read and start the task, not edit it. Without this flag,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c"&gt;# Task Scheduler quietly adds its own Allow entry for the account the task runs as, which is you.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$TASK_DONT_ADD_PRINCIPAL_ACE&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;0&lt;/span&gt;&lt;span class="n"&gt;x10&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFolder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'\WinSetup'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetSecurityDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"D:(A;;FA;;;BA)(A;;FA;;;SY)(A;;FRFX;;;&lt;/span&gt;&lt;span class="nv"&gt;$sid&lt;/span&gt;&lt;span class="s2"&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;$TASK_DONT_ADD_PRINCIPAL_ACE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c"&gt;# Read the stored list back: anyone but Administrators or SYSTEM with more than read and run fails the install.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$readRun&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="nx"&gt;0x1200A9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-bor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0xA0000000&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# FRFX, plus generic read/execute&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$stored&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;Security.AccessControl.RawSecurityDescriptor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFolder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'\WinSetup'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecurityDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$stored&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DiscretionaryAcl&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="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;$ace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AceQualifier&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;'AccessAllowed'&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="s1"&gt;'S-1-5-32-544'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'S-1-5-18'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-notcontains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SecurityIdentifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&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="nv"&gt;$ace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AccessMask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-band&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-bnot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$readRun&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;Unregister-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\WinSetup\'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt; gives &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$ace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SecurityIdentifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; more than read and run. Unregistered it."&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;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\WinSetup\'&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;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TaskName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes on what those lines do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The folder's permissions are written as one complete list (the SDDL string &lt;code&gt;O:BAD:PAI(...)&lt;/code&gt;), at the moment the folder is created, and each script is deleted and copied in fresh so it inherits that list. &lt;code&gt;icacls /grant:r&lt;/code&gt; alone only swaps the entries it names, so an old write grant for your account would survive a rerun, and so would your ability to swap the scripts the tasks run.&lt;/li&gt;
&lt;li&gt;Nothing privileged walks the folder or changes permissions on anything already inside it. Applying a permission list to an existing folder pushes it down into everything inside, and a junction planted in there could point that change at some other folder. So the installer only creates the folder locked, reuses one only if it's already locked, and touches nothing but the files it names. The check walks everything without following links. It refuses to register any tasks if it finds a link, an owner other than Administrators or SYSTEM, or anyone else who can change something. On a folder an older version left behind, that can mean deleting it and rerunning. The old tasks are unregistered before any of this, so a run that stops partway leaves no task pointing at the folder until a rerun gets all the way through.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-RunLevel Highest&lt;/code&gt; with &lt;code&gt;-LogonType Interactive&lt;/code&gt; means the task gets your full admin token, but only while you're signed in.&lt;/li&gt;
&lt;li&gt;The SDDL string at the end is the task's own permission list: &lt;code&gt;FA&lt;/code&gt; (full access) for Administrators (&lt;code&gt;BA&lt;/code&gt;) and SYSTEM (&lt;code&gt;SY&lt;/code&gt;), and only &lt;code&gt;FRFX&lt;/code&gt; (read and run) for your account. Flag &lt;code&gt;0x10&lt;/code&gt; (&lt;code&gt;TASK_DONT_ADD_PRINCIPAL_ACE&lt;/code&gt;) matters here. Without it, Task Scheduler adds its own Allow entry for the account the task runs as, and here that's you, so the stored list isn't the one you wrote. The installer then reads each task's list back and unregisters the task and stops if anyone but Administrators or SYSTEM has more than read and run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-ExecutionPolicy Bypass&lt;/code&gt; is PowerShell's script-signing setting, not UAC. It's there so the task can run unsigned local scripts. The real protection is that only admins can change those scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Install it from an admin window.&lt;/strong&gt; Open PowerShell with &lt;strong&gt;Run as administrator&lt;/strong&gt; and run:&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;powershell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoProfile&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="s2"&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;USERPROFILE&lt;/span&gt;&lt;span class="s2"&gt;\WinSetup\install-admin-tasks.ps1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the five tasks listed as &lt;code&gt;Ready&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Start a task from your normal account&lt;/strong&gt; (a regular, non-elevated PowerShell window), then read its log:&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;Start-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\WinSetup\'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Check'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Start-Sleep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10&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="nx"&gt;C:\ProgramData\WinSetupAdmin\logs&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;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;LastWriteTime&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;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&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;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'RESULT'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want &lt;code&gt;RESULT: Check exit 0&lt;/code&gt;. Open the whole log if it says anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Test the boundary from your normal account.&lt;/strong&gt; All three should print "blocked." If any prints "ALLOWED," stop and fix the permissions before you hand the menu to anything.&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;try&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;Set-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\ProgramData\WinSetupAdmin\probe.txt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-EA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'WRITE ALLOWED (bad)'&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="kr"&gt;catch&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="s1"&gt;'write blocked'&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="kr"&gt;try&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;Add-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\ProgramData\WinSetupAdmin\runner.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'#'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-EA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'EDIT ALLOWED (bad)'&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="kr"&gt;catch&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="s1"&gt;'edit blocked'&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;$svc&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="nt"&gt;-ComObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Schedule.Service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;try&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;$d&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;$svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFolder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'\WinSetup'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Check'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Definition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegistrationInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Description&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;'probe'&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nv"&gt;$svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFolder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'\WinSetup'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterTaskDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Check'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3&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="nx"&gt;Out-Null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'TASK EDIT ALLOWED (bad)'&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="kr"&gt;catch&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="s1"&gt;'task edit blocked'&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;strong&gt;7. Run external tools as their own process, with a time limit, and check the exit code.&lt;/strong&gt; If one misbehaves, read the tool's own log before you blame it. In my tweaks script that looks like this:&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;# --enable must be the only option; adding --disable-interactivity makes winget exit with a usage error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$wg&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;Start-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ArgumentList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'configure'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'--enable'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-WindowStyle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Hidden&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PassThru&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="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;300000&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;$wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Kill&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'winget configure --enable timed out after 5 min (skipped)'&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="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExitCode&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="mi"&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="s2"&gt;"winget configure --enable exited with &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExitCode&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;; check winget's log"&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;winget writes its logs under &lt;code&gt;%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\DiagOutputDir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveats&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This narrows what silent admin can do; it doesn't sandbox anything. Anything running as you can start these five tasks, so make sure every action is something you'd be fine with it starting at any time. Microsoft doesn't treat UAC as a security boundary either, so think of this as shrinking the blast radius from "anything" to "five verbs."&lt;/li&gt;
&lt;li&gt;The menu is only as narrow as the scripts behind it. "Apps" really means "install whatever is in &lt;code&gt;apps.json&lt;/code&gt;," which is why the list lives in the protected folder too.&lt;/li&gt;
&lt;li&gt;Watch what the scripts call. &lt;code&gt;winget&lt;/code&gt; resolves through a per-user folder, so any action that runs it trusts that folder. Keep the list of outside tools short.&lt;/li&gt;
&lt;li&gt;Changing anything means rerunning the installer elevated. That's the approval step, and it's on purpose.&lt;/li&gt;
&lt;li&gt;Undo everything from an admin window: &lt;code&gt;Get-ScheduledTask -TaskPath '\WinSetup\' | Unregister-ScheduledTask -Confirm:$false; Remove-Item C:\ProgramData\WinSetupAdmin -Recurse&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>windows11</category>
      <category>uac</category>
      <category>powershell</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>My OneDrive's problem was 322,219 files, not 268 GB</title>
      <dc:creator>Mikhail Predtetchenski</dc:creator>
      <pubDate>Sat, 31 Oct 2026 23:36:27 +0000</pubDate>
      <link>https://dev.to/mikepred/my-onedrives-problem-was-322219-files-not-268-gb-1md5</link>
      <guid>https://dev.to/mikepred/my-onedrives-problem-was-322219-files-not-268-gb-1md5</guid>
      <description>&lt;h2&gt;
  
  
  Case file
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incident:&lt;/strong&gt; Oct 25, 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; I wanted a folder plan for OneDrive. Instead I had a growing audit-tool spec, scripts with bugs, a 218 MB report I couldn't use, and later OneDrive saying "there are too many files in your "OneDrive" folder."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence:&lt;/strong&gt; the fixed audit counted 322,219 files and 105,150 folders (267.82 GB). All but 14 files were online-only. One local code repo held 296,714 of those files but only about 2.4 GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; a code repo with git worktrees and &lt;code&gt;node_modules&lt;/code&gt; was syncing inside OneDrive. Getting there took longer than it should have because the audit's scope kept growing (classifiers, clustering, checkpoints, ZIP packaging) before any version had produced an inventory I could use. Separately, the duplicate step crawled on that many files, most likely because of &lt;code&gt;Group-Object&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; narrowed the job to "fix the existing script and get one complete run." Rewrote the audit (never download online-only files, log errors, write output outside OneDrive, Dictionary grouping, progress bars), then deleted the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevention:&lt;/strong&gt; keep code repos out of OneDrive, check item counts and not just GB (Microsoft recommends syncing no more than 300,000 items), and ask for the smallest script that answers the question before adding features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; fix and run the narrow thing first. One honest full run answered what the spec was designing around. And OneDrive cares about how many items it tracks; size was never the issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The story
&lt;/h2&gt;

&lt;p&gt;This started as a cleanup, not a coding project. I asked Microsoft 365 Copilot to help me redo how my files are organized, starting with OneDrive, because I wanted to cut down the mental clutter of never knowing where anything lives. It proposed PARA (Projects, Areas, Resources, Archive). I wanted to keep the folders Windows already ships with, like Documents, Pictures, Music, and Videos, so we settled on a hybrid: Windows' libraries for media, and PARA-style folders next to them for everything else.&lt;/p&gt;

&lt;p&gt;To design that hierarchy, Copilot wanted an inventory, and it laid out options from a folder list up to "Option 3: Full Digital Archaeology." It actually recommended the middle one. I picked Option 3. Then I asked whether it could see any content or metadata, and it described levels of visibility up to content previews. Then I asked for one script that pulled everything. So I said yes to a lot of the scope myself.&lt;/p&gt;

&lt;p&gt;After that, each turn added more. The first script had a typo, so I asked Copilot to fix it and review the rest before writing a new one. It came back with a plan for "a production-grade audit script," and noted that a truly comprehensive one "gets large quickly (500-1000+ lines if done cleanly)," with SHA256 verification, ZIP packaging, parallel hashing, and Git and Obsidian detection. I asked it to plan v2 before writing it. The plan added a classifier, topic and project clustering, privacy levels, and a resume and checkpoint strategy. Copilot itself suggested freezing the scope there, and I agreed: "Freeze it. Write the ps1." It said a script that size should be a file, not a chat message, and told me the exact request to send for a downloadable .ps1. I sent it. The v2 it delivered came labeled as a "v2-alpha," with 13 features listed as still missing.&lt;/p&gt;

&lt;p&gt;So I took v2 to QA bot, one of my own Grok Bot assistants at the time (it was later rebuilt as Inspector, which is part of &lt;a href="https://mikepred.github.io/build-log/how-it-started/" rel="noopener noreferrer"&gt;how it started&lt;/a&gt;), with a narrower question: would this actually run? It would, but it had problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It saved its report to &lt;code&gt;$HOME\Desktop&lt;/code&gt;. With my Desktop backed up to OneDrive, that isn't my real Desktop, so I'd never have seen the report. And the obvious fix, the real Desktop, is inside OneDrive, where the report would show up in its own scan.&lt;/li&gt;
&lt;li&gt;The content-preview option ran &lt;code&gt;Get-Content&lt;/code&gt; on online-only files, which makes OneDrive quietly download every one of them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$ErrorActionPreference='SilentlyContinue'&lt;/code&gt; meant anything it couldn't read just vanished from the counts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-HashDuplicates&lt;/code&gt; was declared and never used.&lt;/li&gt;
&lt;li&gt;If nothing was a duplicate, it never wrote the duplicates CSV.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I got a fixed v3 back and showed it to Copilot. Its review was fair. It called v3 "a significant improvement over the version I generated" and singled out the output-outside-OneDrive fix and the cloud-only handling. Then it proposed a v3-beta, said again it would likely run 500-1000+ lines, and generated it. That one also came with a list of what was missing, including checkpoint/resume and memory optimizations for libraries over 100,000 files.&lt;/p&gt;

&lt;p&gt;v3-beta ran. While it did, I asked Copilot what upload size it could handle. It said it didn't have "a published hard limit" and gave rough ranges. The output came to 218 MB. The biggest pieces were Files.csv at about 80 MB and AgeAnalysis.csv at about 59 MB. Copilot's read of that output was accurate: it called AgeAnalysis.csv a "redundant data explosion; essentially re-listing every file" that should have been a few-KB summary. It also guessed my OneDrive held around 100,000 files. The real number turned out to be more than three times that.&lt;/p&gt;

&lt;p&gt;At that point I had a lot of plans and still no hierarchy, so I changed approach. I went back to QA bot with one job: get one complete run I could trust.&lt;/p&gt;

&lt;p&gt;It went through v3-beta. The new reports were genuinely good: empty folders, folders with more than 1,000 files, folder sizes, files older than five years, and common words in file names. But v3-beta had also broken things that already worked. The preview switch did nothing again. Scan errors were collected and never written anywhere. Duplicate hashing hashed every single file instead of only same-size ones, built its list with &lt;code&gt;$hashed +=&lt;/code&gt; (which copies the whole array on every add), and flagged every empty file as a duplicate of every other one. The folder tree, the age breakdown, and the total size dropped out of the summary. And its CSV-safety function rewrote real file names that start with &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;@&lt;/code&gt;. So we kept v3 as the base and merged Copilot's five reports in properly. That was v3.1.&lt;/p&gt;

&lt;p&gt;v3.1 ran, and then it seemed to stop. The output folder had 12 files, then sat there. Task Manager showed pwsh at 2.6 GB of memory. Twelve minutes later it was still going. The 12th file was the file-name topics report, which meant it was on the duplicate step, and that step used &lt;code&gt;Group-Object Name, Length&lt;/code&gt; across every file. &lt;code&gt;Group-Object&lt;/code&gt; gets very slow at that scale, so it was the likely culprit. v3.2 swapped it for a plain Dictionary keyed on name and size. It also added a live progress bar to the scan and every long step, plus a "Done in hh:mm:ss" line, so I'd never again be staring at a silent window wondering whether it was dead.&lt;/p&gt;

&lt;p&gt;v3.2 finished in about 11 minutes with zero skipped paths. On this run the duplicate step took about a minute.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fonedrive-audit-file-count-not-size%2Fwide-vs-narrow.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fonedrive-audit-file-count-not-size%2Fwide-vs-narrow.svg" alt="Two ways the audit went: the wide path stacked up the biggest options, a 500-1000+ line spec, alpha and beta scripts with lists of missing features, and 218 MB of reports. The narrow path fixed the existing script, found the stall in Group-Object, swapped in a Dictionary with progress bars, and got one full 11-minute run whose summary showed the answer" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;322,219 files in 105,150 folders, 267.82 GB&lt;/li&gt;
&lt;li&gt;322,205 of those files online-only, so the scan read metadata and downloaded nothing&lt;/li&gt;
&lt;li&gt;Pictures: about 251 GB, including 78 GB in just 13 Google Takeout zips&lt;/li&gt;
&lt;li&gt;Top file types by count: &lt;code&gt;.html&lt;/code&gt;, &lt;code&gt;.pf_fragment&lt;/code&gt;, &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line didn't look like my stuff. It was one local code repo, git worktrees and their &lt;code&gt;node_modules&lt;/code&gt; included, with 296,714 files. That's about 92% of every file in my OneDrive, and it took up only about 2.4 GB, less than 1% of the space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fonedrive-audit-file-count-not-size%2Fcount-vs-size.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fonedrive-audit-file-count-not-size%2Fcount-vs-size.svg" alt="Three bars from the audit. Items OneDrive tracks: 427,369 files and folders, past Microsoft's 300,000-item sync recommendation. Files: 322,219, and one code repo is 296,714 of them, about 92%. Space: 267.82 GB, and the same repo is about 2.4 GB, under 1%" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd already decided all code could leave OneDrive, so the repo got deleted. Deleting the files locally took about two and a half minutes. An empty top folder stayed behind because another app still had it open. That was the easy part.&lt;/p&gt;

&lt;p&gt;A few hours later I asked why it was still deleting files. It wasn't my delete. It was OneDrive telling the cloud about every one of those roughly 297,000 deletions. Then OneDrive popped up: "there are too many files in your "OneDrive" folder." QA bot explained that the warning is about how many items OneDrive tracks, not about storage. Microsoft &lt;a href="https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa" rel="noopener noreferrer"&gt;recommends syncing no more than 300,000 items&lt;/a&gt;, files and folders together, and says performance can suffer past that. It's a guideline, not a hard cap, but my audit had counted about 427,000. Once the deletions finished syncing I'd be down to about 25,000 files. When I wrote this up that morning, more than three hours after the delete, OneDrive was still working through them.&lt;/p&gt;

&lt;p&gt;So the 268 GB number I'd been worried about was mostly photos, and that's a normal storage problem. The thing actually slowing OneDrive down was a 2.4 GB folder. No classifier or clustering engine found that. A script that finished, and a summary I actually read, did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do it yourself
&lt;/h2&gt;

&lt;p&gt;These snippets are trimmed from my v3.2 script. Run them in order in one PowerShell window (Windows PowerShell 5.1 or PowerShell 7, no admin needed), since each step uses variables from the one before. Steps 1 to 6 only read OneDrive (step 3 writes one error CSV outside it). Step 7 deletes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Find OneDrive and keep the report out of it.&lt;/strong&gt; With Known Folder Move on, &lt;code&gt;$HOME\Desktop&lt;/code&gt; is the old, unused folder. &lt;code&gt;[Environment]::GetFolderPath('Desktop')&lt;/code&gt; returns the Desktop you actually see, which may be inside OneDrive, so check before writing there.&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;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&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;OneDrive&lt;/span&gt;&lt;span class="p"&gt;,&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;OneDriveCommercial&lt;/span&gt;&lt;span class="p"&gt;,&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;OneDriveConsumer&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="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;$candidate&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;Test-Path&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;$candidate&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;$OneDrivePath&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;$candidate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;break&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;span class="nv"&gt;$desktop&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;Environment&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;GetFolderPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Desktop'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$inOneDrive&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;$desktop&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;$desktop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$OneDrivePath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&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="s1"&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="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nx"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&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;$desktop&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="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$inOneDrive&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;$OutputDir&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;$desktop&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="kr"&gt;else&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;$OutputDir&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;$&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;USERPROFILE&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;strong&gt;2. Flag online-only files instead of opening them.&lt;/strong&gt;&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;$RecallOnDataAccess&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;0&lt;/span&gt;&lt;span class="n"&gt;x400000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$RecallOnOpen&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;0&lt;/span&gt;&lt;span class="n"&gt;x40000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Test-CloudOnly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$Item&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;$a&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;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Attributes&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-band&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$RecallOnDataAccess&lt;/span&gt;&lt;span class="p"&gt;)&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-band&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$RecallOnOpen&lt;/span&gt;&lt;span class="p"&gt;)&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-band&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;IO.FileAttributes&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Offline&lt;/span&gt;&lt;span class="p"&gt;)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip hashing and previews for anything this returns &lt;code&gt;$true&lt;/code&gt; for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Scan with errors logged and a live count.&lt;/strong&gt; Do not use &lt;code&gt;Get-ChildItem -Recurse&lt;/code&gt; on Windows PowerShell 5.1: it follows directory junctions and directory symlinks, which can count the same files twice (for example pnpm-style &lt;code&gt;node_modules&lt;/code&gt; links) or walk outside OneDrive. That wasn't what inflated my count; the repo really had that many files. PowerShell 7 only follows them if you pass &lt;code&gt;-FollowSymlink&lt;/code&gt;. This walk lists every item, including the link folder itself, and descends into every folder except junctions and symlinks. It checks &lt;code&gt;LinkType&lt;/code&gt; rather than the &lt;code&gt;ReparsePoint&lt;/code&gt; attribute because online-only OneDrive folders are reparse points too, and skipping those would leave their contents out of the count.&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;$scanErrors&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="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$allList&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="s1"&gt;'System.Collections.Generic.List[object]'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$dirs&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="s1"&gt;'System.Collections.Generic.Queue[string]'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$dirs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$OneDrivePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dirs&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="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&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="nv"&gt;$dir&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;$dirs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dequeue&lt;/span&gt;&lt;span class="p"&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;-LiteralPath&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;-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="nt"&gt;-ErrorVariable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;scanErrors&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;$allList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;)&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;$allList&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="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2000&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="mi"&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;Write-Progress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Activity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Scanning OneDrive'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Status&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;$allList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; items"&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="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;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSIsContainer&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;LinkType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-notin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Junction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'SymbolicLink'&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;$dirs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&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;FullName&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="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-Progress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Activity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Scanning OneDrive'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Completed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$fileRows&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="nv"&gt;$allList&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="o"&gt;-not&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;PSIsContainer&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;$scanErrors&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pscustomobject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Path&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="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TargetObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Error&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="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Exception&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&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;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Export-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$OutputDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'onedrive-scan-errors.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoTypeInformation&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"Skipped paths: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$scanErrors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SilentlyContinue&lt;/code&gt; is fine here because &lt;code&gt;-ErrorVariable&lt;/code&gt; still collects every skipped path, and the CSV shows you what's missing from the counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Group duplicates with a Dictionary, not &lt;code&gt;Group-Object&lt;/code&gt;.&lt;/strong&gt;&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;$byNameSize&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="s1"&gt;'System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[object]]'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;System.StringComparer&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$fileRows&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;$key&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;$f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&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;'|'&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;$f&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="nv"&gt;$list&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;$null&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="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$byNameSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&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="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$list&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;$list&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="s1"&gt;'System.Collections.Generic.List[object]'&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$byNameSize&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$key&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;$list&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;$list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$f&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;$dupeGroups&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="nv"&gt;$byNameSize&lt;/span&gt;&lt;span class="err"&gt;.Values&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;Count&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="mi"&gt;1&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="s2"&gt;"Name + size matches: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$dupeGroups&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; groups"&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;5. If you hash, hash only files that share a size with another file.&lt;/strong&gt; Empty files all "match" each other, and online-only files would download, so leave both out.&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;$sizeCount&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="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$fileRows&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;$sizeCount&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;long&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&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;1&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;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$sizeCount&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;long&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&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;$toHash&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="nv"&gt;$fileRows&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;Length&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="mi"&gt;0&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;$sizeCount&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;long&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;Length&lt;/span&gt;&lt;span class="p"&gt;]&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="mi"&gt;1&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="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-CloudOnly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&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;span class="s2"&gt;"Files worth hashing: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$toHash&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Count items, not just gigabytes.&lt;/strong&gt; Files plus folders is the number Microsoft's 300,000-item sync recommendation is about.&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;$folderCount&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;$allList&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="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$fileRows&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="nv"&gt;$gb&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="nv"&gt;$fileRows&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;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Sum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1GB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s1"&gt;'{0:N0} files + {1:N0} folders = {2:N0} items, {3:N2} GB'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$fileRows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$folderCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$allList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$gb&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No script handy? Right-click the OneDrive folder in File Explorer, pick &lt;strong&gt;Properties&lt;/strong&gt;, and read the &lt;strong&gt;Contains&lt;/strong&gt; line. That's Microsoft's own quick check, and it counts the local folder only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Deleting a big folder.&lt;/strong&gt; I used &lt;code&gt;rd&lt;/code&gt; with the long-path prefix. Close anything that has the folder open first (editors, terminals, dev servers).&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;$repo&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;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$OneDrivePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'path\to\the-repo'&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# set this to the exact folder&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cmd.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/q&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"\\?\&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&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;strong&gt;Caveats&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start small. Ask for (or write) the smallest script that answers your actual question, run it on the whole thing, and only then add features. Classification, clustering, and checkpoints can wait until a full run shows you need them.&lt;/li&gt;
&lt;li&gt;Reading an online-only file's contents (previews, &lt;code&gt;Get-FileHash&lt;/code&gt;, &lt;code&gt;Get-Content&lt;/code&gt;) downloads it. On a mostly cloud OneDrive, that can mean a lot of downloading.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rd /s /q&lt;/code&gt; doesn't ask, and it skips the Windows Recycle Bin. Double-check &lt;code&gt;$repo&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;Deletions inside OneDrive still sync to the OneDrive recycle bin online. You can restore from there for 30 days on a personal account (93 on work or school, unless an admin changed it). Don't count on it as a backup.&lt;/li&gt;
&lt;li&gt;Deleting locally is quick. Syncing hundreds of thousands of deletions to the cloud can take hours. Leave OneDrive running, and don't re-run the audit until it says it's up to date.&lt;/li&gt;
&lt;li&gt;The 300,000-item number is Microsoft's performance guideline, not a hard limit. Past it, expect slow syncing, not lost files.&lt;/li&gt;
&lt;li&gt;The scan holds every item in memory. Mine had pwsh at 2.6 GB with about 427,000 items.&lt;/li&gt;
&lt;li&gt;Junction and symlink folders still appear in the inventory once. Their targets are not walked, so linked folders are not counted twice and a link out of OneDrive is not followed. Online-only cloud folders are walked normally.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>onedrive</category>
      <category>powershell</category>
      <category>windows11</category>
      <category>aiassistants</category>
    </item>
    <item>
      <title>Windows Update pushed an AMD chipset driver over mine and broke boot</title>
      <dc:creator>Mikhail Predtetchenski</dc:creator>
      <pubDate>Sat, 31 Oct 2026 09:21:28 +0000</pubDate>
      <link>https://dev.to/mikepred/windows-update-pushed-an-amd-chipset-driver-over-mine-and-broke-boot-1h7l</link>
      <guid>https://dev.to/mikepred/windows-update-pushed-an-amd-chipset-driver-over-mine-and-broke-boot-1h7l</guid>
      <description>&lt;h2&gt;
  
  
  Case file
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; after a fresh Windows 11 Pro install plus Gigabyte chipset drivers, Windows Update ran, the PC restarted, and it got stuck in a boot loop that ended in Automatic Repair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence:&lt;/strong&gt; &lt;code&gt;SrtTrail.txt&lt;/code&gt; said "a recent driver update installation could be preventing Windows from starting." Update history showed "AMD - System 20.50.0.0" installed at 3:07 AM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Likely cause:&lt;/strong&gt; Windows Update installed its own AMD driver for the CPU's PCIe root ports on top of the vendor chipset package. No dump or boot log survived from the failed boots, but the repair log, the timing and the rollback all point there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Startup Repair uninstalled that driver on its own; Continue booted normally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevention:&lt;/strong&gt; &lt;code&gt;ExcludeWUDriversInQualityUpdate = 1&lt;/code&gt;, plus crash-evidence settings: keep dumps, AutoReboot off, boot logging, and a startup task that copies dumps aside. Settings verified live; the copy task hasn't had a crash to catch yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; run Windows Update &lt;em&gt;before&lt;/em&gt; vendor drivers, or block its drivers entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The story
&lt;/h2&gt;

&lt;p&gt;I'd just reinstalled Windows 11 Pro on my desktop (Ryzen 9 5900X on a Gigabyte X570 AORUS MASTER, RTX 5070 Ti). I did the responsible-adult thing and installed the chipset drivers straight from Gigabyte's site, then the NVIDIA Studio driver. Then I ran Windows Update. In hindsight, that probably should have been step one.&lt;/p&gt;

&lt;p&gt;A couple of updates went in, the PC restarted, and then nothing. Black screen. I honestly thought I wasn't even getting POST. I hard-shut it down, tried again, same thing, and after a few rounds Windows gave up and showed "Automatic Repair" with something about &lt;code&gt;SrtTrail.txt&lt;/code&gt;. It cycled like that until it dropped me at a recovery screen, where I just hit Continue and Windows loaded like nothing had happened.&lt;/p&gt;

&lt;p&gt;It worked, but I wanted to know why, so I had dr eggbot, the Grok Bot I was working with that night, pull three things: the Startup Repair log, the Windows Update history, and the boot and crash events from the System log.&lt;/p&gt;

&lt;p&gt;The repair log was the jackpot. It listed exactly one cause: "a recent driver update installation could be preventing Windows from starting." Update history filled in the rest. At 3:07 AM, Windows Update had installed "AMD - System 20.50.0.0," a driver for the CPU's PCIe root ports, right on top of the Gigabyte package I'd just put on. Startup Repair's fix was to uninstall it, which is why Continue worked. Checking afterward, those root ports were back on Microsoft's standard driver, Device Manager had no problem devices, and my Gigabyte AMD drivers were still in place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fwindows-update-amd-chipset-driver-broke-boot%2Fboot-loop-timeline.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fwindows-update-amd-chipset-driver-broke-boot%2Fboot-loop-timeline.svg" alt="Timeline of the night: fresh Windows 11 install, Gigabyte chipset and NVIDIA drivers, Windows Update installs AMD - System 20.50.0.0 at 3:07 AM, boot loop into Automatic Repair, Startup Repair uninstalls that driver, then a policy blocks Windows Update drivers" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One correction to my own story: a Windows driver can't stop the board from POSTing. Most likely the hang came right after POST while the screen was still dark, and my hard shutdowns are what kicked off Automatic Repair after a couple of tries.&lt;/p&gt;

&lt;p&gt;There was a side quest. Earlier that night, after the first batch of updates (Realtek, Intel Bluetooth, Logitech), Windows had blue-screened with &lt;code&gt;0x4E PFN_LIST_CORRUPT&lt;/code&gt;. That one usually means a misbehaving driver, and less often RAM, and the logs alone couldn't say which. The event log said a dump had been saved to &lt;code&gt;C:\Windows\Minidump&lt;/code&gt;. I went to look. Empty. I ran a search of the other places Windows sometimes stashes a copy, from an admin window. Nothing. The dump was gone, and I dropped it: one crash in the middle of a big driver shuffle, and it hadn't come back. If another &lt;code&gt;0x4E&lt;/code&gt; shows up, a memory test is next.&lt;/p&gt;

&lt;p&gt;The real fix is boring: stop Windows Update from sending drivers at all. I install mine from Gigabyte and NVIDIA anyway. That's one policy value, &lt;code&gt;ExcludeWUDriversInQualityUpdate&lt;/code&gt;. I set it by hand on this PC, and it's also in my tweaks script so a fresh install never repeats this. Security and feature updates still come through. Later that morning, Bench Tech, a PC-maintenance Grok Bot built to look after this machine, ran its first health check and confirmed the block was still on.&lt;/p&gt;

&lt;p&gt;Losing that dump bugged me, though, so I asked the follow-up question: if this happens again, how do I catch the evidence before it disappears? It turns out that in a boot loop, Windows holds the crash data in the page file and only writes it out as a dump file on the next &lt;em&gt;successful&lt;/em&gt; boot. So the setup now (confirmed with the script's own check, which came back "All settings match."):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keeps the memory dump even when disk space is low, and saves an automatic memory dump (&lt;code&gt;MEMORY.DMP&lt;/code&gt;) alongside the small one&lt;/li&gt;
&lt;li&gt;stays on the blue screen instead of instantly rebooting, so I can take a phone photo of the stop code&lt;/li&gt;
&lt;li&gt;turns on boot logging, so if a boot hangs with no blue screen, &lt;code&gt;ntbtlog.txt&lt;/code&gt; gives me something to compare against a good boot&lt;/li&gt;
&lt;li&gt;runs a startup task a minute after every boot that copies any new dump into a folder cleanup tools don't touch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fwindows-update-amd-chipset-driver-broke-boot%2Fcrash-evidence-flow.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fwindows-update-amd-chipset-driver-broke-boot%2Fcrash-evidence-flow.svg" alt="How crash evidence survives now: a blue screen stays on screen for a photo, the crash data waits in the page file, the next successful boot writes MEMORY.DMP and a minidump, and a startup task copies them to C:\CrashVault. A hang with no blue screen leaves a section in ntbtlog.txt to compare against a Safe Mode boot" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Do it yourself
&lt;/h2&gt;

&lt;p&gt;These are PowerShell commands. Anything that writes to &lt;code&gt;HKLM&lt;/code&gt;, &lt;code&gt;bcdedit&lt;/code&gt;, or registers a SYSTEM task needs an &lt;strong&gt;elevated&lt;/strong&gt; PowerShell window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Read the Startup Repair log.&lt;/strong&gt;&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-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Windows\System32\LogFiles\Srt\SrtTrail.txt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the root cause line. Mine named "a recent driver update installation." If you're still stuck in recovery, open &lt;strong&gt;Troubleshoot &amp;gt; Advanced options &amp;gt; Command Prompt&lt;/strong&gt; and run &lt;code&gt;type C:\Windows\System32\LogFiles\Srt\SrtTrail.txt&lt;/code&gt; instead (that's cmd, not PowerShell). In recovery the Windows drive can have a different letter, so try &lt;code&gt;D:&lt;/code&gt; if &lt;code&gt;C:&lt;/code&gt; doesn't have it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check what Windows Update installed, and when.&lt;/strong&gt;&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;$s&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="nt"&gt;-ComObject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.Update.Session&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$h&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;$s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateUpdateSearcher&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueryHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;25&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="s1"&gt;'{0:yyyy-MM-dd HH:mm}  result={1}  {2}'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&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;Date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLocalTime&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;ResultCode&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;Title&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;Line the timestamps up against your failed boot. &lt;code&gt;result=2&lt;/code&gt; means it installed, &lt;code&gt;3&lt;/code&gt; installed with errors, &lt;code&gt;4&lt;/code&gt; failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Look for an earlier bugcheck.&lt;/strong&gt;&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-WinEvent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilterHashtable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;LogName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'System'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ProviderName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Microsoft-Windows-WER-SystemErrorReporting'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MaxEvents&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;-EA&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;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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TimeCreated&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;Message&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;strong&gt;4. Stop Windows Update from installing drivers.&lt;/strong&gt; This is the same as the Group Policy "Do not include drivers with Windows Updates."&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;$wu&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;'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'&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="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$wu&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;New-Item&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;$wu&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&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;New-ItemProperty&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;$wu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ExcludeWUDriversInQualityUpdate&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="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DWord&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&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;5. Keep crash evidence.&lt;/strong&gt;&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;$cc&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;'HKLM:\SYSTEM\CurrentControlSet\Control\CrashControl'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CrashDumpEnabled&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="nx"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DWord&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c"&gt;# automatic memory dump (MEMORY.DMP + minidump)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AlwaysKeepMemoryDump&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="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DWord&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# don't delete MEMORY.DMP when disk space is low&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AutoReboot&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="nx"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DWord&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c"&gt;# stay on the blue screen so you can read the stop code&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;bcdedit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{current}'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bootlog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;yes&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c"&gt;# writes C:\Windows\ntbtlog.txt at boot&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading &lt;code&gt;ntbtlog.txt&lt;/code&gt; takes a little care. Every boot adds its own section to the end (a version line, then a timestamp), so cut out just the failed boot's section first. Its last few lines are leads, not a verdict. The log records what Windows decided to load ("Loaded driver" / "Did not load driver"), not which driver hung. A better test: drop the "Did not load driver" lines from the failed boot's section and from a Safe Mode boot's section, then compare the two. Drivers that only load in the normal boot are your suspects. If the failed boot has no section at all, or an empty one, the hang came before Windows could write the log to disk, and the log can't help with that one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Copy dumps aside at every startup.&lt;/strong&gt; Create the folder first (&lt;code&gt;New-Item C:\ProgramData\WinSetup -ItemType Directory -Force&lt;/code&gt;), then save this as &lt;code&gt;C:\ProgramData\WinSetup\save-dumps.ps1&lt;/code&gt;:&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;$vault&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;'C:\CrashVault'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ItemType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Directory&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'C:\Windows\Minidump\*.dmp'&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;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;$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="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$vault&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;Name&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="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dest&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;Copy-Item&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;FullName&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="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;$mem&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-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'C:\Windows\MEMORY.DMP'&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="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;$mem&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;$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="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$vault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MEMORY-{0:yyyyMMdd-HHmmss}.DMP'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$mem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastWriteTime&lt;/span&gt;&lt;span class="p"&gt;)&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="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dest&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;Copy-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$mem&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="nv"&gt;$dest&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;Then register it to run as SYSTEM one minute after boot:&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;$act&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-ScheduledTaskAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Execute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'powershell.exe'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Argument&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'-NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\WinSetup\save-dumps.ps1"'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$trig&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-ScheduledTaskTrigger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AtStartup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$trig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&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;'PT1M'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Register-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'SaveCrashDumps'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$act&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Trigger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$trig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'SYSTEM'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RunLevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Highest&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&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;Caveats&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With the driver policy on, you own driver updates. Get chipset and GPU drivers from the vendor yourself.&lt;/li&gt;
&lt;li&gt;Dumps only land after a successful boot. The startup task catches them then, before cleanup can.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MEMORY.DMP&lt;/code&gt; can be several GB; the script names each copy by timestamp so it isn't re-copied every boot.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ntbtlog.txt&lt;/code&gt; keeps growing while boot logging is on.&lt;/li&gt;
&lt;li&gt;With AutoReboot off, the PC sits on the blue screen until you restart it. Photograph the stop code and any driver name first.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>windows11</category>
      <category>windowsupdate</category>
      <category>drivers</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>How it started: nine bots in a week, then back to one</title>
      <dc:creator>Mikhail Predtetchenski</dc:creator>
      <pubDate>Sat, 31 Oct 2026 06:38:54 +0000</pubDate>
      <link>https://dev.to/mikepred/how-it-started-nine-bots-in-a-week-then-back-to-one-18e1</link>
      <guid>https://dev.to/mikepred/how-it-started-nine-bots-in-a-week-then-back-to-one-18e1</guid>
      <description>&lt;h2&gt;
  
  
  Case file
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; I wanted AI help with my PC and my projects, and a public record of it, without designing a whole system up front or tracking everything by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence:&lt;/strong&gt; one week, one Windows PC, and a pile of chats: one bot on the first night, three by sunrise, seven by the next morning, nine by the end. Fourteen draft entries sat in pull requests, none of them merged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; every bot was added for a real reason, but each one was another chat to visit, another set of instructions to keep current, and another place for work to get stuck. And I had the team architect minting new bots before I fully understood what we were building.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; on October 30, 2026 I deleted the whole team and handed the build log to one bot, HQ. It's my main workbench, and this log is a byproduct of the work I do there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevention:&lt;/strong&gt; the rules the team taught me stay, even with one bot: evidence before structure, one job per role with written anti-jobs, a checker that owns no code, a second round of arguing, real fixes as the only content, code for anything with no judgment in it, and respect for how much context one bot can hold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; the team was the scaffolding. The rules are what I'm keeping.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The story
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Night one: Patient Zero
&lt;/h3&gt;

&lt;p&gt;At 1:59 AM on Saturday, October 24, I opened a chat with dr eggbot, a Grok Bot whose whole job is designing other Grok Bots, and gave it a strange first message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let's consider you "Patient Zero." I don't yet know what kind of bot I want or need yet. It will emerge at your discretion based on what I ask of you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't want to spec out a team I'd never used. dr eggbot turned that into a rule: when the same kind of ask came back a few times, it would tell me which bot it should become, and I'd say yes or no.&lt;/p&gt;

&lt;p&gt;Ten minutes later my freshly reinstalled Windows 11 PC became "Patient One," and the night turned into real work: setup scripts I could rerun, a boot loop caused by a Windows Update driver, crash dumps that actually stick around, and a small menu of admin tasks the assistant could start but not edit. Around 4:21 AM I added a side note: I wanted all this to turn into a public journal, a portfolio, without tracking it by hand. By 5:23 AM dr eggbot had made the first new bot, Case Notes, which turns real sessions into entries and never publishes anything itself.&lt;/p&gt;

&lt;p&gt;At 6:16 AM I asked whether there was enough for a bot to run the build-log sessions. I was half expecting a do-anything worker. dr eggbot said no to that and yes to something narrower: nearly every case so far had been my PC, and "a general 'do any session' bot would just duplicate me." At 6:18 AM Bench Tech existed, with one machine to look after and a list of things it must never do without my yes: restarts, BIOS, firmware, driver installs, apps I didn't ask for.&lt;/p&gt;

&lt;p&gt;Then I nearly added a third bot for no reason. Should Case Notes draft &lt;em&gt;and&lt;/em&gt; publish, or should a publisher bot do that? dr eggbot said publishing "is fixed steps with no judgment in them, so code does it more reliably than a bot." The bot I was tempted to add turned out to be a GitHub Action. By sunrise I had three bots, each with one job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Night two: lanes
&lt;/h3&gt;

&lt;p&gt;By early Sunday I had five: dr eggbot, Bench Tech, Case Notes, and two I was still testing.&lt;/p&gt;

&lt;p&gt;One was &lt;strong&gt;QA bot&lt;/strong&gt;. It came from a marketplace template built to gate production deploys, and I'd been using it to check PowerShell that Microsoft 365 Copilot wrote for me. When I tried to say what it was for, the honest word was "confusing." Sorting that out gave me a rule: bots that write code are makers, one bot checks, and the checker owns no code. QA bot was rebuilt as &lt;strong&gt;Inspector&lt;/strong&gt;: verdict first, findings marked proven or reasoned, a minimal patch handed back and never applied.&lt;/p&gt;

&lt;p&gt;The other was &lt;strong&gt;Grok Sparring&lt;/strong&gt;. It takes a question to Grok on grok.com, pushes back on the first answer, and brings back one settled answer plus a list of what nobody verified. In one night of four rounds, Grok reversed itself once, dropped a bad idea once, and cited sources nobody had checked. Every bit of the value was in round two.&lt;/p&gt;

&lt;p&gt;At 3:50 AM, with five bots and a dozen open threads, I typed that I was "jumping between things, getting confused, and getting a little frustrated with myself." dr eggbot's answer is the line I remember most from the whole week:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;getting lost isn't a you problem. there's just no one bot whose job is the big picture.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the big picture got a lane. &lt;strong&gt;Front Desk&lt;/strong&gt; tracked and routed work but couldn't create bots. dr eggbot stretched from "bot minter" to "team architect." &lt;strong&gt;Stacks&lt;/strong&gt; became the librarian for private notes. Every bot got the same two standing rules: off-scope asks get a short answer plus a quiet note to the architect, and half-formed ideas get parked with Stacks. By 4:45 AM it was, in the team page's own words, "seven bots, split into lanes: one front door, three makers, one checker, one second opinion, and an architect."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fhow-it-started%2Fteam-timeline.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fhow-it-started%2Fteam-timeline.svg" alt="The team over one week: one bot on night one, three by sunrise, seven after night two, nine at the peak, then back to one on October 30" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The rest of the week
&lt;/h3&gt;

&lt;p&gt;The last two showed up the way the rest did, some on the same day and some spread over a few days. I honestly don't remember the exact order. One was &lt;strong&gt;Student Discount Finder&lt;/strong&gt;. The other was &lt;strong&gt;Hermes Agent Fixer&lt;/strong&gt;. Neither came from a pile of repeated asks. I just liked the idea that every bot should have its own lane, so that when one conversation got long, the history wouldn't be stuck in one place and another bot could pick it up.&lt;/p&gt;

&lt;p&gt;The build log itself came out of that week too. A PRD I'd been handed for an "autonomous content engine" wanted ten channels, affiliate links, daily posts "even when no article is released," and 50 articles in six months. It got called what it was, a content farm. What survived is one markdown file per real problem, a pull request, and my merge as the only approval. My own words from that session are basically this log's mission statement: "Errors come up through some course of work, the process of fixing it becomes the content."&lt;/p&gt;

&lt;p&gt;By the end of the week the team had drafted fourteen entries. All of them were still open pull requests. None had shipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  October 30: back to one
&lt;/h3&gt;

&lt;p&gt;On October 30, 2026 I deleted all nine bots.&lt;/p&gt;

&lt;p&gt;What tipped it: I had the librarian, Stacks, start working with the debate partner, Grok Sparring, to go outside the platform and ask for more revisions to my Obsidian vault. What came back were revisions I couldn't follow. When I can't follow what my own bots did to my own notes, there are too many of them.&lt;/p&gt;

&lt;p&gt;The work moves to one bot, &lt;strong&gt;HQ&lt;/strong&gt;. It's my main workbench: I do the actual work there, and this build log is a byproduct of that work instead of a separate team's job. Its first act was a survey of what the team left behind: an Astro site that had never published a post, fourteen draft pull requests, and a cross-posting pipeline that had never run. The plan: a running log page, this retro, then the three drafts closest to done, then archive the rest as attempt #1.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fhow-it-started%2Fthen-and-now.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmikepred.github.io%2Fbuild-log%2Fimages%2Fhow-it-started%2Fthen-and-now.svg" alt="Then and now: nine bots with separate lanes, versus one workbench where the work happens, a draft entry, my merge, and a GitHub Action that ships it" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deleting the team doesn't mean the week was wasted. Nearly every rule I'd keep came out of a specific moment with a specific bot. I just don't need nine chats to follow them. Less is more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do it yourself
&lt;/h2&gt;

&lt;p&gt;These are the seven rules I'm keeping. They work with one assistant or with ten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Evidence before you add a bot.&lt;/strong&gt; Start with one general assistant and tell it the rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I don't know what bots I need yet. Do the work yourself.
When the same kind of ask comes back two or three times,
tell me which bot it should become, and I'll say yes or no.
Don't suggest a bot on a single ask.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it proposes one, ask what asks it's counting. "Nearly every case was this one PC" is evidence. "You might want..." isn't. The flip side: if the asks stop coming, the bot shouldn't exist anymore either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. One job per bot, with written anti-jobs.&lt;/strong&gt; For every bot (or every role, if you only have one bot), write one line for its job and a short list of what it never does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;One job&lt;/th&gt;
&lt;th&gt;Never does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PC tech&lt;/td&gt;
&lt;td&gt;keeps one Windows PC healthy&lt;/td&gt;
&lt;td&gt;restarts, BIOS, firmware, or driver installs without a yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writer&lt;/td&gt;
&lt;td&gt;drafts entries from real sessions&lt;/td&gt;
&lt;td&gt;publishes or merges on its own; invents steps or numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checker&lt;/td&gt;
&lt;td&gt;reviews code and scripts&lt;/td&gt;
&lt;td&gt;applies, pushes, merges, or deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second opinion&lt;/td&gt;
&lt;td&gt;pressure-tests a question with another AI&lt;/td&gt;
&lt;td&gt;acts on that AI's advice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;3. Makers and one checker.&lt;/strong&gt; Whoever writes the code owns it and is the only one who changes it. The checker owns nothing and reviews everything: verdict first, each finding marked proven or reasoned, a minimal patch handed back and never applied. A bot checking its own work misses things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Argue for a second round.&lt;/strong&gt; Don't act on a first answer. Push back with specifics (your own rules, its sources, the smallest version that would work), then keep a short list of what nobody verified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here's your answer. Push on it:
- Does it break any rule you set in the same answer?
- Which sources did you actually check?
- What's the smallest version of this that would work?
Give me one settled answer, what changed, and what's still unverified.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. The process of fixing is the content.&lt;/strong&gt; If you want your work to double as a portfolio, publish the real problem and the real fix, one entry per case, and make your own merge the only approval. No daily filler, no affiliate links, no fifty-articles goal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Code over bots for deterministic jobs.&lt;/strong&gt; If a step has no judgment in it, like publishing on merge or cross-posting, use a CI job or a script. A bot in that spot is one more thing that can go wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Don't underestimate context.&lt;/strong&gt; Know how much one bot can hold and still be useful before another has to take over. A long conversation slowly stops being useful, and that limit arrives sooner than you'd think. Splitting into bots doesn't fix it either: you just get more places to lose track. Plan for the handoff (a short written summary, a log, a pull request) before you need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveats&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Two or three asks" is a judgment call. Count on purpose.&lt;/li&gt;
&lt;li&gt;More bots means more instructions to keep in sync, and more chats to check. Every split has a cost; the rules above are worth more than the count.&lt;/li&gt;
&lt;li&gt;If you share bot instructions publicly, strip out machine names, usernames, and folder paths first.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aiagents</category>
      <category>grokbot</category>
      <category>botdesign</category>
      <category>buildlog</category>
    </item>
  </channel>
</rss>
