<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Troubleshooting · Loosely Decoupled</title>
    <link>https://looselydecoupled.com/shape/troubleshooting/</link>
    <description>Answers to PowerShell, Windows Server and OpenSSH questions the docs skip, with the reasoning behind them. Answer first, discussion after.</description>
    <language>en-us</language>
    
    <copyright>© 2026 Emmanuel Tsouris</copyright>
    <lastBuildDate>Sat, 17 Oct 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://looselydecoupled.com/shape/troubleshooting/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Why Doesn&#39;t My Claude Code Plugin Load in a Cloud Session?</title>
      <link>https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/</guid>
      <pubDate>Sat, 17 Oct 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Automation</category>
      <category>DevOps</category>
      
      <description>The install worked in my test cloud session only because it had the plugin&#39;s private repository attached. Sessions without it failed silently.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>Claude Code cloud sessions don&rsquo;t install the plugins a repository&rsquo;s <code>.claude/settings.json</code> turns on, and their GitHub proxy only serves the repositories attached to the session. So a plugin from a private marketplace doesn&rsquo;t load there. Its skill still can. Zip the skill folder, upload it under <strong>Customize</strong> &gt; <strong>Skills</strong> on claude.ai, and turn it on. In a new cloud session with nothing attached, it showed up as <code>anthropic-skills:&lt;skill&gt;</code>. The trade-offs: you upload again for every release, and the skill loads in every session on your account. A missing skill doesn&rsquo;t raise an error. Claude still answers, but without the skill&rsquo;s process, so ask the session to list its skills.</p>

</div>

<p>I was working on a project spread across several private repositories, and I created a skill I wanted to share between them as a Claude Code plugin. My plan was a private GitHub repository that&rsquo;s both the plugin and its marketplace, so the skill stays in sync on my laptop and in cloud sessions. Most of my work happens in Claude Code on the web, because I like that it keeps the work off my laptop, runs in isolation, and is there on my phone. I tested the install commands by hand in a cloud session, then put them in the environment&rsquo;s setup script, and the test worked. In the next real session the skill never loaded, and I got no error, not even a small one. Here&rsquo;s how I tracked down the cause, what I tried that didn&rsquo;t work, and the two workarounds that did.</p>
<h2 id="skill-doesnt-load-in-a-cloud-session-and-theres-no-error">Skill doesn&rsquo;t load in a cloud session, and there&rsquo;s no error<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#skill-doesnt-load-in-a-cloud-session-and-theres-no-error"
     aria-label="Link to section: Skill doesn’t load in a cloud session, and there’s no error" data-pagefind-ignore></a>
</h2>
<p>The session starts normally. You paste the notes the skill is built for, and Claude gives a reasonable, generic answer instead of following the skill.</p>
<p>That&rsquo;s what makes it easy to miss. When a skill doesn&rsquo;t load, Claude still answers, and nothing hints that a skill should have run. The only clue I had was that the answer was good but didn&rsquo;t follow the skill&rsquo;s steps.</p>
<p>My test had passed only because of how that session was set up. The session I tested in had the skills repository attached as one of its sources. The real sessions had only the repository I was working in. I also never confirmed that the saved setup script still had the install commands in it. Its log only showed output from another installer.</p>
<p>A second session found the cause. I messaged the failing session from the one where I built the plugin and asked it to inspect its own environment: the plugin list, the contents of <code>~/.claude</code>, and the settings files. It reported that no marketplace had ever been added.</p>
<h2 id="cloud-session-cant-reach-a-private-repository-that-isnt-attached">Cloud session can&rsquo;t reach a private repository that isn&rsquo;t attached<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#cloud-session-cant-reach-a-private-repository-that-isnt-attached"
     aria-label="Link to section: Cloud session can’t reach a private repository that isn’t attached" data-pagefind-ignore></a>
</h2>
<p>A cloud session reaches GitHub through a proxy, and the <a href="https://code.claude.com/docs/en/cloud-environments#github-proxy" rel="external noopener" target="_blank">cloud environments documentation<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> says it serves GitHub API requests only for the repositories attached to the session. Git behaved the same way for me. In a fresh session with only the consuming repository attached, I tried to reach the skills repository:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">git</span> <span class="console__token">ls-remote</span> <span class="console__token">https://github.com/&lt;owner&gt;/&lt;skills-repo&gt;.git</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">fatal:</span> <span class="console__token">could</span> <span class="console__token">not</span> <span class="console__token">read</span> <span class="console__token">Username</span> <span class="console__token">for</span> <span class="console__token">&#39;https://github.com&#39;:</span> <span class="console__token">terminal</span> <span class="console__token">prompts</span> <span class="console__token">disabled</span></span></code></pre>
    </div>
  </div></div><p>No credentials were supplied for it, so git asked for a username and gave up, even though the Claude GitHub App had access to that repository on my account. I didn&rsquo;t run the same command against the attached repository to compare. The same session hit the limit again, this time with nothing to do with plugins: <code>npm ci</code> couldn&rsquo;t fetch the project&rsquo;s private SDK, which lives in another private repository that wasn&rsquo;t attached.</p>
<p>Here&rsquo;s what that means for each way of installing the plugin:</p>
<ul>
<li><strong>Repository settings:</strong> The <a href="https://code.claude.com/docs/en/cloud-environments#what-carries-over-from-your-setup" rel="external noopener" target="_blank">documentation<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> lists plugins and marketplaces declared in a repo&rsquo;s <code>.claude/settings.json</code> as not available in cloud sessions. I never put the plugin there, so this is what the docs say, not something I tested.</li>
<li><strong>Setup-script install:</strong> I never confirmed my install commands ran, and the log was empty. If the script can only reach attached repositories, like the session, it had nothing to fetch. I haven&rsquo;t tested that.</li>
<li><strong>Plugin installed mid-session:</strong> In one session, installing from an attached copy left the plugin enabled but not loaded, and the cloud UI has <code>/reload-skills</code> but no <code>/reload-plugins</code>. In an earlier session the same install did make the skill appear. I don&rsquo;t know what was different.</li>
</ul>
<h2 id="plugin-added-on-claudeai-never-reaches-the-session">Plugin added on claude.ai never reaches the session<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#plugin-added-on-claudeai-never-reaches-the-session"
     aria-label="Link to section: Plugin added on claude.ai never reaches the session" data-pagefind-ignore></a>
</h2>
<p>Your claude.ai account has a plugins page too. Under <strong>Customize</strong> &gt; <strong>Plugins</strong> &gt; <strong>Add</strong> &gt; <strong>Add marketplace</strong> &gt; <strong>Add from a repository</strong>, claude.ai takes a private GitHub repository once the Claude GitHub App can read it. The <a href="https://claude.com/docs/plugins/overview" rel="external noopener" target="_blank">plugins overview<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> says a plugin you add there downloads to Claude Code as a synced plugin, which the <a href="https://code.claude.com/docs/en/plugins/loading#synced-plugins" rel="external noopener" target="_blank">plugin loading reference<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> names <code>&lt;name&gt;@synced</code>. That reference says synced plugins load in Cowork and in signed-in terminal sessions. It doesn&rsquo;t mention cloud sessions, although mine did get Anthropic&rsquo;s synced plugins.</p>
<figure><img src="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/add-marketplace_hu_cc27f6804a5ca6de.png" alt="The Add marketplace dialog on claude.ai, with a URL field that takes a GitHub owner/repo or a Git repository URL." width="704" height="264" loading="lazy" decoding="async"><figcaption>Customize, Plugins, Add, Add marketplace, Add from a repository.</figcaption>
</figure>
<p>The marketplace synced and the plugin showed as added, with no error. In the ten minutes I watched, it never showed up anywhere:</p>
<ul>
<li><strong>Cloud session, started five minutes after the add:</strong> The synced manifest under <code>~/.claude/plugins/synced/</code> listed 10 plugins, all from Anthropic&rsquo;s <code>knowledge-work-plugins</code> marketplace. Mine wasn&rsquo;t there.</li>
<li><strong>Terminal on my Mac, started ten minutes after the add:</strong> It printed <code>Plugins changed. Run /reload-plugins to activate.</code>, and the rewritten manifest listed 4 plugins from the same Anthropic marketplace. Mine wasn&rsquo;t there either.</li>
<li><strong><code>claude plugin marketplace list</code>:</strong> Its <code>From claude.ai:</code> section listed only <strong>My Uploads</strong> and the Anthropic Directory.</li>
</ul>
<p>One caveat: my Mac already had the same plugin installed from the command line, which may have affected that result. The cloud session had no local copy, so its result is clean. <strong>Add</strong> &gt; <strong>Upload plugin</strong> is a second way to put a plugin on your account. I didn&rsquo;t try it.</p>
<p>An open issue, <a href="https://github.com/anthropics/claude-code/issues/92031" rel="external noopener" target="_blank">anthropics/claude-code#92031<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>, describes the same thing from another account: plugins installed on claude.ai never reach cloud sessions, while skills enabled on the account do. That&rsquo;s the route in the next section.</p>
<h2 id="upload-the-skill-to-your-claudeai-account">Upload the skill to your claude.ai account<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#upload-the-skill-to-your-claudeai-account"
     aria-label="Link to section: Upload the skill to your claude.ai account" data-pagefind-ignore></a>
</h2>
<p>A skill doesn&rsquo;t need to be a plugin to reach a cloud session. The <a href="https://code.claude.com/docs/en/skills#skills-in-cowork-and-cloud-sessions" rel="external noopener" target="_blank">skills documentation<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> says cloud sessions load the skills enabled for your claude.ai account, synced at session start.</p>
<ol>
<li>
<p>On your machine, zip the skill folder, with its <code>SKILL.md</code> inside the folder. To build the zip from a release tag in the skills repository, run:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">git</span> <span class="console__token">archive</span> <span class="console__token">--format=zip</span> <span class="console__token">--prefix=&lt;skill&gt;/</span> <span class="console__token">-o</span> <span class="console__token">&lt;skill&gt;.zip</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">&lt;tag&gt;:plugins/&lt;plugin&gt;/skills/&lt;skill&gt;</span></span></code></pre>
    </div>
  </div></div></li>
<li>
<p>On claude.ai, go to <strong>Customize</strong> &gt; <strong>Skills</strong> &gt; <strong>Add</strong> &gt; <strong>Upload skill</strong>, upload the zip, and turn the skill on.</p>
</li>
<li>
<p>Start a new cloud session, and ask Claude to list its skills.</p>
</li>
</ol>
<figure><img src="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/upload-skill_hu_769c3e4dfaeb44d3.png" alt="The Upload a skill page on claude.ai. It takes a .md, .zip or .skill file, and a security scan runs on upload." width="1030" height="480" loading="lazy" decoding="async"><figcaption>Customize, Skills, Add, Upload skill.</figcaption>
</figure>
<p>On upload, claude.ai previews the skill&rsquo;s name and description and runs a security scan. In a new cloud session with only the consuming repository attached, the skill showed up as <code>anthropic-skills:&lt;skill&gt;</code>. I checked that in one session. My test of Claude actually using the skill, in the section on confirming it loaded, ran on the copied version from the next section, which has the same files.</p>
<p>There are two costs. claude.ai numbers the skill&rsquo;s versions v1, v2 and so on, not by your git tags, and each release means another upload. It&rsquo;s an account skill, so it loads in every session on the account, not only the repositories that use it. That includes terminal sessions: on my Mac it appeared beside the same skill from the installed plugin, so the session offered it twice. Uninstall the local plugin, or turn the account skill off, to keep one.</p>
<h2 id="copy-the-skill-into-the-cloud-session">Copy the skill into the cloud session<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#copy-the-skill-into-the-cloud-session"
     aria-label="Link to section: Copy the skill into the cloud session" data-pagefind-ignore></a>
</h2>
<p>If a session needs a specific release, copy the skill in from the skills repository instead. The <a href="https://code.claude.com/docs/en/skills" rel="external noopener" target="_blank">skills documentation<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> says Claude Code watches <code>~/.claude/skills/</code> and picks up a skill added there during the session, without a restart.</p>
<ol>
<li>
<p>Start the cloud session with two repositories attached: the one you&rsquo;re working in and the skills repository.</p>
</li>
<li>
<p>Ask Claude where the skills repository was cloned. It was <code>/home/user/&lt;skills-repo&gt;</code> in my sessions.</p>
</li>
<li>
<p>Ask Claude to copy the skill folder into your user skills directory, changing nothing else:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">mkdir</span> <span class="console__token">-p</span> <span class="console__token">~/.claude/skills</span>
</span><span class="line"><span class="prompt">$ </span><span class="console__token">cp</span> <span class="console__token">-R</span> <span class="console__token">/home/user/&lt;skills-repo&gt;/plugins/&lt;plugin&gt;/skills/&lt;skill&gt;</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">~/.claude/skills/&lt;skill&gt;</span></span></code></pre>
    </div>
  </div></div></li>
