<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Youfu Hsu</title>
    <description>The latest articles on DEV Community by Youfu Hsu (@youfuhsu).</description>
    <link>https://dev.to/youfuhsu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4111810%2F08dd26e1-4ba0-4ac7-8fd8-535e566479f1.png</url>
      <title>DEV Community: Youfu Hsu</title>
      <link>https://dev.to/youfuhsu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/youfuhsu"/>
    <language>en</language>
    <item>
      <title>Exit code 0 is a lie: 7 ways my unattended automation silently did nothing</title>
      <dc:creator>Youfu Hsu</dc:creator>
      <pubDate>Sun, 06 Sep 2026 03:44:16 +0000</pubDate>
      <link>https://dev.to/youfuhsu/exit-code-0-is-a-lie-7-ways-my-unattended-automation-silently-did-nothing-501j</link>
      <guid>https://dev.to/youfuhsu/exit-code-0-is-a-lie-7-ways-my-unattended-automation-silently-did-nothing-501j</guid>
      <description>&lt;p&gt;I run about thirty scheduled jobs on a single Windows box. Some are scrapers, some generate content, some are trading bots, some just check that the other jobs are alive. Most of them were written and are maintained by an AI coding agent that I let run unattended.&lt;/p&gt;

&lt;p&gt;Over three months, every one of the failures below reported &lt;strong&gt;success&lt;/strong&gt;. The scheduler said &lt;code&gt;LastTaskResult = 0&lt;/code&gt;. The logs looked fine or didn't exist. And nothing had happened.&lt;/p&gt;

&lt;p&gt;If you only take one thing from this post: &lt;strong&gt;stop checking exit codes, start checking artifacts.&lt;/strong&gt; I'll get to why at the end. First, the seven ways I got lied to.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The wrapper that always returns 0
&lt;/h2&gt;

&lt;p&gt;To stop console windows flashing on my desktop every few minutes, I wrapped each scheduled task in a tiny VBScript launcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vb"&gt;&lt;code&gt;&lt;span class="k"&gt;Set&lt;/span&gt; &lt;span class="n"&gt;WshShell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CreateObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WScript.Shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;WshShell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="s"&gt;"cmd /c &lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt;python job.py &amp;gt;&amp;gt; job.log 2&amp;gt;&amp;amp;1&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0&lt;/code&gt; hides the window. &lt;code&gt;True&lt;/code&gt; waits for completion. I assumed &lt;code&gt;True&lt;/code&gt; also meant the exit code came back. &lt;strong&gt;It does not.&lt;/strong&gt; &lt;code&gt;WshShell.Run&lt;/code&gt; used as a &lt;em&gt;statement&lt;/em&gt; discards the return value, so &lt;code&gt;wscript.exe&lt;/code&gt; exits 0 no matter what the child did.&lt;/p&gt;

&lt;p&gt;I found this because a content pipeline had been dead for five days while the scheduler reported green every single day.&lt;/p&gt;

&lt;p&gt;The fix is to call &lt;code&gt;Run&lt;/code&gt; as a &lt;em&gt;function&lt;/em&gt; and pass the value out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vb"&gt;&lt;code&gt;&lt;span class="k"&gt;Set&lt;/span&gt; &lt;span class="n"&gt;WshShell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CreateObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WScript.Shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WshShell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cmd /c &lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt;python job.py &amp;gt;&amp;gt; job.log 2&amp;gt;&amp;amp;1&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;WScript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the parentheses — required when you're taking a return value. After fixing this across&lt;br&gt;
17 launchers, one task showed a non-zero result &lt;strong&gt;for the first time in its life&lt;/strong&gt;. It had&lt;br&gt;
been failing for weeks.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. The last line of your batch file overwrites the exit code
&lt;/h2&gt;