<li>
<p>Ask Claude to list its skills. If <code>~/.claude/skills/</code> didn&rsquo;t exist when the session started, run <code>/reload-skills</code> first, because Claude Code wasn&rsquo;t watching it.</p>
</li>
</ol>
<figure><img src="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/reload-skills_hu_873f416ef627492b.png" alt="The prompt box of a cloud session with /reload-skills typed and its description: pick up skills added or changed on disk during this session." width="845" height="140" loading="lazy" decoding="async">
</figure>
<p>The skill loads as plain <code>&lt;skill&gt;</code>, with no prefix. To copy a specific release, run <code>git -C /home/user/&lt;skills-repo&gt; fetch --tags</code> and <code>git -C /home/user/&lt;skills-repo&gt; checkout &lt;tag&gt;</code> before the copy. I haven&rsquo;t tested that step. The copy doesn&rsquo;t carry over to other sessions.</p>
<p>If every session in a repository needs the skill, commit a copy under that repository&rsquo;s <code>.claude/skills/</code> instead. The documentation lists the repository&rsquo;s own skills as available in cloud sessions. The cost is a copy in every repository, which is what the plugin was meant to avoid. I built it once, checked it against the release tag, and didn&rsquo;t merge it.</p>
<p>Both routes bring over the plugin&rsquo;s skills only. Hooks, agents and MCP servers in the plugin don&rsquo;t load this way, and I haven&rsquo;t found a way to load them in a cloud session without <code>/reload-plugins</code>.</p>
<h2 id="confirm-the-skill-loaded">Confirm the skill loaded<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#confirm-the-skill-loaded"
     aria-label="Link to section: Confirm the skill loaded" data-pagefind-ignore></a>
</h2>
<p>There are two checks. The session&rsquo;s skill list shows the skill loaded. Asking a question the skill is built for, without naming the skill, shows that Claude actually uses it. My first test was a request to look over a project and say where it could be better. The session announced the skill&rsquo;s first step and worked through its process. If the answer is generic, ask which skill it used.</p>
<p>For the skill list, ask Claude to list its skills, and look for <code>&lt;skill&gt;</code> or <code>anthropic-skills:&lt;skill&gt;</code>.</p>
<p>A plugin that shows as enabled in <code>claude plugin list</code> isn&rsquo;t proof. In one of my sessions it was enabled and never loaded. A folder in <code>~/.claude/skills/</code> isn&rsquo;t proof either, until the session lists it.</p>
<h2 id="do-this-with-a-coding-agent">Do this with a coding agent<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#do-this-with-a-coding-agent"
     aria-label="Link to section: Do this with a coding agent" data-pagefind-ignore></a>
</h2>
<p>What found my cause was an agent inspecting its own environment. Paste this into the coding agent in the cloud session you want to check. Run it once in a session where the skill works and once where it doesn&rsquo;t, then compare the two reports. Replace the placeholders first.</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Inspect your own environment, read-only, and report whether a skill is available and where it came from. Change nothing: do not install, enable, disable, add, remove, copy, or edit anything, and do not print any token, key, or password you come across.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Skill name: &lt;skill-name&gt;
</span></span><span class="line"><span class="cl">Plugin name: &lt;plugin-name&gt;
</span></span><span class="line"><span class="cl">Skills repository: &lt;owner&gt;/&lt;skills-repo&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Print the Claude Code version.
</span></span><span class="line"><span class="cl">2. List the installed plugins with their version, scope, and status. Note which ones are &lt;name&gt;@synced.
</span></span><span class="line"><span class="cl">3. List the plugin marketplaces that have been added.
</span></span><span class="line"><span class="cl">4. List ~/.claude/skills/ and ~/.claude/plugins/, including plugins/synced/.
</span></span><span class="line"><span class="cl">5. List the repositories attached to this session, and say whether the skills repository is one of them.
</span></span><span class="line"><span class="cl">6. Run git ls-remote https://github.com/&lt;owner&gt;/&lt;skills-repo&gt;.git and quote any error verbatim.
</span></span><span class="line"><span class="cl">7. Report whether this repository&#39;s .claude/settings.json has enabledPlugins or extraKnownMarketplaces entries.
</span></span><span class="line"><span class="cl">8. Report whether a setup script log exists for this session and whether it contains output from a plugin install.
</span></span><span class="line"><span class="cl">9. List the skills available to this session, and say whether &lt;skill-name&gt; is among them and under what name.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Then answer in this form:
</span></span><span class="line"><span class="cl">- Skill &lt;skill-name&gt; available: yes or no, and under what name
</span></span><span class="line"><span class="cl">- Plugin &lt;plugin-name&gt; installed: yes or no, and whether it is loaded or only enabled
</span></span><span class="line"><span class="cl">- Synced plugins: the names, or none
</span></span><span class="line"><span class="cl">- Skills repository attached: yes or no
</span></span><span class="line"><span class="cl">- Skills repository reachable with git: yes, or the error
</span></span><span class="line"><span class="cl">- Plugin settings in the repository: the entries, or none
</span></span><span class="line"><span class="cl">- Setup script install output: found, not found, or no log
</span></span><span class="line"><span class="cl">- Anything you could not check, and why</span></span></code></pre></div></div>
  </div>
</div><h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<h3 id="git-fails-with-terminal-prompts-disabled">Git fails with <code>terminal prompts disabled</code><a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#git-fails-with-terminal-prompts-disabled"
     aria-label="Link to section: Git fails with terminal prompts disabled" data-pagefind-ignore></a>
</h3>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">fatal:</span> <span class="console__token">could</span> <span class="console__token">not</span> <span class="console__token">read</span> <span class="console__token">Username</span> <span class="console__token">for</span> <span class="console__token">&#39;https://github.com&#39;:</span> <span class="console__token">terminal</span> <span class="console__token">prompts</span> <span class="console__token">disabled</span></span></code></pre>
    </div>
  </div></div><p>In a Claude Code cloud session, git prints this for a private repository that isn&rsquo;t attached to the session, because the GitHub proxy supplies no credentials for it. A mistyped repository name probably produces the same prompt. Check the name, then attach the repository or start a session with it attached. The documentation describes the proxy&rsquo;s answer to a GitHub API request for an unattached repository as a 403 whose message starts <code>GitHub access to</code> and contains <code>is not enabled for this session</code>.</p>
<h3 id="setup-script-install-leaves-no-trace">Setup-script install leaves no trace<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#setup-script-install-leaves-no-trace"
     aria-label="Link to section: Setup-script install leaves no trace" data-pagefind-ignore></a>
</h3>
<p>First check that the saved setup script still contains the install commands. Mine was never checked. If it does, a private skills repository that isn&rsquo;t attached is a likely cause. The <a href="https://code.claude.com/docs/en/cloud-environments#environment-caching" rel="external noopener" target="_blank">documentation<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> also says later sessions can start from a cached snapshot and skip the setup script, so a missing log line doesn&rsquo;t prove the script failed. Check the session itself with <code>claude plugin list</code>.</p>
<h3 id="enabledplugins-in-claudesettingsjson-does-nothing-in-a-cloud-session"><code>enabledPlugins</code> in <code>.claude/settings.json</code> does nothing in a cloud session<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#enabledplugins-in-claudesettingsjson-does-nothing-in-a-cloud-session"
     aria-label="Link to section: enabledPlugins in .claude/settings.json does nothing in a cloud session" data-pagefind-ignore></a>
</h3>
<p>A cloud session doesn&rsquo;t install the plugins a repository turns on under <code>enabledPlugins</code>. The <a href="https://code.claude.com/docs/en/plugins/loading#plugins-shared-through-a-repository" rel="external noopener" target="_blank">plugin loading reference<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> says it doesn&rsquo;t add the marketplaces under <code>extraKnownMarketplaces</code> either, because that needs the workspace trust dialog, which a cloud session never shows. Those keys still work for local sessions. The open issue <a href="https://github.com/anthropics/claude-code/issues/78119" rel="external noopener" target="_blank">anthropics/claude-code#78119<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> tracks this, and a comment there reports that plugins declared in organization-managed settings don&rsquo;t install in cloud sessions either. I can&rsquo;t test that on an individual account.</p>
<h3 id="plugin-is-enabled-but-the-skill-isnt-listed">Plugin is enabled but the skill isn&rsquo;t listed<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#plugin-is-enabled-but-the-skill-isnt-listed"
     aria-label="Link to section: Plugin is enabled but the skill isn’t listed" data-pagefind-ignore></a>
</h3>
<p>A plugin installed after the session starts loads only when Claude Code restarts or runs <code>/reload-plugins</code>, and the cloud UI has no <code>/reload-plugins</code>. Upload the skill to your claude.ai account, or copy it into <code>~/.claude/skills/</code>.</p>
<h3 id="install-worked-in-a-test-session-and-fails-in-the-real-one">Install worked in a test session and fails in the real one<a class="heading-anchor" href="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/#install-worked-in-a-test-session-and-fails-in-the-real-one"
     aria-label="Link to section: Install worked in a test session and fails in the real one" data-pagefind-ignore></a>
</h3>
<p>What&rsquo;s attached to a session decides what it can reach. Test in a session set up like the one that will use the skill, with the same repositories attached and nothing more.</p>
<p><strong>Still open:</strong> a way to load a whole private plugin, with its hooks, agents and MCP servers, into a cloud session without attaching its repository. Its skills have a route. I haven&rsquo;t found one for the rest. If you&rsquo;ve found a way that works, I&rsquo;d like to hear about it. Reply to any newsletter issue.</p>
<figure><img src="https://looselydecoupled.com/claude-code-plugin-not-loading-cloud-session/cover_hu_22d45f9a47861538.png" alt="A four-panel comic. Panel one: an engineer at a laptop says “Run the skill.” to a robot. Panel two: the robot instantly holds out a neat stack of paper. It says “Done!” Panel three: the engineer, reading the pages with a worried frown, says “That’s not the skill.” while the robot beams. Panel four: the robot, now wearing a small fake moustache, hands on hips, says “No. It’s my impression of it.”" width="1276" height="530" loading="lazy" decoding="async"><figcaption>I got no error, not even a small one.</figcaption>
</figure>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>Claude Code</dt> <dd>2.1.302</dd></div>
    <div class="tested__row"><dt>Account</dt> <dd>individual claude.ai plan</dd></div>
    <div class="tested__row"><dt>Cloud session</dt> <dd>claude.ai/code</dd></div>
    <div class="tested__row"><dt>Terminal</dt> <dd>macOS, signed in with a claude.ai account</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>Rivian Dashcam Camera Missing from a Drive: It Restarted in Another Folder</title>
      <link>https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/</guid>
      <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Automation</category>
      <category>Python</category>
      
      <description>A Rivian R1T camera that stops partway through a drive often restarted into a sibling folder; motion correlation, not folder names, gives its offset.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>The camera didn&rsquo;t stop recording. It restarted seconds later into a new drive
folder that holds only the restarted cameras. Look for a folder named a few
seconds after the broken one, with fewer than five cameras in it.</p>
<p>Don&rsquo;t take the offset from the two folder names. Measure it by correlating the
motion of two cameras that face the same way, such as the Gear Guard camera
and the rear camera. Then re-encode the restarted cameras from the sibling folder at that
offset, so that they start on the exact frame.</p>

</div>

<p>I cut a five-camera clip from a drive on August 7 and dropped it into a
multicam timeline. Three angles played to the end. The rear camera and the
side-left camera went black after 33 seconds. Every file was readable and nothing
reported an error. The footage I thought was lost was in the next folder.</p>
<h2 id="rear-or-side-camera-stopped-recording-after-a-few-seconds">Rear or side camera stopped recording after a few seconds<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#rear-or-side-camera-stopped-recording-after-a-few-seconds"
     aria-label="Link to section: Rear or side camera stopped recording after a few seconds" data-pagefind-ignore></a>
</h2>
<p>The symptom has a few forms, depending on what you build from the drive:</p>
<ul>
<li>A camera&rsquo;s only file in the drive folder is a few seconds long, while the
other cameras have a full set of 15-minute segments.</li>
<li>A multicam clip or a grid of the five cameras shows black tiles for one or two
angles for most of the drive.</li>
<li>A hyperlapse of one angle is much shorter than the others, or ends in black.</li>
</ul>
<p>On the August 7 drive, the folder <code>08_07_26_140000</code> holds full segments for the
front, Gear Guard and side-right cameras, and one 33-second file each for the
rear and side-left cameras:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-show_entries</span> <span class="console__token">format=duration</span> <span class="console__token">-of</span> <span class="console__token">csv=p=0</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">08_07_26_140000/08_07_26_140000_rearCenter_001.mp4</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">33.000000</span></span></code></pre>
    </div>
  </div></div><figure><img src="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/five-cameras-before-stitch_hu_1be5a11e4db848dc.jpg" alt="A five-camera grid on the I-90 floating bridge built from the original folder alone. The front, side-right and Gear Guard tiles show the bridge; the rear and side-left tiles are solid black." width="1600" height="853" loading="lazy" decoding="async"><figcaption>The August 7 drive on the floating bridge, from 08_07_26_140000 only. Plates blurred.</figcaption>