&lt;p&gt;Fixed the launcher, still got false greens. The next layer down was a &lt;code&gt;.cmd&lt;/code&gt; shim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;node&lt;/span&gt; &lt;span class="kd"&gt;pipeline&lt;/span&gt;.js &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;run&lt;/span&gt;.log &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kd"&gt;done&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt; &lt;span class="kd"&gt;code&lt;/span&gt; &lt;span class="nv"&gt;%errorlevel%&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;run&lt;/span&gt;.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;echo&lt;/code&gt; is the last command, &lt;code&gt;echo&lt;/code&gt; always succeeds, so the batch file returns &lt;strong&gt;its&lt;/strong&gt; exit code — zero — regardless of what &lt;code&gt;node&lt;/code&gt; did. The log even contained the correct non-zero errorlevel. It just never made it out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;node&lt;/span&gt; &lt;span class="kd"&gt;pipeline&lt;/span&gt;.js &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;run&lt;/span&gt;.log &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="kd"&gt;NODE_EXIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;%errorlevel%&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kd"&gt;done&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt; &lt;span class="kd"&gt;code&lt;/span&gt; &lt;span class="nv"&gt;%NODE_EXIT%&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;run&lt;/span&gt;.log
&lt;span class="k"&gt;exit&lt;/span&gt; &lt;span class="na"&gt;/b &lt;/span&gt;&lt;span class="nv"&gt;%NODE_EXIT%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Debugging heuristic:&lt;/strong&gt; when you fix one layer and still get false greens, assume there's&lt;br&gt;
another layer. Mine was three deep: scheduler → vbs → cmd → python.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. No console, no stdout, no error, no service
&lt;/h2&gt;

&lt;p&gt;A Flask service was set to start at login through a VBS launcher using &lt;code&gt;pythonw.exe&lt;/code&gt; (the GUI-subsystem Python with no console). The script began with a routine encoding guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconfigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;code&gt;pythonw&lt;/code&gt;, &lt;code&gt;sys.stdout&lt;/code&gt; isn't a usable stream. That line raised, uncaught, before the server ever bound its port. No console existed to print the traceback to, so there was no error anywhere. The visible symptom was "the port isn't open after reboot," which sends you straight to firewall and networking — the wrong place entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; don't use &lt;code&gt;pythonw&lt;/code&gt;. Use regular &lt;code&gt;python.exe&lt;/code&gt; with output redirected to a file, so&lt;br&gt;
stdout is a real stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vb"&gt;&lt;code&gt;&lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WshShell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cmd /c &lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt;python.exe app.py &amp;gt;&amp;gt; app.log 2&amp;gt;&amp;amp;1&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Heuristic:&lt;/strong&gt; if a background service won't come up, check &lt;code&gt;tasklist&lt;/code&gt; for the process&lt;br&gt;
&lt;em&gt;first&lt;/em&gt;. If it isn't there, the program died on startup — it's not a network problem.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Two scheduler defaults that quietly kill laptop jobs
&lt;/h2&gt;

&lt;p&gt;Windows Task Scheduler ships with defaults that are reasonable for a desktop and lethal on a laptop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;What it does to you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DisallowStartIfOnBatteries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;true&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Job doesn't run at all when unplugged. No error.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StopIfGoingOnBatteries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;true&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Job dies mid-run if you unplug.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StopOnIdleEnd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;true&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Job is &lt;strong&gt;killed when you touch the machine&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StartWhenAvailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;A missed run is skipped, not retried&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That third row cost me the most. A job would start on schedule, I'd sit down and move the mouse, idle state would end, and the scheduler would terminate the task mid-flight. The result code was "terminated," which is easy to misread as a crash in your own code.&lt;/p&gt;

&lt;p&gt;Every new task I create now gets all four flipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-ScheduledTaskSettingsSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AllowStartIfOnBatteries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;-DontStopIfGoingOnBatteries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-StartWhenAvailable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MultipleInstances&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IgnoreNew&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Set-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Settings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IdleSettings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopOnIdleEnd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Set-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Settings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Settings&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;StopOnIdleEnd&lt;/code&gt; isn't exposed on &lt;code&gt;New-ScheduledTaskSettingsSet&lt;/code&gt;, which is why it needs the second, uglier step — and why it's the one people miss.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. A comment warning about a trap became the trap
&lt;/h2&gt;

&lt;p&gt;This one is my favourite, because it's so stupid.&lt;/p&gt;

&lt;p&gt;A batch file had a comment reminding future-me about a previous bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="c"&gt;rem 坑備忘：exit /b %errorlevel% 必留&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(It's in Chinese — "trap memo: &lt;code&gt;exit /b %errorlevel%&lt;/code&gt; must stay" — which matters, because that's what makes it multi-byte.)&lt;/p&gt;