</figure>
<h2 id="cause-the-camera-restarted-into-a-sibling-folder">Cause: the camera restarted into a sibling folder<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#cause-the-camera-restarted-into-a-sibling-folder"
     aria-label="Link to section: Cause: the camera restarted into a sibling folder" data-pagefind-ignore></a>
</h2>
<p>The truck writes one folder per drive, named <code>MM_DD_YY_HHMMSS</code> in local time, and
one file per camera per 15-minute segment inside it. When a camera stops and
restarts partway through a drive, its recording doesn&rsquo;t continue in the
drive&rsquo;s folder. A new folder is created a few seconds later, and it holds only
the cameras that restarted. The rest of the drive stays in the original folder.</p>
<p>The two folders together hold the whole drive. Nothing was lost, but the drive is
now split across two clocks.</p>
<h2 id="find-the-sibling-folder">Find the sibling folder<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#find-the-sibling-folder"
     aria-label="Link to section: Find the sibling folder" data-pagefind-ignore></a>
</h2>
<p>Run this in the <code>DriveStream</code> folder of the truck&rsquo;s drive, or of your copy of it.
It prints each drive folder with the cameras it holds:</p>
<div class="code-block" data-copyable data-copy-label="Bash">
  <div class="code-block__head"><span class="code-block__lang">Bash</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">for</span> d in */<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">printf</span> <span class="s1">&#39;%s &#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">d</span><span class="p">%/</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  ls <span class="s2">&#34;</span><span class="nv">$d</span><span class="s2">&#34;</span> <span class="p">|</span> grep -v <span class="s1">&#39;^\._&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="p">|</span> sed -E <span class="s1">&#39;s/^[0-9_]+_([A-Za-z]+)_[0-9]{3}\.mp4$/\1/&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="p">|</span> sort -u <span class="p">|</span> tr <span class="s1">&#39;\n&#39;</span> <span class="s1">&#39; &#39;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span></span></span></code></pre></div></div>
  </div>
</div><p>The <code>grep -v '^\._'</code> drops the AppleDouble files that macOS leaves beside every
clip on the truck&rsquo;s exFAT drive. The August 7 folders look like this:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">08_07_26_100000 frontCenter gearGuard rearCenter sideLeft sideRight
</span></span><span class="line"><span class="cl">08_07_26_140000 frontCenter gearGuard rearCenter sideLeft sideRight
</span></span><span class="line"><span class="cl">08_07_26_140034 rearCenter sideLeft
</span></span><span class="line"><span class="cl">08_07_26_160000 frontCenter gearGuard rearCenter sideLeft sideRight</span></span></code></pre></div></div>
  </div>
</div><p>A sibling folder has all three of these properties:</p>
<ul>
<li>Its name is seconds, not minutes, after the previous folder&rsquo;s name.</li>
<li>It holds fewer than five cameras.</li>
<li>Its cameras are the ones that stop early in the previous folder.</li>
</ul>
<p>A short folder with one camera and no partner is a different case: a stray
fragment, not half of a drive. Across 233 drive folders, one was a 21-second
fragment from a single camera.</p>
<h2 id="folder-name-offset-is-wrong">Folder name offset is wrong<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#folder-name-offset-is-wrong"
     aria-label="Link to section: Folder name offset is wrong" data-pagefind-ignore></a>
</h2>
<p>The folder names differ by 34 seconds, so the obvious fix is to place the
restarted cameras 34 seconds into the drive. Measured from the footage, the
restarted cameras begin 32.7 seconds into the drive. The names are wrong by more
than a second. The files&rsquo; <code>creation_time</code> tags are no better: the two folders'
differ by 35 seconds, a third answer.</p>
<p>A folder name is a label, not a clock: nothing ties it to the first frame of
the video inside. An error of more than a second is visible in a multicam edit.
A car that passes the truck crosses the rear view and the side view at moments
that no longer match. The truck&rsquo;s files carry no timecode track to correct it, so
the offset has to come from the pictures.</p>
<h2 id="measure-the-offset-by-motion-correlation">Measure the offset by motion correlation<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#measure-the-offset-by-motion-correlation"
     aria-label="Link to section: Measure the offset by motion correlation" data-pagefind-ignore></a>
</h2>
<p>Two cameras that see the same scene change at the same moments. When the truck
hits a bump or a car passes behind it, both backward-facing pictures move at
once. The script reduces each camera to a motion signal and slides one signal
along the other until they match best:</p>
<ol>
<li>Decode a window of each clip at a fixed sample rate, scaled to a tiny
grayscale frame.</li>
<li>Take the mean absolute change between consecutive frames. That&rsquo;s the motion
signal: high when the picture changes, low when it holds still.</li>
<li>Standardize the reference, and each stretch of the other signal it&rsquo;s
compared with, so that a brighter or busier camera or stretch doesn&rsquo;t
outweigh the rest.</li>
<li>Try every lag within a search range around your guess, and keep the lag with
the highest correlation.</li>
</ol>
<p>It needs Python 3 and <code>ffmpeg</code> on your path, and nothing else:</p>
<div class="code-block" data-copyable data-copy-label="measure_offset.py">
  <div class="code-block__head"><span class="code-block__lang code-block__file">measure_offset.py</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;Measure the time offset between two dashcam clips by correlating their motion.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Usage:
</span></span></span><span class="line"><span class="cl"><span class="s2">    python3 measure_offset.py REFERENCE OTHER --at SECONDS [--guess SECONDS]
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Reads a window of REFERENCE starting at --at, and a slightly wider window of
</span></span></span><span class="line"><span class="cl"><span class="s2">OTHER around --at minus --guess. Prints the offset that lines the two up:
</span></span></span><span class="line"><span class="cl"><span class="s2">a moment at time t in REFERENCE is at time t - offset in OTHER.
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">argparse</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">WIDTH</span><span class="p">,</span> <span class="n">HEIGHT</span> <span class="o">=</span> <span class="mi">32</span><span class="p">,</span> <span class="mi">24</span>
</span></span><span class="line"><span class="cl"><span class="n">SAMPLES_PER_SECOND</span> <span class="o">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">motion</span><span class="p">(</span><span class="n">clip</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">start</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">seconds</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Mean absolute change between consecutive tiny grayscale frames.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">raw</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="s2">&#34;ffmpeg&#34;</span><span class="p">,</span> <span class="s2">&#34;-nostdin&#34;</span><span class="p">,</span> <span class="s2">&#34;-v&#34;</span><span class="p">,</span> <span class="s2">&#34;error&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="s2">&#34;-ss&#34;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">start</span><span class="si">:</span><span class="s2">.3f</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;-t&#34;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">seconds</span><span class="si">:</span><span class="s2">.3f</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;-i&#34;</span><span class="p">,</span> <span class="n">clip</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="s2">&#34;-vf&#34;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;fps=</span><span class="si">{</span><span class="n">SAMPLES_PER_SECOND</span><span class="si">}</span><span class="s2">,scale=</span><span class="si">{</span><span class="n">WIDTH</span><span class="si">}</span><span class="s2">:</span><span class="si">{</span><span class="n">HEIGHT</span><span class="si">}</span><span class="s2">,format=gray&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="s2">&#34;-f&#34;</span><span class="p">,</span> <span class="s2">&#34;rawvideo&#34;</span><span class="p">,</span> <span class="s2">&#34;-&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span><span class="o">.</span><span class="n">stdout</span>
</span></span><span class="line"><span class="cl">    <span class="n">size</span> <span class="o">=</span> <span class="n">WIDTH</span> <span class="o">*</span> <span class="n">HEIGHT</span>
</span></span><span class="line"><span class="cl">    <span class="n">frames</span> <span class="o">=</span> <span class="p">[</span><span class="n">raw</span><span class="p">[</span><span class="n">i</span><span class="p">:</span><span class="n">i</span> <span class="o">+</span> <span class="n">size</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span> <span class="o">-</span> <span class="n">size</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="nb">sum</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">a</span> <span class="o">-</span> <span class="n">b</span><span class="p">)</span> <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">before</span><span class="p">,</span> <span class="n">after</span><span class="p">))</span> <span class="o">/</span> <span class="n">size</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">before</span><span class="p">,</span> <span class="n">after</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">frames</span><span class="p">,</span> <span class="n">frames</span><span class="p">[</span><span class="mi">1</span><span class="p">:])]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">standardize</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">mean</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">variance</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">((</span><span class="n">v</span> <span class="o">-</span> <span class="n">mean</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">)</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">spread</span> <span class="o">=</span> <span class="n">variance</span> <span class="o">**</span> <span class="mf">0.5</span> <span class="ow">or</span> <span class="mf">1.0</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[(</span><span class="n">v</span> <span class="o">-</span> <span class="n">mean</span><span class="p">)</span> <span class="o">/</span> <span class="n">spread</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">best_lag</span><span class="p">(</span><span class="n">reference</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">],</span> <span class="n">other</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Slide reference along other; return the best correlation and its lag.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">scores</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">lag</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">other</span><span class="p">)</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">reference</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">window</span> <span class="o">=</span> <span class="n">standardize</span><span class="p">(</span><span class="n">other</span><span class="p">[</span><span class="n">lag</span><span class="p">:</span><span class="n">lag</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">reference</span><span class="p">)])</span>
</span></span><span class="line"><span class="cl">        <span class="n">score</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">a</span> <span class="o">*</span> <span class="n">b</span> <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">reference</span><span class="p">,</span> <span class="n">window</span><span class="p">))</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">reference</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">scores</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">score</span><span class="p">,</span> <span class="n">lag</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">max</span><span class="p">(</span><span class="n">scores</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;reference&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;other&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;--at&#34;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;window start in REFERENCE, seconds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;--guess&#34;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;expected offset, seconds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;--length&#34;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">120.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;window length, seconds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;--search&#34;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">5.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;how far either side of the guess to look&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">reference</span> <span class="o">=</span> <span class="n">standardize</span><span class="p">(</span><span class="n">motion</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">reference</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">at</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">length</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># The other window starts --search seconds early, so lag 0 means guess + search.</span>
</span></span><span class="line"><span class="cl">    <span class="n">other_start</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">at</span> <span class="o">-</span> <span class="n">args</span><span class="o">.</span><span class="n">guess</span> <span class="o">-</span> <span class="n">args</span><span class="o">.</span><span class="n">search</span>
</span></span><span class="line"><span class="cl">    <span class="n">other_length</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">length</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">args</span><span class="o">.</span><span class="n">search</span>
</span></span><span class="line"><span class="cl">    <span class="n">other</span> <span class="o">=</span> <span class="n">motion</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">other</span><span class="p">,</span> <span class="n">other_start</span><span class="p">,</span> <span class="n">other_length</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">,</span> <span class="n">lag</span> <span class="o">=</span> <span class="n">best_lag</span><span class="p">(</span><span class="n">reference</span><span class="p">,</span> <span class="n">other</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">offset</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">guess</span> <span class="o">+</span> <span class="n">args</span><span class="o">.</span><span class="n">search</span> <span class="o">-</span> <span class="n">lag</span> <span class="o">/</span> <span class="n">SAMPLES_PER_SECOND</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;window at </span><span class="si">{</span><span class="n">args</span><span class="o">.</span><span class="n">at</span><span class="si">:</span><span class="s2">g</span><span class="si">}</span><span class="s2"> s: correlation </span><span class="si">{</span><span class="n">score</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">, offset </span><span class="si">{</span><span class="n">offset</span><span class="si">:</span><span class="s2">+.1f</span><span class="si">}</span><span class="s2"> s&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">main</span><span class="p">()</span></span></span></code></pre></div></div>
  </div>
</div><p>Give it a camera from the original folder as the reference, a camera from the
sibling folder as the other, and the folder-name difference as the guess. Both
folders number their segments from <code>001</code> and every segment but the last runs
900 seconds, so segment <code>002</code> of the sibling starts the same offset after
segment <code>002</code> of the original. Compare files with the same segment number, and
keep the window inside one segment.</p>
<p>In the <code>DriveStream</code> folder, run:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">python3</span> <span class="console__token">measure_offset.py</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">08_07_26_140000/08_07_26_140000_gearGuard_001.mp4</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">08_07_26_140034/08_07_26_140034_rearCenter_001.mp4</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">--at</span> <span class="console__token">300</span> <span class="console__token">--guess</span> <span class="console__token">34</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">window</span> <span class="console__token">at</span> <span class="console__token">300</span> <span class="console__token">s:</span> <span class="console__token">correlation</span> <span class="console__token">0.84,</span> <span class="console__token">offset</span> <span class="console__token">+32.6</span> <span class="console__token">s</span></span></code></pre>
    </div>
  </div></div><p>One window is one measurement. Repeat it in windows that don&rsquo;t overlap, from
different segments, and trust the offset only when they agree. Four windows on
the August 7 drive gave this:</p>
<div class="table-wrap" data-scrollable data-label="Table: Segment">
  <table>
    <thead>
      <tr><th scope="col">Segment</th><th scope="col"><code>--at</code></th><th scope="col">Correlation</th><th scope="col">Offset</th></tr>
    </thead>
    <tbody>
      <tr><td><code>001</code></td><td>300</td><td>0.84</td><td>+32.6 s</td></tr>
      <tr><td><code>001</code></td><td>700</td><td>0.73</td><td>+32.7 s</td></tr>
      <tr><td><code>002</code></td><td>600</td><td>0.85</td><td>+32.8 s</td></tr>
      <tr><td><code>003</code></td><td>200</td><td>0.69</td><td>+32.7 s</td></tr>
    </tbody>
  </table>
</div>
<p>That&rsquo;s 32.7 seconds, plus or minus 0.1, from four independent windows. A moment
at time <em>t</em> in the original folder is at <em>t</em> − 32.7 in the sibling folder.</p>
<figure>
  <img src="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/motion-correlation-offset.svg" alt="Two plots of motion against time, from 300 to 330 seconds into the Gear Guard file, with the Gear Guard signal in blue and the restarted rear camera in amber. Shifted by the folder-name offset of 34 seconds, each amber peak lands about a second after the blue one. Shifted by the measured 32.7 seconds, the peaks fall together." loading="lazy" decoding="async"><figcaption>Motion from measure_offset.py&rsquo;s motion() function, standardized, for the first window in the table.</figcaption>
</figure>
<h3 id="compare-cameras-that-face-the-same-way">Compare cameras that face the same way<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#compare-cameras-that-face-the-same-way"
     aria-label="Link to section: Compare cameras that face the same way" data-pagefind-ignore></a>
</h3>
<p>The front camera was my first choice for the reference, because it recorded the
whole drive. Against the restarted rear camera it gave two answers:</p>
<div class="table-wrap" data-scrollable data-label="Table: Segment">
  <table>
    <thead>
      <tr><th scope="col">Segment</th><th scope="col"><code>--at</code></th><th scope="col">Correlation</th><th scope="col">Offset</th></tr>
    </thead>
    <tbody>
      <tr><td><code>001</code></td><td>300</td><td>0.74</td><td>+31.9 s</td></tr>
      <tr><td><code>002</code></td><td>600</td><td>0.52</td><td>+32.8 s</td></tr>
    </tbody>
  </table>
</div>
<p>The two windows disagree by 0.9 seconds, and each one looks plausible on its
own. The front camera sees the road ahead and the rear camera sees the road
behind. Their pictures change for different reasons most of the time, so the
best match moves from one window to the next. The Gear Guard camera looks backward over
the bed, the same direction as the rear camera, and the two agree.</p>
<p>Pick a reference camera that kept recording in the original folder and faces
the same direction as one of the restarted cameras. The files in a sibling
folder share one start time and the same segment lengths, so the offset you
measure for one of its cameras applies to all of them.</p>
<h2 id="stitch-the-restarted-cameras">Stitch the restarted cameras<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#stitch-the-restarted-cameras"
     aria-label="Link to section: Stitch the restarted cameras" data-pagefind-ignore></a>
</h2>
<p>A stream copy can only start on a keyframe, and the truck writes one keyframe per
second. The stream-copied angles from the original folder start on their own
keyframes. An offset of 32.7 seconds isn&rsquo;t a whole number of seconds, so no
keyframe in the sibling folder falls on the same instant. Re-encode only the restarted
cameras, cut at the exact frame, and leave the rest as stream copies.</p>
<p>Suppose your clip covers <code>START</code> to <code>END</code> seconds of the original folder, and
<code>OFFSET</code> is the measured offset. The window has to begin after the restart, so
<code>START</code> is greater than <code>OFFSET</code>. In the sibling folder, the clip begins at
<code>START</code> minus <code>OFFSET</code> and lasts <code>END</code> minus <code>START</code> seconds.</p>
<p>In the <code>DriveStream</code> folder, the following commands write a concat list of the
restarted camera&rsquo;s segments in the sibling folder, and then cut the clip from
the joined segments, re-encoding to HEVC. They create two files and change
nothing in the drive folders:</p>
<div class="code-block" data-copyable data-copy-label="Bash">
  <div class="code-block__head"><span class="code-block__lang">Bash</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">printf</span> <span class="s2">&#34;file &#39;%s&#39;\n&#34;</span> <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span>/08_07_26_140034/08_07_26_140034_rearCenter_*.mp4 <span class="se">\
</span></span></span><span class="line"><span class="cl">  &gt; rearCenter.txt
</span></span><span class="line"><span class="cl">ffmpeg -f concat -safe <span class="m">0</span> -i rearCenter.txt -ss &lt;sibling-start&gt; -t &lt;duration&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  -c:v hevc_videotoolbox -q:v <span class="m">80</span> -tag:v hvc1 -color_range pc -an <span class="se">\
</span></span></span><span class="line"><span class="cl">  &lt;clip-folder&gt;/rearCenter.mp4</span></span></code></pre></div></div>
  </div>
</div><p>Run the same two commands for every other camera in the sibling folder.</p>
<p><code>-ss</code> after <code>-i</code> decodes from the start of the list and discards frames up to
the exact time, so the output starts on that instant rather than on the nearest
keyframe. It&rsquo;s slow for a window late in the drive, because ffmpeg decodes
everything before the window first. Joining the segments in one concat pass,
rather than cutting each file and joining the cuts, keeps the frames whose
group of pictures runs across a segment boundary. <a href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/">Cut multi-camera dashcam
clips with ffmpeg</a> covers the
stream-copied cameras and that join in full.</p>
<p>The encoder <code>hevc_videotoolbox</code> is the hardware HEVC encoder on macOS. On
another system, use the encoder you&rsquo;d use for the rest of the project. The
option <code>-color_range pc</code> labels the re-encoded angle full range like the
truck&rsquo;s footage, so it matches the stream-copied ones; check it with
<code>ffprobe -show_entries stream=color_range</code>. The truck
records no audio, so <code>-an</code> drops nothing.</p>
<p>Every file now starts at time 0. The re-encoded angles start on the exact
frame, and a stream-copied one starts early by the lead <a href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#build-the-multicam-clip">the cut
script</a>
printed for it. In DaVinci Resolve, mark each lead as an In point, then create
the multicam clip with angle sync set to the In point.</p>
<figure><img src="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/five-cameras-after-stitch_hu_dedc50ea5b067455.jpg" alt="The same moment on the floating bridge after the stitch. All five tiles are filled: the rear tile shows the car following behind, which the Gear Guard tile also shows over the truck bed, and the side-left tile shows the bridge deck and railing." width="1600" height="853" loading="lazy" decoding="async"><figcaption>Rear and side-left from 08_07_26_140034, cut at the measured 32.7 s offset. Plates and the following driver blurred.</figcaption>
</figure>
<h2 id="confirm-the-stitch">Confirm the stitch<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#confirm-the-stitch"
     aria-label="Link to section: Confirm the stitch" data-pagefind-ignore></a>
</h2>
<p>Run the same script on the finished clip, with the guess left at zero. In the
clip&rsquo;s folder, compare the Gear Guard camera, which was stream-copied from the original
folder, with the re-encoded rear camera:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">python3</span> <span class="console__token">measure_offset.py</span> <span class="console__token">08_07_26_140000_gearGuard.mp4</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">08_07_26_140000_rearCenter.mp4</span> <span class="console__token">--at</span> <span class="console__token">300</span> <span class="console__token">--length</span> <span class="console__token">90</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">window</span> <span class="console__token">at</span> <span class="console__token">300</span> <span class="console__token">s:</span> <span class="console__token">correlation</span> <span class="console__token">0.79,</span> <span class="console__token">offset</span> <span class="console__token">+0.1</span> <span class="console__token">s</span></span></code></pre>
    </div>
  </div></div><p>An offset of +0.1 seconds is one sample step of <code>SAMPLES_PER_SECOND</code>, the
resolution of the method. The angles are in sync. The same script reported
32.7 seconds on the unstitched folders, so it does notice an offset when one
exists. A zero from a check that has never reported anything else proves
nothing.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Why Moving $null to the Left Side Broke My PowerShell Script</title>
      <link>https://looselydecoupled.com/powershell-null-left-side-comparison-array/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/powershell-null-left-side-comparison-array/</guid>
      <pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>PowerShell</category>
      <category>Code quality</category>
      
      <description>The analyzer&#39;s fix is safe only where code meant a null check; this function relied on the array filter in ways only empty and nearly empty inputs reveal.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>PSScriptAnalyzer&rsquo;s <code>PSPossibleIncorrectComparisonWithNull</code> rule is right for
most code. When the left side of <code>-ne</code> is an array, PowerShell filters the array instead
of returning true or false. These two tests ask different questions:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># Does $items hold at least one item that is not null?</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$items</span> <span class="o">-ne</span> <span class="vm">$null</span><span class="p">)</span> <span class="p">{</span> <span class="s1">&#39;has items&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Is the variable itself something other than null?</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="vm">$null</span> <span class="o">-ne</span> <span class="nv">$items</span><span class="p">)</span> <span class="p">{</span> <span class="s1">&#39;not null&#39;</span> <span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>An empty array fails the first test and passes the second. If the code you are
cleaning up relies on the first, state that meaning in a form the analyzer
accepts. It differs in one case: when the filter leaves exactly one item and
that item is falsy, such as <code>0</code>, <code>''</code>, <code>$false</code> or an empty array. That case
is covered below:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Test-HasItem</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">(</span><span class="nv">$Value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="vm">@</span><span class="p">(</span><span class="nv">$Value</span> <span class="p">|</span> <span class="nb">Where-Object</span> <span class="p">{</span> <span class="vm">$null</span> <span class="o">-ne</span> <span class="nv">$_</span> <span class="p">}).</span><span class="py">Count</span> <span class="o">-gt</span> <span class="mf">0</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nb">Test-HasItem</span> <span class="nv">$items</span><span class="p">)</span> <span class="p">{</span> <span class="s1">&#39;has items&#39;</span> <span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div>
</div>

<p>I ran into this cleaning up old PowerShell that someone else wrote. The plan
was mechanical: run PSScriptAnalyzer and take its suggestions one at a time.
One of them came from the rule <code>PSPossibleIncorrectComparisonWithNull</code>:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$null should be on the left side of equality comparisons.</span></span></code></pre></div></div>
  </div>
</div><p>I flipped the comparisons, and the warning went away. So did part of what the
function did. The original author had written <code>$array -ne $null</code> on purpose,
and the function depended on the behavior that the rule exists to warn about.</p>
<p>That&rsquo;s why I wanted to write it up. Nothing here is a PowerShell bug, and the
rule gives good advice for most code. This code was the exception: the author
meant what they wrote, and fixing it broke it.</p>
<!-- style-check: procedure -->
<h2 id="reproduce-the-difference">Reproduce the difference<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#reproduce-the-difference"
     aria-label="Link to section: Reproduce the difference" data-pagefind-ignore></a>
</h2>
<p>In Windows PowerShell 5.1 or PowerShell 7, run:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">$values</span> <span class="console__token">=</span> <span class="console__token">@()</span>
</span><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">if</span> <span class="console__token">($values</span> <span class="console__token">-ne</span> <span class="console__token">$null)</span> <span class="console__token">{</span> <span class="console__token">&#39;has</span> <span class="console__token">values&#39;</span> <span class="console__token">}</span> <span class="console__token">else</span> <span class="console__token">{</span> <span class="console__token">&#39;nothing</span> <span class="console__token">to</span> <span class="console__token">add&#39;</span> <span class="console__token">}</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">nothing</span> <span class="console__token">to</span> <span class="console__token">add</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">if</span> <span class="console__token">($null</span> <span class="console__token">-ne</span> <span class="console__token">$values)</span> <span class="console__token">{</span> <span class="console__token">&#39;has</span> <span class="console__token">values&#39;</span> <span class="console__token">}</span> <span class="console__token">else</span> <span class="console__token">{</span> <span class="console__token">&#39;nothing</span> <span class="console__token">to</span> <span class="console__token">add&#39;</span> <span class="console__token">}</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">has</span> <span class="console__token">values</span></span></code></pre>
    </div>
  </div></div><p>The same variable, compared with the same operator, gives opposite answers.</p>
<!-- style-check: end procedure -->
<h2 id="why-the-two-comparisons-disagree">Why the two comparisons disagree<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#why-the-two-comparisons-disagree"
     aria-label="Link to section: Why the two comparisons disagree" data-pagefind-ignore></a>
</h2>
<p>An equality or matching operator with a collection on its left filters the
collection. It
returns every item for which the comparison is true, as an array.
<a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comparison_operators" rel="external noopener" target="_blank">about_Comparison_Operators<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
documents this for every equality and matching operator.</p>
<p>So <code>@() -ne $null</code> returns an empty array, and <code>if</code> converts an empty array to
<code>$false</code>. <code>$null -ne @()</code> has a scalar on the left, so it compares once and
returns <code>$true</code>, because an empty array isn&rsquo;t <code>$null</code>.</p>
<p>That&rsquo;s why the analyzer asks for <code>$null</code> on the left: most code that writes
<code>$x -ne $null</code> means &ldquo;is <code>$x</code> null?&rdquo;, and gets a filter instead when <code>$x</code>
happens to be an array. The rule&rsquo;s own
<a href="https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/possibleincorrectcomparisonwithnull" rel="external noopener" target="_blank">documentation<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
says the same. The rule is correct about that code. It can&rsquo;t tell that
somebody&rsquo;s code wanted the filter.</p>
<p>The filter has a second property, which matters later. <code>if</code> treats an array
that holds exactly one item as that item. <code>@(0) -ne $null</code> returns <code>@(0)</code>, and
<code>if (@(0))</code> is <code>$false</code> because <code>0</code> is.</p>
<h2 id="what-the-original-code-meant">What the original code meant<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#what-the-original-code-meant"
     aria-label="Link to section: What the original code meant" data-pagefind-ignore></a>
</h2>
<p>The function built combinations. Each call took the rows built so far and a set
of values, and appended every value to every row. The two comparisons that the
analyzer flagged are highlighted:</p>
<div class="code-block" data-copyable data-copy-label="Add-ToAll.ps1">
  <div class="code-block__head"><span class="code-block__lang code-block__file">Add-ToAll.ps1</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Add-ToAll</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)]</span> <span class="nv">$currentResult</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)]</span> <span class="nv">$valuesToAdd</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$returnValue</span> <span class="p">=</span> <span class="vm">@</span><span class="p">()</span>