&lt;p&gt;The file is UTF-8. &lt;code&gt;cmd.exe&lt;/code&gt; decodes batch files using the system codepage, which on this machine is CP950, not UTF-8. Multi-byte characters get re-paired at the wrong boundaries, the comment gets truncated partway through, and &lt;strong&gt;the remainder of the line is executed as a command.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every morning at 08:30 a console popped up: &lt;code&gt;'errorlevel' is not recognized as an internal or external command&lt;/code&gt;. The pipeline itself succeeded. The exit code was 0. The only symptom was a comment's corpse trying to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule I follow now:&lt;/strong&gt; comments in &lt;code&gt;.cmd&lt;/code&gt;/&lt;code&gt;.bat&lt;/code&gt; files that run unattended are ASCII-only.&lt;br&gt;
If you truly need non-ASCII, &lt;code&gt;chcp 65001 &amp;gt;nul&lt;/code&gt; must be the &lt;em&gt;first&lt;/em&gt; line — it can't rescue anything above itself.&lt;/p&gt;

&lt;p&gt;The same class of bug bites &lt;code&gt;.vbs&lt;/code&gt; (WSH reads it as ANSI — save as UTF-16 LE if it contains non-ASCII) and &lt;code&gt;.ps1&lt;/code&gt; (non-ASCII comments can desync the parser so badly that source lines end up inside string variables).&lt;/p&gt;


&lt;h2&gt;
  
  
  6. Your OS silently blocks the CLI tool you pip-installed
&lt;/h2&gt;

&lt;p&gt;A publishing job died on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OSError: [WinError 4551] This file is blocked by application control policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was calling &lt;code&gt;yt-dlp&lt;/code&gt; via &lt;code&gt;subprocess&lt;/code&gt;. Windows Smart App Control had decided that this particular unsigned, freshly-updated standalone &lt;code&gt;.exe&lt;/code&gt; had no reputation yet and blocked it. The queue silently backed up for a day.&lt;/p&gt;

&lt;p&gt;The fix is not to weaken your security policy. It's to stop invoking the standalone executable at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yt_dlp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...])&lt;/span&gt;   &lt;span class="c1"&gt;# goes through trusted python.exe
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most pip-installed CLI tools support &lt;code&gt;-m&lt;/code&gt; invocation. Prefer it in anything unattended.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The audit script you don't have
&lt;/h2&gt;

&lt;p&gt;Every failure above shares a property: &lt;strong&gt;the status channel said one thing and reality said another.&lt;/strong&gt; So I stopped trusting the status channel.&lt;/p&gt;

&lt;p&gt;I now run two scripts, and they answer deliberately different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Did it run, and what happened?&lt;/strong&gt; Enumerate every scheduled task, resolve where its log
actually goes (mine hide behind &lt;code&gt;set LOG=&lt;/code&gt; variables, nested vbs→cmd calls, and relative redirects), and read the tail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did anything come out?&lt;/strong&gt; For each job, define the artifact it's supposed to produce and
the maximum age that's acceptable. Anything staler than that is flagged, regardless of what the exit code says.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second one is the important one. It's how I found out a video pipeline had been running "successfully" every day for weeks while producing zero files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CHECKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# (name, artifact glob, max age in hours)
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily-report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\...\memory\daily_*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product-radar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\...\data\radar_*.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video-pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\...\videos\*.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen lines of config, and it catches an entire class of failure that no amount of exit-code checking will.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Six of these seven are the same bug wearing different clothes: &lt;strong&gt;a layer between you and the work reported on itself instead of on the work.&lt;/strong&gt; The wrapper reported on the wrapper. The batch file reported on its last &lt;code&gt;echo&lt;/code&gt;. The scheduler reported that it launched something, not that the something did anything.&lt;/p&gt;

&lt;p&gt;So the habit that actually protects you isn't better error handling. It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Define, in advance, the observable thing that proves the job did its work.&lt;br&gt;
Then check for &lt;em&gt;that&lt;/em&gt;, on a schedule, and alert when it goes stale.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Exit codes tell you a process ended. Artifacts tell you a job happened. Only one of those is what you actually care about at 3am when you're asleep and the machine is working.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I package the full version of this — 22 documented traps, the two audit scripts, wrapper&lt;br&gt;
templates, and the memory system that stops an AI agent re-learning all of it every session — as the &lt;a href="https://alphatech4.gumroad.com/l/claude-code-automation-playbook" rel="noopener noreferrer"&gt;Claude Code Automation Playbook&lt;/a&gt;. But everything above is standalone; take it and go fix your own false greens.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>sre</category>
      <category>devops</category>
      <category>python</category>
    </item>
  </channel>
</rss>