</span></span><span class="line hl"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$valuesToAdd</span> <span class="o">-ne</span> <span class="vm">$null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">foreach</span> <span class="p">(</span><span class="nv">$value</span> <span class="k">in</span> <span class="nv">$valuesToAdd</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line hl"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nv">$currentResult</span> <span class="o">-ne</span> <span class="vm">$null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">foreach</span> <span class="p">(</span><span class="nv">$result</span> <span class="k">in</span> <span class="nv">$currentResult</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nv">$newResult</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="nv">$result</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="nv">$newResult</span> <span class="p">+=</span> <span class="p">,</span><span class="nv">$value</span>
</span></span><span class="line"><span class="cl">                    <span class="nv">$returnValue</span> <span class="p">+=</span> <span class="p">,</span><span class="nv">$newResult</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$returnValue</span> <span class="p">+=</span> <span class="p">,</span><span class="vm">@</span><span class="p">(,</span><span class="nv">$value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$returnValue</span> <span class="p">=</span> <span class="nv">$currentResult</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">,</span><span class="nv">$returnValue</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>Both comparisons use the filter on purpose:</p>
<ul>
<li><code>$valuesToAdd -ne $null</code> is false for an empty array, so there&rsquo;s nothing to
add and the rows come back unchanged.</li>
<li><code>$currentResult -ne $null</code> is false for an empty array, so there are no rows
yet and each value starts one.</li>
</ul>
<p>The first call passes <code>@()</code> as <code>$currentResult</code> to start the chain. With the
operands flipped, <code>$null -ne @()</code> is true, the inner loop runs over nothing,
and the first call returns an empty array. Every later call appends to no rows
and also returns nothing.</p>
<p>Each version, run on the same inputs, with its result as JSON:</p>
<div class="table-wrap" data-scrollable data-label="Table: Rows so far">
  <table>
    <thead>
      <tr><th scope="col">Rows so far</th><th scope="col">Values to add</th><th scope="col">Original returns</th><th scope="col">Flipped returns</th></tr>
    </thead>
    <tbody>
      <tr><td><code>@()</code></td><td><code>@()</code></td><td><code>[]</code></td><td><code>[]</code></td></tr>
      <tr><td><code>@()</code></td><td><code>@('A', 'B')</code></td><td><code>[[&quot;A&quot;],[&quot;B&quot;]]</code></td><td><code>[]</code></td></tr>
      <tr><td><code>@(@('X'), @('Y'))</code></td><td><code>@()</code></td><td><code>[[&quot;X&quot;],[&quot;Y&quot;]]</code></td><td><code>[]</code></td></tr>
      <tr><td><code>@(@('X'), @('Y'))</code></td><td><code>@($null)</code></td><td><code>[[&quot;X&quot;],[&quot;Y&quot;]]</code></td><td><code>[[&quot;X&quot;,null],[&quot;Y&quot;,null]]</code></td></tr>
      <tr><td><code>@(,@('X'))</code></td><td><code>'A'</code></td><td><code>[[&quot;X&quot;,&quot;A&quot;]]</code></td><td><code>[[&quot;X&quot;,&quot;A&quot;]]</code></td></tr>
      <tr><td><code>@(@('X'), @('Y'))</code></td><td><code>@(0)</code></td><td><code>[[&quot;X&quot;],[&quot;Y&quot;]]</code></td><td><code>[[&quot;X&quot;,0],[&quot;Y&quot;,0]]</code></td></tr>
    </tbody>
  </table>
</div>
<p>Two rows still agree: both inputs empty, and the ordinary case, a real value
added to rows that exist. A test suite built from typical inputs passes, and the change surfaces in
production, on the first empty input.</p>
<!-- style-check: procedure -->
<h2 id="pin-the-current-behavior-in-a-pester-test">Pin the current behavior in a Pester test<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#pin-the-current-behavior-in-a-pester-test"
     aria-label="Link to section: Pin the current behavior in a Pester test" data-pagefind-ignore></a>
</h2>
<p>The analyzer&rsquo;s fix looked safe because nothing checked what the function
returned. Before you change inherited code, write a characterization test: run
the current code on chosen inputs and assert what it returns now, whether or
not that looks right. A refactor then has to keep every answer or change one
on purpose.</p>
<p>This test for Pester 5, the PowerShell test framework, turns the earlier table
into cases. Save the function as <code>Add-ToAll.ps1</code> in the same folder:</p>
<div class="code-block" data-copyable data-copy-label="Add-ToAll.Tests.ps1">
  <div class="code-block__head"><span class="code-block__lang code-block__file">Add-ToAll.Tests.ps1</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">BeforeAll</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span> <span class="nv">$PSScriptRoot</span><span class="p">/</span><span class="nb">Add-ToAll</span><span class="p">.</span><span class="py">ps1</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Describe</span> <span class="s1">&#39;Add-ToAll&#39;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">It</span> <span class="s1">&#39;&lt;Name&gt;&#39;</span> <span class="n">-TestCases</span> <span class="vm">@</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;both empty&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">();</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;start from empty&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;A&#34;],[&#34;B&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">();</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="s1">&#39;A&#39;</span><span class="p">,</span> <span class="s1">&#39;B&#39;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;nothing to add&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;X&#34;],[&#34;Y&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="vm">@</span><span class="p">(</span><span class="s1">&#39;X&#39;</span><span class="p">),</span> <span class="vm">@</span><span class="p">(</span><span class="s1">&#39;Y&#39;</span><span class="p">));</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;add null&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;X&#34;],[&#34;Y&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="vm">@</span><span class="p">(</span><span class="s1">&#39;X&#39;</span><span class="p">),</span> <span class="vm">@</span><span class="p">(</span><span class="s1">&#39;Y&#39;</span><span class="p">));</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="vm">$null</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;one value&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;X&#34;,&#34;A&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(,</span><span class="vm">@</span><span class="p">(</span><span class="s1">&#39;X&#39;</span><span class="p">));</span> <span class="n">Values</span> <span class="p">=</span> <span class="s1">&#39;A&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;a single zero&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;X&#34;],[&#34;Y&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="vm">@</span><span class="p">(</span><span class="s1">&#39;X&#39;</span><span class="p">),</span> <span class="vm">@</span><span class="p">(</span><span class="s1">&#39;Y&#39;</span><span class="p">));</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$result</span> <span class="p">=</span> <span class="nb">Add-ToAll</span> <span class="n">-currentResult</span> <span class="nv">$Rows</span> <span class="n">-valuesToAdd</span> <span class="nv">$Values</span>
</span></span><span class="line"><span class="cl">        <span class="nb">ConvertTo-Json</span> <span class="n">-InputObject</span> <span class="nv">$result</span> <span class="n">-Compress</span> <span class="p">|</span> <span class="n">Should</span> <span class="n">-Be</span> <span class="nv">$Json</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>The cases that matter are the ones the old comparison treated specially: an
empty array, an array of nulls and a single falsy value. They are where a
flipped comparison changes the result, and they are the inputs a typical test
suite leaves out.</p>
<p>Run <code>Invoke-Pester ./Add-ToAll.Tests.ps1</code> before you touch the function. Every
case passes, because each expected value was copied from the original&rsquo;s
output. Flip the two comparisons and four cases fail: start from empty,
nothing to add, add null and a single zero.</p>
<div class="admonition admonition--note" role="note">
  <p class="admonition__label">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#i-info"/></svg>
    <span>Note</span>
  </p>
  <p>Windows PowerShell 5.1 ships with Pester 3.4, which runs this file
differently. Install Pester 5 first; the
<a href="https://pester.dev/docs/introduction/installation" rel="external noopener" target="_blank">Pester installation guide<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
covers replacing the built-in version.</p>
</div><p>Each case compares JSON text instead of the array itself. Pass the result to
<code>ConvertTo-Json</code> with <code>-InputObject</code>, not through the pipeline:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">@(,@(&#39;X&#39;,&#39;A&#39;))</span> <span class="console__token">|</span> <span class="console__token">ConvertTo-Json</span> <span class="console__token">-Compress</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">[&#34;X&#34;,&#34;A&#34;]</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">@(&#39;X&#39;,&#39;A&#39;)</span> <span class="console__token">|</span> <span class="console__token">ConvertTo-Json</span> <span class="console__token">-Compress</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">[&#34;X&#34;,&#34;A&#34;]</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">ConvertTo-Json</span> <span class="console__token">-InputObject</span> <span class="console__token">@(,@(&#39;X&#39;,&#39;A&#39;))</span> <span class="console__token">-Compress</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">[[&#34;X&#34;,&#34;A&#34;]]</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">ConvertTo-Json</span> <span class="console__token">-InputObject</span> <span class="console__token">@(&#39;X&#39;,&#39;A&#39;)</span> <span class="console__token">-Compress</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">[&#34;X&#34;,&#34;A&#34;]</span></span></code></pre>
    </div>
  </div></div><p>That output is PowerShell 7. Windows PowerShell 5.1 prints the first line as
<code>{&quot;value&quot;:[&quot;X&quot;,&quot;A&quot;],&quot;Count&quot;:2}</code>, which is one more reason to use
<code>-InputObject</code>.</p>
<p>The pipeline sends the items of an array, not the array, and that loses
information in two ways:</p>
<ul>
<li>A result with one row loses a level of nesting. One row, <code>[[&quot;X&quot;,&quot;A&quot;]]</code>, and
a flat row, <code>[&quot;X&quot;,&quot;A&quot;]</code>, serialize to the same text, so a refactor that
returned the wrong shape would pass.</li>
<li>An empty result sends nothing, so <code>ConvertTo-Json</code> writes nothing. With
<code>-InputObject</code>, the same result is written as <code>[]</code>.</li>
</ul>
<p>When you write the cases, two inputs look different and are not:</p>
<ul>
<li><code>@(@())</code> is an empty array, not an array that holds one empty array. The
array subexpression operator does not nest. Write <code>,@()</code> for the nested one.</li>
<li><code>@(@('X'))</code> is <code>@('X')</code>, for the same reason. Write <code>@(,@('X'))</code>.</li>
</ul>
<h2 id="make-the-intent-explicit">Make the intent explicit<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#make-the-intent-explicit"
     aria-label="Link to section: Make the intent explicit" data-pagefind-ignore></a>
</h2>
<p>Replace each filtering comparison with a test that says what it checks. Keep
the function&rsquo;s structure and change only the two conditions:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># True when $Value holds at least one item that is not null. An empty array</span>
</span></span><span class="line"><span class="cl"><span class="c"># and an array of nulls hold none, which is what `$Value -ne $null` tested.</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Test-HasItem</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">(</span><span class="nv">$Value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="vm">@</span><span class="p">(</span><span class="nv">$Value</span> <span class="p">|</span> <span class="nb">Where-Object</span> <span class="p">{</span> <span class="vm">$null</span> <span class="o">-ne</span> <span class="nv">$_</span> <span class="p">}).</span><span class="py">Count</span> <span class="o">-gt</span> <span class="mf">0</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nb">Test-HasItem</span> <span class="nv">$valuesToAdd</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$value</span> <span class="k">in</span> <span class="nv">$valuesToAdd</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nb">Test-HasItem</span> <span class="nv">$currentResult</span><span class="p">)</span> <span class="p">{</span></span></span></code></pre></div></div>
  </div>
</div><p>PSScriptAnalyzer reports nothing for this version. Run the test again and
one case fails: a single zero. Given <code>@(0)</code>, the explicit version returns
<code>[[&quot;X&quot;,0],[&quot;Y&quot;,0]]</code>, and the original returned the rows unchanged.</p>
<p>The original treated <code>@(0)</code> as nothing to add, because an array with one item
converts to that item and <code>0</code> is false. The same is true of <code>@('')</code>,
<code>@($false)</code> and <code>@(,@())</code>, an array holding one empty array, and of any input
the filter reduces to one such item, such as <code>@($null, 0)</code>. That is the one-item rule from earlier, not a decision anyone
made, and the explicit version drops it. If your data can contain a single
zero, empty string, <code>$false</code> or empty array, decide which behavior is correct. Change that
case&rsquo;s expected value only if the new behavior is the one you want.</p>
<!-- style-check: end procedure -->
<h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<p><strong>The warning is on <code>-eq</code>, not <code>-ne</code>.</strong> The same filter applies.
<code>@($null, $null) -eq $null</code> returns two nulls, and an array with two items is
true in an <code>if</code>, even though both items are null.</p>
<p><strong>The left side is never an array.</strong> Then flipping the comparison changes
nothing, and it guards against the day it becomes one. Check where the value
comes from: a cmdlet that returns one object returns it bare, and one that
returns several returns an array.</p>
<p><strong>You want to keep the old line and silence the rule.</strong> Suppress it on that
function with a reason, so the next person reads why:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">[</span><span class="n">Diagnostics</span><span class="p">.</span><span class="py">CodeAnalysis</span><span class="p">.</span><span class="py">SuppressMessageAttribute</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;PSPossibleIncorrectComparisonWithNull&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">Justification</span> <span class="p">=</span> <span class="s1">&#39;Empty array means nothing to add.&#39;</span><span class="p">)]</span></span></span></code></pre></div></div>
  </div>
</div><p>The attribute goes above the function&rsquo;s <code>param()</code> block. The explicit test is
still the better fix, because the reason then lives in the code rather than in
a comment about it.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>Windows PowerShell</dt> <dd>5.1 on Windows</dd></div>
    <div class="tested__row"><dt>PowerShell</dt> <dd>7 on macOS, 7.4.20 on Ubuntu 24.04</dd></div>
    <div class="tested__row"><dt>PSScriptAnalyzer</dt> <dd>1.23.0, 1.25.0</dd></div>
    <div class="tested__row"><dt>Pester</dt> <dd>5.9.1</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>September 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>Why $LASTEXITCODE Doesn&#39;t Work the Way You Expect in PowerShell</title>
      <link>https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/</guid>
      <pubDate>Wed, 23 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>PowerShell</category>
      <category>Windows Server</category>
      <category>Automation</category>
      <category>AWS</category>
      
      <description>Cmdlets never set LASTEXITCODE, so after a failed cmdlet it still holds whatever the last native program returned, often zero.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p><code>$LASTEXITCODE</code> is not the success status of the last PowerShell cmdlet. Use it
for a native executable such as <code>aws.exe</code>, and capture it immediately:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">aws</span> <span class="p">-</span><span class="n">-version</span>
</span></span><span class="line"><span class="cl"><span class="nv">$Code</span> <span class="p">=</span> <span class="nv">$LASTEXITCODE</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$Code</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">&#34;AWS CLI exited with code </span><span class="nv">$Code</span><span class="s2">&#34;</span> <span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>For a cmdlet, use PowerShell error handling:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-Item</span> <span class="n">-LiteralPath</span> <span class="s1">&#39;C:\Data\settings.json&#39;</span> <span class="n">-ErrorAction</span> <span class="n">Stop</span></span></span></code></pre></div></div>
  </div>
</div><p><code>-ErrorAction Stop</code> turns a cmdlet&rsquo;s non-terminating error into a terminating
one, which stops the script or reaches an enclosing <code>catch</code>.</p>

</div>

<p>While working on a backup script for my
<a href="https://looselydecoupled.com/backup-7-days-to-die-server-s3/">7 Days to Die game server</a>,
I thought it would be good to share this.</p>
<p>You replace an AWS CLI command with a PowerShell cmdlet and keep the success
check. The cmdlet prints an error. The script announces success anyway.</p>
<p>The check still runs. It&rsquo;s reading the result of a different operation.</p>
<!-- style-check: procedure -->
<h2 id="reproduce-a-failed-command-with-exit-code-zero">Reproduce a failed command with exit code zero<a class="heading-anchor" href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#reproduce-a-failed-command-with-exit-code-zero"
     aria-label="Link to section: Reproduce a failed command with exit code zero" data-pagefind-ignore></a>
</h2>
<p>Run this in PowerShell on Windows:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">cmd</span><span class="p">.</span><span class="py">exe</span> <span class="p">/</span><span class="n">d</span> <span class="p">/</span><span class="n">c</span> <span class="n">exit</span> <span class="mf">0</span>
</span></span><span class="line"><span class="cl"><span class="nb">Write-Error</span> <span class="s1">&#39;Demonstration: the upload failed&#39;</span> <span class="n">-ErrorAction</span> <span class="k">Continue</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;Native exit code: </span><span class="nv">$LASTEXITCODE</span><span class="s2">&#34;</span></span></span></code></pre></div></div>
  </div>
</div><p>The error appears, followed by:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Native exit code: 0</span></span></code></pre></div></div>
  </div>
</div><p>The zero came from <code>cmd.exe</code>. <code>Write-Error</code> did not update it. The explicit
<code>Continue</code> makes this demonstration non-terminating even if your session normally
uses a stricter error preference.</p>
<p>The reverse is just as misleading:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">cmd</span><span class="p">.</span><span class="py">exe</span> <span class="p">/</span><span class="n">d</span> <span class="p">/</span><span class="n">c</span> <span class="n">exit</span> <span class="mf">7</span>
</span></span><span class="line"><span class="cl"><span class="nb">Get-Date</span> <span class="p">|</span> <span class="nb">Out-Null</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;Native exit code: </span><span class="nv">$LASTEXITCODE</span><span class="s2">&#34;</span></span></span></code></pre></div></div>
  </div>
</div><p>This prints <code>7</code> even though <code>Get-Date</code> succeeded. These examples illustrate why
a value left in the variable is not evidence about a later cmdlet.</p>
<p>Microsoft defines the variable for native programs and PowerShell scripts;
script behavior also depends on invocation and the <code>exit</code> keyword. Ordinary
cmdlet success or failure does not make it a universal status register. See
<a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables#lastexitcode" rel="external noopener" target="_blank">about_Automatic_Variables<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>.</p>
<h2 id="for-native-commands-check-each-result-at-the-boundary">For native commands, check each result at the boundary<a class="heading-anchor" href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#for-native-commands-check-each-result-at-the-boundary"
     aria-label="Link to section: For native commands, check each result at the boundary" data-pagefind-ignore></a>
</h2>
<p>This example assumes AWS CLI is installed, the source exists, and the active
AWS identity can access your chosen bucket:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$Source</span> <span class="p">=</span> <span class="s1">&#39;C:\Data\exports&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$Destination</span> <span class="p">=</span> <span class="s1">&#39;s3://YOUR-BUCKET/exports/&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="vm">$null</span> <span class="p">=</span> <span class="nb">Get-Command</span> <span class="n">aws</span> <span class="n">-CommandType</span> <span class="n">Application</span> <span class="n">-ErrorAction</span> <span class="n">Stop</span>
</span></span><span class="line"><span class="cl"><span class="n">aws</span> <span class="n">s3</span> <span class="n">sync</span> <span class="nv">$Source</span> <span class="nv">$Destination</span>
</span></span><span class="line"><span class="cl"><span class="nv">$Code</span> <span class="p">=</span> <span class="nv">$LASTEXITCODE</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$Code</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">throw</span> <span class="s2">&#34;S3 sync did not complete cleanly; AWS CLI exit code </span><span class="nv">$Code</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="s1">&#39;Export transfer completed&#39;</span></span></span></code></pre></div></div>
  </div>
</div><p>Replace <code>YOUR-BUCKET</code> before running it. AWS documents nonzero outcomes including
failed S3 transfers and skipped files. A skipped file is enough to reject a
backup run as incomplete; inspect the command output for the cause. See the
<a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-usage-returncodes.html" rel="external noopener" target="_blank">AWS CLI return-code reference<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>.</p>
<div class="admonition admonition--warning" role="note">
  <p class="admonition__label">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#i-triangle-alert"/></svg>
    <span>Warning</span>
  </p>
  <p>Do not run three transfers and check only after the third. A later successful
native command replaces an earlier failure code with zero, so the script
reports success for a backup that partly failed. That is worse than no
success message at all: you now have positive evidence for something untrue,
and you will not look again until you need the data.</p>
</div><p>Capture and handle each result before moving on.</p>
<p>Also distinguish a process failure from a launch failure. If <code>aws</code> is not
found, no AWS CLI process returned an exit code, and the check reads whatever
an earlier native command left. The <code>Get-Command</code> line stops the script before
that can happen.</p>
<!-- style-check: end procedure -->
<h2 id="why-trycatch-sometimes-misses-a-cmdlet-error">Why try/catch sometimes misses a cmdlet error<a class="heading-anchor" href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#why-trycatch-sometimes-misses-a-cmdlet-error"
     aria-label="Link to section: Why try/catch sometimes misses a cmdlet error" data-pagefind-ignore></a>
</h2>
<p>PowerShell can report an error while continuing execution. For example:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Write-Error</span> <span class="s1">&#39;Demonstration failure&#39;</span> <span class="n">-ErrorAction</span> <span class="k">Continue</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;Still inside try&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;Caught the error&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>The error is displayed and <code>Still inside try</code> prints. The catch block isn&rsquo;t
entered. Change that one invocation to <code>-ErrorAction Stop</code> and the catch block
runs instead.</p>
<p>Some errors already terminate execution; they don&rsquo;t need this promotion.
<code>-ErrorAction Stop</code> makes the intended behavior explicit for cmdlet errors that
would otherwise continue. Microsoft&rsquo;s <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_try_catch_finally" rel="external noopener" target="_blank">try/catch documentation<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
explains which errors those blocks handle.</p>
<h2 id="catch-a-write-s3object-failure-with--erroraction-stop">Catch a <code>Write-S3Object</code> failure with <code>-ErrorAction Stop</code><a class="heading-anchor" href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#catch-a-write-s3object-failure-with--erroraction-stop"
     aria-label="Link to section: Catch a Write-S3Object failure with -ErrorAction Stop" data-pagefind-ignore></a>
</h2>
<p>With <code>AWS.Tools.S3</code> installed and AWS access configured, use this for an
intentional upload of one existing file:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Import-Module</span> <span class="n">AWS</span><span class="p">.</span><span class="py">Tools</span><span class="p">.</span><span class="py">S3</span> <span class="n">-ErrorAction</span> <span class="n">Stop</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Write-S3Object</span> <span class="n">-BucketName</span> <span class="s1">&#39;YOUR-BUCKET&#39;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">        <span class="o">-File</span> <span class="s1">&#39;C:\Data\exports\report.json&#39;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">        <span class="n">-Key</span> <span class="s1">&#39;exports/report.json&#39;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">        <span class="n">-ErrorAction</span> <span class="n">Stop</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;Upload completed&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Write-Warning</span> <span class="p">(</span><span class="s2">&#34;Upload failed: {0}&#34;</span> <span class="o">-f</span> <span class="nv">$_</span><span class="p">.</span><span class="py">Exception</span><span class="p">.</span><span class="n">Message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">throw</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>The success message is reachable only after the upload returns normally. The
bare <code>throw</code> propagates the caught error to the caller. Logging and then
continuing would hide the failure from whatever invoked the script.</p>
<p>If you have no context to add and no recovery to attempt, remove the <code>try/catch</code>
and keep <code>-ErrorAction Stop</code>. A catch block that only rethrows isn&rsquo;t required.</p>
<h2 id="psnativecommanduseerroractionpreference-in-powershell-7"><code>$PSNativeCommandUseErrorActionPreference</code> in PowerShell 7<a class="heading-anchor" href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#psnativecommanduseerroractionpreference-in-powershell-7"
     aria-label="Link to section: $PSNativeCommandUseErrorActionPreference in PowerShell 7" data-pagefind-ignore></a>
</h2>
<p>PowerShell 7.4 added <code>$PSNativeCommandUseErrorActionPreference</code>. When it&rsquo;s
<code>$true</code>, nonzero native exit codes produce PowerShell errors governed by
<code>$ErrorActionPreference</code>; with <code>Stop</code>, those can reach <code>catch</code>. The default is
<code>$false</code>. Microsoft documents it in
<a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables#psnativecommanduseerroractionpreference" rel="external noopener" target="_blank">about_Preference_Variables<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>.</p>
<p>That doesn&rsquo;t make <code>-ErrorAction Stop</code> a parameter for <code>aws.exe</code>. Passed to a
native program, it&rsquo;s an argument for that program to interpret. Explicit
exit-code checks remain useful for scripts spanning Windows PowerShell 5.1 and
PowerShell 7, and for tools whose nonzero codes have special meanings.</p>
<p>If native error promotion is enabled with <code>Stop</code>, execution can leave the native
invocation before the following explicit check. Either path must preserve the
failure; don&rsquo;t catch it and then print success.</p>
<h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/powershell-lastexitcode-cmdlet-errors/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<p><strong>LASTEXITCODE is empty or stale.</strong> There may be no relevant native result in the
session. Choose the error mechanism for the operation you actually ran.</p>
<p><strong>Can I replace it with <code>$?</code> everywhere?</strong> <code>$?</code> describes the last command&rsquo;s
execution status and is overwritten by the next command. It isn&rsquo;t a durable
record of a multi-step job. Structured error handling is clearer for cmdlets.</p>
<p><strong>The task still reports success.</strong> Check whether a wrapper catches and suppresses
the error, then exits normally. At the outer script boundary, preserve a failure
through an unhandled terminating error or an intentional nonzero <code>exit</code> code.</p>
<p>A script needs to remember whether the work succeeded. That starts by listening
to the failure signal the command actually provides.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>OS</dt> <dd>Windows Server 2019 (10.0.17763)</dd></div>
    <div class="tested__row"><dt>PowerShell</dt> <dd>Windows PowerShell 5.1</dd></div>
    <div class="tested__row"><dt>PowerShell 7 section</dt> <dd>7.6.6 on macOS 27.0</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>Why Does My Batch File Exit When I Call a Function?</title>
      <link>https://looselydecoupled.com/batch-file-exits-when-calling-function/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/batch-file-exits-when-calling-function/</guid>
      <pubDate>Mon, 07 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Windows Server</category>
      <category>Batch</category>
      <category>Automation</category>
      
      <description>The subroutine runs before anything calls it, and its goto :eof ends the whole script, because cmd.exe reads straight down through a label.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>A batch label does not create a function body that <code>cmd.exe</code> skips. Execution
runs straight through the label unless you redirect it.</p>
<p>Put the main work first, end it with <code>exit /b</code>, and place subroutines below it:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="BATCH">
  <div class="code-block__head"><span class="code-block__lang">BATCH</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-batch" data-lang="batch"><span class="line"><span class="cl"><span class="p">@</span><span class="k">echo</span> off
</span></span><span class="line"><span class="cl"><span class="k">setlocal</span> EnableExtensions
</span></span><span class="line"><span class="cl"><span class="k">call</span> <span class="p">:</span><span class="nl">timestamp</span>
</span></span><span class="line"><span class="cl"><span class="k">echo</span> [<span class="nv">%stamp%</span>] Work starts here
</span></span><span class="line"><span class="cl"><span class="k">exit</span> /b 0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">:</span><span class="nl">timestamp</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="s2">&#34;stamp=</span><span class="nv">%DATE%</span><span class="s2"> </span><span class="nv">%TIME%</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">goto</span> <span class="p">:</span><span class="nl">eof</span></span></span></code></pre></div></div>
  </div>
</div><p><code>call :timestamp</code> creates a return point. <code>goto :eof</code> returns from that call.
Reached in the main batch context, it ends the batch instead.</p>

</div>

<p>You add a timestamp helper to a working batch file. The helper looks reasonable.
The next run prints almost nothing, and the real work never starts.</p>
<p>It&rsquo;s tempting to debug the timestamp expression. First check whether the
script ever reaches the line that calls it.</p>
<h2 id="minimal-reproduction-the-call-never-happens">Minimal reproduction: the call never happens<a class="heading-anchor" href="https://looselydecoupled.com/batch-file-exits-when-calling-function/#minimal-reproduction-the-call-never-happens"
     aria-label="Link to section: Minimal reproduction: the call never happens" data-pagefind-ignore></a>
</h2>
<p>Save this as <code>fall-through.cmd</code>:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="fall-through.cmd">
  <div class="code-block__head"><span class="code-block__lang code-block__file">fall-through.cmd</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-batch" data-lang="batch"><span class="line"><span class="cl"><span class="p">@</span><span class="k">echo</span> off
</span></span><span class="line"><span class="cl"><span class="k">echo</span> Starting script
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">:</span><span class="nl">timestamp</span>
</span></span><span class="line"><span class="cl"><span class="k">echo</span> Inside timestamp
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="s2">&#34;stamp=</span><span class="nv">%DATE%</span><span class="s2"> </span><span class="nv">%TIME%</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">goto</span> <span class="p">:</span><span class="nl">eof</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">call</span> <span class="p">:</span><span class="nl">timestamp</span>
</span></span><span class="line"><span class="cl"><span class="k">echo</span> [<span class="nv">%stamp%</span>] Doing the work</span></span></code></pre></div></div>
  </div>
</div><p>Run it from an existing Command Prompt so the output stays visible:</p>
<div class="console"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Expected output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">C:\scripts&gt;</span> <span class="console__token">cmd</span> <span class="console__token">/d</span> <span class="console__token">/e:on</span> <span class="console__token">/c</span> <span class="console__token">fall-through.cmd</span>
</span><span class="line"><span class="console__token">Starting</span> <span class="console__token">script</span>
</span><span class="line"><span class="console__token">Inside</span> <span class="console__token">timestamp</span>
</span><span class="line">
</span><span class="line"><span class="console__token">C:\scripts&gt;</span></span></code></pre>
    </div>
  </div></div><p>That output is the clue: the helper ran even though the <code>call</code> below it was
never reached. Execution arrived there by reading downward.</p>
<p>A label marks a location. It doesn&rsquo;t surround or hide the commands beneath it.
The <code>goto :eof</code> then ends the current batch context, so both remaining lines
are unreachable on this path. Microsoft&rsquo;s <a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/goto" rel="external noopener" target="_blank">goto reference<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/batch-file-exits-when-calling-function/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
documents the special end-of-file target; you don&rsquo;t need to define an <code>:eof</code> label.</p>
<h2 id="end-the-main-path-with-exit-b-before-the-subroutines">End the main path with <code>exit /b</code> before the subroutines<a class="heading-anchor" href="https://looselydecoupled.com/batch-file-exits-when-calling-function/#end-the-main-path-with-exit-b-before-the-subroutines"
     aria-label="Link to section: End the main path with exit /b before the subroutines" data-pagefind-ignore></a>
</h2>
<p>Here is the complete repaired example:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="no-fall-through.cmd">
  <div class="code-block__head"><span class="code-block__lang code-block__file">no-fall-through.cmd</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-batch" data-lang="batch"><span class="line"><span class="cl"><span class="p">@</span><span class="k">echo</span> off
</span></span><span class="line"><span class="cl"><span class="k">setlocal</span> EnableExtensions
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">echo</span> Starting script
</span></span><span class="line"><span class="cl"><span class="k">call</span> <span class="p">:</span><span class="nl">timestamp</span>
</span></span><span class="line"><span class="cl"><span class="k">echo</span> [<span class="nv">%stamp%</span>] Doing the work
</span></span><span class="line"><span class="cl"><span class="k">exit</span> /b 0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">:</span><span class="nl">timestamp</span>
</span></span><span class="line"><span class="cl"><span class="k">echo</span> Inside timestamp
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="s2">&#34;stamp=</span><span class="nv">%DATE%</span><span class="s2"> </span><span class="nv">%TIME%</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">goto</span> <span class="p">:</span><span class="nl">eof</span></span></span></code></pre></div></div>
  </div>
</div><p>Expected output, with the timestamp varying by locale and clock:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Starting script
</span></span><span class="line"><span class="cl">Inside timestamp
</span></span><span class="line"><span class="cl">[&lt;local date&gt; &lt;local time&gt;] Doing the work</span></span></code></pre></div></div>
  </div>
</div><p>The main path calls the helper, resumes after the call, does its work, and exits
before reaching the helper a second time. The helper is now reachable only
through an intentional call.</p>
<p>Moving a subroutine to the bottom <strong>without</strong> the main-path exit is incomplete.
The call returns correctly, but after the main work finishes, execution falls
into the helper again.</p>
<h2 id="what-call-goto-and-exit-b-each-do">What <code>call</code>, <code>goto</code> and <code>exit /b</code> each do<a class="heading-anchor" href="https://looselydecoupled.com/batch-file-exits-when-calling-function/#what-call-goto-and-exit-b-each-do"
     aria-label="Link to section: What call, goto and exit /b each do" data-pagefind-ignore></a>
</h2>
<p><code>call :label</code> starts a new batch context within the same file. When that context
ends, execution resumes at the statement after the call. <code>goto label</code> only jumps;
it doesn&rsquo;t arrange a return. See Microsoft&rsquo;s <a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/call" rel="external noopener" target="_blank">call reference<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/batch-file-exits-when-calling-function/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>.</p>
<div class="table-wrap" data-scrollable data-label="Table: Command">
  <table>
    <thead>
      <tr><th scope="col">Command</th><th scope="col">Effect</th></tr>
    </thead>
    <tbody>
      <tr><td><code>call :timestamp</code></td><td>Enter the subroutine with a return point</td></tr>
      <tr><td><code>goto timestamp</code></td><td>Jump to the label without a return point</td></tr>
      <tr><td><code>goto :eof</code></td><td>End the current batch context</td></tr>
      <tr><td><code>exit /b 0</code></td><td>End the current batch context and set a success status</td></tr>
      <tr><td><code>exit</code></td><td>Exit the command interpreter</td></tr>
    </tbody>
  </table>
</div>
<div class="admonition admonition--warning" role="note">
  <p class="admonition__label">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/batch-file-exits-when-calling-function/#i-triangle-alert"/></svg>
    <span>Warning</span>
  </p>
  <p>Plain <code>exit</code> inside a batch file exits the <em>command interpreter</em>, not only the
script. Run it from a Command Prompt you were using and that window closes,
taking any unsaved session state with it. <code>exit /b</code> is almost always what you
want.</p>
</div><p>The <code>/b</code> matters. <code>exit /b</code> ends the current batch context instead of cmd.exe,
as Microsoft&rsquo;s <a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/exit" rel="external noopener" target="_blank">exit reference<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/batch-file-exits-when-calling-function/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
says. A <code>call :label</code> runs in its own context, so inside a subroutine <code>exit /b</code>
returns to the caller, and at the top level it ends the file. Use an explicit
code when the caller needs a result.</p>
<p>These label-call and <code>:eof</code> examples assume command extensions are enabled, as
they normally are. The repaired file makes that requirement explicit with
<code>setlocal EnableExtensions</code>.</p>
<h2 id="helpers-can-also-go-above-main">Helpers can also go above main<a class="heading-anchor" href="https://looselydecoupled.com/batch-file-exits-when-calling-function/#helpers-can-also-go-above-main"
     aria-label="Link to section: Helpers can also go above main" data-pagefind-ignore></a>
</h2>
<p>There&rsquo;s no rule that subroutines must live at the bottom. If you prefer helpers
first, jump over them on entry:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="BATCH">
  <div class="code-block__head"><span class="code-block__lang">BATCH</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-batch" data-lang="batch"><span class="line"><span class="cl"><span class="p">@</span><span class="k">echo</span> off
</span></span><span class="line"><span class="cl"><span class="k">setlocal</span> EnableExtensions
</span></span><span class="line"><span class="cl"><span class="k">goto</span> <span class="nl">main</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">:</span><span class="nl">timestamp</span>
</span></span><span class="line"><span class="cl"><span class="k">set</span> <span class="s2">&#34;stamp=</span><span class="nv">%DATE%</span><span class="s2"> </span><span class="nv">%TIME%</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">goto</span> <span class="p">:</span><span class="nl">eof</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">:</span><span class="nl">main</span>
</span></span><span class="line"><span class="cl"><span class="k">call</span> <span class="p">:</span><span class="nl">timestamp</span>
</span></span><span class="line"><span class="cl"><span class="k">echo</span> [<span class="nv">%stamp%</span>] Doing the work
</span></span><span class="line"><span class="cl"><span class="k">exit</span> /b 0</span></span></code></pre></div></div>
  </div>
</div><p>Placement is a readability choice. An unambiguous execution path is the fix.</p>
<h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/batch-file-exits-when-calling-function/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<p><strong>The window disappears immediately.</strong> Run the file from an existing Command
Prompt. A window opened only to run a batch file can close when that file
ends; the disappearance alone doesn&rsquo;t tell you which command caused it.</p>
<p><strong>The helper runs twice.</strong> Look for a missing <code>exit /b</code> or <code>goto :eof</code> between
the end of the main work and the first helper label.</p>
<p><strong>The main work never resumes.</strong> Check for <code>goto timestamp</code> where you intended
<code>call :timestamp</code>, or plain <code>exit</code> inside the helper.</p>
<p><strong>I only needed a timestamp in a log.</strong> A separate subroutine may be unnecessary:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="BATCH">
  <div class="code-block__head"><span class="code-block__lang">BATCH</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-batch" data-lang="batch"><span class="line"><span class="cl"><span class="k">echo</span> [<span class="nv">%DATE%</span> <span class="nv">%TIME%</span>] Backup started<span class="p">&gt;&gt;</span><span class="s2">&#34;backup.log&#34;</span></span></span></code></pre></div></div>
  </div>
</div><p>This is a display timestamp, not a locale-independent filename format. Keep
this simple example outside a parenthesized block, where percent-variable
expansion introduces a separate timing issue.</p>
<p>When a batch file stops early, follow the lines it actually executes. A label
that looks like a function declaration is still on that path.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>OS</dt> <dd>Windows Server 2019 (10.0.17763)</dd></div>
    <div class="tested__row"><dt>Shell</dt> <dd>cmd.exe</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>balenaEtcher Stuck at Starting on macOS: Erase the USB Drive as exFAT</title>
      <link>https://looselydecoupled.com/balena-etcher-stuck-starting-macos/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/balena-etcher-stuck-starting-macos/</guid>
      <pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>macOS</category>
      <category>Linux</category>
      
      <description>An APFS-formatted USB drive stops Etcher&#39;s helper before it writes a byte; erased as exFAT, the same drive flashes normally.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>When balenaEtcher stays at &ldquo;Starting&hellip;&rdquo; with no progress bar and no error,
check the USB drive&rsquo;s format. On a drive formatted as APFS, Etcher&rsquo;s helper
receives the write request and never starts writing. Running Etcher as <code>sudo</code>
doesn&rsquo;t change that. Erase the whole drive as exFAT and flash again.</p>
<p>The erase deletes everything on the drive. Replace <code>N</code> in <code>diskN</code> with the
number <code>diskutil list</code> shows for your USB drive, and check it before the last
command.</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">diskutil</span> <span class="console__token">list</span>
</span><span class="line"><span class="prompt">$ </span><span class="console__token">diskutil</span> <span class="console__token">unmountDisk</span> <span class="console__token">force</span> <span class="console__token">/dev/diskN</span>
</span><span class="line"><span class="prompt">$ </span><span class="console__token">diskutil</span> <span class="console__token">eraseDisk</span> <span class="console__token">ExFAT</span> <span class="console__token">USB</span> <span class="console__token">GPT</span> <span class="console__token">/dev/diskN</span></span></code></pre>
    </div>
  </div></div><p>If Etcher still stops, write the ISO with <code>dd</code> instead, as described in
<a href="https://looselydecoupled.com/bootable-linux-usb-macos-dd/">Create a bootable Linux USB drive on macOS with dd</a>.</p>

</div>

<p>I wanted to repurpose my old Mac, a 2011 iMac, by putting Linux Mint on it.
I wrote the installer USB drive on my Apple Silicon MacBook Pro with
balenaEtcher. Etcher stopped at &ldquo;Starting&hellip;&rdquo; and gave no reason, so I wrote
the drive with <code>dd</code>, and the iMac booted from it. Later I reproduced the hang
on a second drive to find the cause.</p>
<h2 id="symptoms">Symptoms<a class="heading-anchor" href="https://looselydecoupled.com/balena-etcher-stuck-starting-macos/#symptoms"
     aria-label="Link to section: Symptoms" data-pagefind-ignore></a>
</h2>
<p>balenaEtcher (also searched as Balena Etcher) detects the drive and loads the
ISO. After you click Flash, the window stays at:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Starting...</span></span></code></pre></div></div>
  </div>
</div><p>No progress bar appears and no error is raised. Launching the app from a
terminal with elevated permissions makes no difference:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">sudo /Applications/balenaEtcher.app/Contents/MacOS/balenaEtcher</span></span></code></pre></div></div>
  </div>
</div><p>Etcher&rsquo;s DevTools console shows the flasher starting and then stopping short
of the write. This is Etcher 2.1.6 with an APFS-formatted drive:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Starting priviledged flasher sidecar on port 3435
</span></span><span class="line"><span class="cl">/Applications/balenaEtcher.app/Contents/Resources/etcher-util
</span></span><span class="line"><span class="cl">... with privileges ...
</span></span><span class="line"><span class="cl">elevated pending
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">elevated granted
</span></span><span class="line"><span class="cl">etcher-&lt;id&gt;
</span></span><span class="line"><span class="cl">Connection to sidecar flasher process attempt 1 / 10 failed; retrying in 1000ms...
</span></span><span class="line"><span class="cl">etcher-&lt;id&gt;
</span></span><span class="line"><span class="cl">CHILD READY
</span></span><span class="line"><span class="cl">start heartbeat
</span></span><span class="line"><span class="cl">params Object
</span></span><span class="line"><span class="cl">CHILD LOG: Ready ...
</span></span><span class="line"><span class="cl">CHILD LOG: write requested
</span></span><span class="line"><span class="cl">CHILD LOG: Image: /Users/&lt;user&gt;/Downloads/&lt;image&gt;.iso
</span></span><span class="line"><span class="cl">CHILD LOG: Devices: /dev/diskN
</span></span><span class="line"><span class="cl">CHILD LOG: Auto blockmapping: true
</span></span><span class="line"><span class="cl">CHILD LOG: Decompress first: true</span></span></code></pre></div></div>
  </div>
</div><p>The log reaches <code>write requested</code>, echoes the job&rsquo;s settings, and no bytes
follow. In the log, <code>&lt;user&gt;</code> is your account name and <code>&lt;image&gt;</code> is the ISO
you selected. <code>N</code> is your drive&rsquo;s disk number, and <code>&lt;id&gt;</code> is a random name
Etcher gives each helper run.</p>
<h2 id="apfs-drive-that-stops-the-helper-before-the-write">APFS drive that stops the helper before the write<a class="heading-anchor" href="https://looselydecoupled.com/balena-etcher-stuck-starting-macos/#apfs-drive-that-stops-the-helper-before-the-write"
     aria-label="Link to section: APFS drive that stops the helper before the write" data-pagefind-ignore></a>
</h2>
<p>Etcher doesn&rsquo;t write the disk itself. The Electron app launches a separate
privileged helper, the &ldquo;sidecar flasher&rdquo;, and hands it the image and the
device. The <code>CHILD LOG</code> lines after <code>write requested</code> are the helper echoing
that request back. Etcher asks macOS for administrator rights for the helper
itself, which is what the <code>elevated pending</code> and <code>elevated granted</code> lines
record, so <code>sudo</code> adds nothing. Run as <code>sudo</code>, the log is identical.</p>
<p>The log doesn&rsquo;t show why the helper stops. The drive&rsquo;s format does. I
reproduced the hang with a second USB drive, a different ISO and Etcher 2.1.6,
and changed only the format between two runs:</p>
<div class="table-wrap" data-scrollable data-label="Table: Drive format">
  <table>
    <thead>
      <tr><th scope="col">Drive format</th><th scope="col">What Etcher did</th></tr>
    </thead>
    <tbody>
      <tr><td>APFS, GUID partition map</td><td>Stopped at &ldquo;Starting&hellip;&rdquo;; the log ended at <code>Decompress first: true</code></td></tr>
      <tr><td>exFAT, GUID partition map</td><td>Started writing; the next log line was the first progress line</td></tr>
    </tbody>
  </table>
</div>
<p>Erased as exFAT, the same flash continued straight into progress:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">CHILD LOG: Decompress first: true
</span></span><span class="line"><span class="cl">Flashing 1 device, 0% at 1.60 MB/s (total 1.60 MB/s) eta in 235s with 0 failed devices</span></span></code></pre></div></div>
  </div>
</div><p>That matches the reports on balena&rsquo;s forums.
<a href="https://forums.balena.io/t/etcher-stalls-on-0-flashing-never-starts-fix/375032" rel="external noopener" target="_blank">One thread<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/balena-etcher-stuck-starting-macos/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
states that Etcher doesn&rsquo;t work on an APFS drive and surfaces no error.
<a href="https://forums.balena.io/t/mac-14-8-23j21-and-balena-etcher/374842" rel="external noopener" target="_blank">Another<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/balena-etcher-stuck-starting-macos/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
with this hang was resolved by erasing the drive as exFAT.</p>
<p>A USB drive isn&rsquo;t APFS unless someone made it so. Disk Utility offers APFS
only when you erase the whole device with a GUID partition map, not when you
erase a volume on a drive with the older MBR layout. I don&rsquo;t know what the
first drive was formatted as when it hung. <code>dd</code> wrote the Mint image to it
without an error, so the ISO and the drive weren&rsquo;t at fault.</p>
<!-- style-check: procedure -->
<h2 id="erase-the-usb-drive-as-exfat">Erase the USB drive as exFAT<a class="heading-anchor" href="https://looselydecoupled.com/balena-etcher-stuck-starting-macos/#erase-the-usb-drive-as-exfat"
     aria-label="Link to section: Erase the USB drive as exFAT" data-pagefind-ignore></a>
</h2>
<p>The erase replaces the drive&rsquo;s partition map and every volume on it. Copy off
anything you need from the drive first.</p>
<h3 id="1-find-the-usb-drive">1. Find the USB drive<a class="heading-anchor" href="https://looselydecoupled.com/balena-etcher-stuck-starting-macos/#1-find-the-usb-drive"
     aria-label="Link to section: 1. Find the USB drive" data-pagefind-ignore></a>
</h3>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">diskutil</span> <span class="console__token">list</span></span></code></pre>
    </div>
  </div></div><p>Find the header that reads <code>external, physical</code> and matches your drive&rsquo;s size.
Its number replaces <code>N</code> in the next step.
An APFS drive also appears a second time, as a <code>synthesized</code> container. Use
the <code>external, physical</code> identifier, not the container&rsquo;s.</p>
<h3 id="2-erase-it">2. Erase it<a class="heading-anchor" href="https://looselydecoupled.com/balena-etcher-stuck-starting-macos/#2-erase-it"
     aria-label="Link to section: 2. Erase it" data-pagefind-ignore></a>
</h3>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">diskutil</span> <span class="console__token">unmountDisk</span> <span class="console__token">force</span> <span class="console__token">/dev/diskN</span>
</span><span class="line"><span class="prompt">$ </span><span class="console__token">diskutil</span> <span class="console__token">eraseDisk</span> <span class="console__token">ExFAT</span> <span class="console__token">USB</span> <span class="console__token">GPT</span> <span class="console__token">/dev/diskN</span></span></code></pre>
    </div>
  </div></div><p><code>force</code> matters when Spotlight is indexing the drive. Without it, the erase
can fail with <code>Couldn't unmount disk. : (-69888)</code> and name the process <code>mds</code>.
<code>USB</code> is the new volume name; any name works, because the flash overwrites it.</p>
<h3 id="3-flash-again">3. Flash again<a class="heading-anchor" href="https://looselydecoupled.com/balena-etcher-stuck-starting-macos/#3-flash-again"
     aria-label="Link to section: 3. Flash again" data-pagefind-ignore></a>
</h3>
<p>Select the ISO and the drive in Etcher, and click Flash. Within a few seconds,
the window shows <code>Flashing...</code> with a percentage, and the DevTools log shows
<code>Flashing 1 device</code> lines.</p>
<!-- style-check: end procedure -->
<h2 id="write-the-iso-with-dd-instead">Write the ISO with dd instead<a class="heading-anchor" href="https://looselydecoupled.com/balena-etcher-stuck-starting-macos/#write-the-iso-with-dd-instead"
     aria-label="Link to section: Write the ISO with dd instead" data-pagefind-ignore></a>
</h2>
<p>If Etcher still stops after the erase, <code>dd</code> writes the image without Etcher&rsquo;s
helper. That&rsquo;s how the Mint drive in this article was written. The steps, the
checks that keep <code>dd</code> off the wrong disk, and how to confirm the write are in
<a href="https://looselydecoupled.com/bootable-linux-usb-macos-dd/">Create a bootable Linux USB drive on macOS with dd</a>.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>macOS</dt> <dd>Tahoe 26.3.1, MacBook Pro (Apple Silicon)</dd></div>
    <div class="tested__row"><dt>USB</dt> <dd>PNY 32 GB USB 3.0</dd></div>
    <div class="tested__row"><dt>ISO</dt> <dd>Linux Mint 22.3 Cinnamon 64-bit</dd></div>
    <div class="tested__row"><dt>Reproduction</dt> <dd>balenaEtcher 2.1.6, CHIPSBNK 1 GB USB 2.0, Alpine 3.24.2 x86_64</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>March 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
