<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Shell on Lessons learned</title><link>https://eghuro.cz/tags/shell/</link><description>Recent content in Shell on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Sat, 31 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://eghuro.cz/tags/shell/index.xml" rel="self" type="application/rss+xml"/><item><title>Xcode Archive can ship the wrong display name despite correct source settings</title><link>https://eghuro.cz/posts/xcode-archive-processed-plist-identity/</link><pubDate>Thu, 29 Oct 2026 14:22:00 +0100</pubDate><guid>https://eghuro.cz/posts/xcode-archive-processed-plist-identity/</guid><description>&lt;p&gt;A production iOS archive had the production bundle ID and backend environment, but its &lt;code&gt;CFBundleDisplayName&lt;/code&gt; was the beta name. The tagged source said otherwise, and App Store Connect never shows that name, so only an installed build revealed it. Add a Run Script phase that checks the processed app &lt;code&gt;Info.plist&lt;/code&gt;, so a manual Product &amp;gt; Archive also rejects the wrong identity.&lt;/p&gt;&#10;&lt;p&gt;Using synthetic names, the mismatch was:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CFBundleIdentifier = com.example.notebook&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CFBundleDisplayName = NotebookBeta&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;h2 id="source-checks-missed-the-artifact"&gt;Source checks missed the artifact &lt;/h2&gt;&#10;&lt;p&gt;The investigation initially ruled out the mismatch from project settings and runtime configuration checks. That conclusion was too strong. The runtime check validated the product variant against the backend environment; it did not validate the display name. A production bundle with a beta display name could satisfy it.&lt;/p&gt;&#10;&lt;p&gt;Reading the archived app&amp;rsquo;s plist settled the question. Its name disagreed with the committed production settings. An uncommitted local edit was the likely explanation, but the archive alone could not establish exactly how the value changed.&lt;/p&gt;&#10;&lt;p&gt;The release flow had no step that showed the name either. App Store Connect metadata covers the store listing&amp;rsquo;s app name, not the bundle&amp;rsquo;s &lt;code&gt;CFBundleDisplayName&lt;/code&gt;, which only appears under the home screen icon. The only check is to install the production build from TestFlight, or from the App Store after release, and read that label by hand. It is easy to overlook when the bundle ID and backend already look right.&lt;/p&gt;&#10;&lt;p&gt;Source-level regression tests still help, but manual Archive does not run the scheme&amp;rsquo;s Test action. A check confined to a deployment lane also misses someone archiving directly in Xcode.&lt;/p&gt;&#10;&#10;&lt;h2 id="check-the-processed-plist-during-the-build"&gt;Check the processed plist during the build &lt;/h2&gt;&#10;&lt;p&gt;Add a Run Script phase named &lt;code&gt;Verify App Identity&lt;/code&gt; to the app target, after its existing phases. Use &lt;code&gt;/bin/sh&lt;/code&gt;. Leave &lt;strong&gt;Run script only when installing&lt;/strong&gt; unchecked.&lt;/p&gt;&#10;&lt;p&gt;Declare this Input File:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Declare this Output File:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$(DERIVED_FILE_DIR)/verified-app-identity.stamp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Uncheck &lt;strong&gt;Based on dependency analysis&lt;/strong&gt; for this small guard. It should validate every build, including repeated archives. The input declaration still establishes the dependency on plist processing; visual phase order alone is insufficient. &lt;a&#10; href="https://developer.apple.com/documentation/xcode/running-custom-scripts-during-a-build"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Apple&amp;rsquo;s Run Script documentation&lt;/a&gt;&#10; explains how declared inputs and outputs determine task ordering.&lt;/p&gt;&#10;&lt;p&gt;Paste this script, adapting the explicit configuration allowlist:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;: &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCRIPT_OUTPUT_FILE_0&lt;/span&gt;&lt;span class="p"&gt;:?Declare the stamp Output File&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPT_OUTPUT_FILE_0&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stamp&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;: &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET_BUILD_DIR&lt;/span&gt;&lt;span class="p"&gt;:?&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFOPLIST_PATH&lt;/span&gt;&lt;span class="p"&gt;:?&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIGURATION&lt;/span&gt;&lt;span class="p"&gt;:?&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;plist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BUILD_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$INFOPLIST_PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fail&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;error: %s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$CONFIGURATION&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; in&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Release&lt;span class="o"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;expected_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;com.example.notebook&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;expected_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Notebook&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Debug&lt;span class="o"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;expected_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;com.example.notebook.beta&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;expected_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NotebookBeta&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; *&lt;span class="o"&gt;)&lt;/span&gt; fail &lt;span class="s2"&gt;&amp;#34;No identity policy for configuration &lt;/span&gt;&lt;span class="nv"&gt;$CONFIGURATION&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;esac&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$plist&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;&amp;#34;Missing processed Info.plist: &lt;/span&gt;&lt;span class="nv"&gt;$plist&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;/usr/libexec/PlistBuddy -c &lt;span class="s1"&gt;&amp;#39;Print :CFBundleIdentifier&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$plist&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; fail &lt;span class="s1"&gt;&amp;#39;Cannot read CFBundleIdentifier&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;/usr/libexec/PlistBuddy -c &lt;span class="s1"&gt;&amp;#39;Print :CFBundleDisplayName&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$plist&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; fail &lt;span class="s1"&gt;&amp;#39;Cannot read CFBundleDisplayName&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$expected_id&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$expected_name&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; fail &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$CONFIGURATION&lt;/span&gt;&lt;span class="s2"&gt; requires &lt;/span&gt;&lt;span class="nv"&gt;$expected_id&lt;/span&gt;&lt;span class="s2"&gt; / &lt;/span&gt;&lt;span class="nv"&gt;$expected_name&lt;/span&gt;&lt;span class="s2"&gt;; got &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt; / &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Verified app identity: %s / %s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;: &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stamp&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Do not substitute the source &lt;code&gt;INFOPLIST_FILE&lt;/code&gt;: generated keys and build-setting expansion can change the packaged values. Keep the expected identity independent of the settings under test.&lt;/p&gt;&#10;&lt;p&gt;The declared paths also describe the script&amp;rsquo;s file access for user-script sandboxing. Keep sandboxing enabled where possible and test these declarations with your Xcode version. The original target had it disabled. The stamp lives outside the signed app and is created only after validation. Removing it first, failing on missing values, and running unconditionally prevent an old stamp from becoming a false success after a failed check.&lt;/p&gt;&#10;&#10;&lt;h2 id="prove-that-wrong-builds-fail"&gt;Prove that wrong builds fail &lt;/h2&gt;&#10;&lt;p&gt;The historical implementation passed normal builds across all four original configurations. Four deliberate mutations failed: the production ID, production name, beta ID, and beta name, changed separately. Each failure reported the mismatched identity; the project file was restored afterward. These were build checks, not evidence of a fresh Archive after the fix.&lt;/p&gt;&#10;&lt;p&gt;For your project, repeat those negative controls, then restore the settings and archive twice without cleaning. Confirm the guard runs both times and inspect the resulting archive&amp;rsquo;s app plist. Also test a missing key and an unrecognized configuration.&lt;/p&gt;&#10;&lt;p&gt;This checks the app&amp;rsquo;s base plist only. Localized &lt;code&gt;InfoPlist.strings&lt;/code&gt; can affect the displayed name, and extensions need separate policies. Check the Archive action&amp;rsquo;s selected configuration in your scheme, then deliberately break its display name and confirm Archive fails before trusting the guard.&lt;/p&gt;&#10;</description></item><item><title>rsync code 23: atomic archives can still form an incomplete release</title><link>https://eghuro.cz/posts/rsync-atomic-archives-incomplete-release/</link><pubDate>Thu, 22 Oct 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/rsync-atomic-archives-incomplete-release/</guid><description>&lt;p&gt;A dataset transfer ended with:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1327) [sender=3.2.5]&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The packing job published each finished archive by rename, so final filenames never exposed an archive still being written. The transfer nevertheless began before the collection was complete. Repeating it after packing finished, then checking destination hashes, recovered the release.&lt;/p&gt;&#10;&lt;p&gt;This happened in the &lt;a&#10; href="https://eghuro.cz/posts/claude-code-ssh-openpbs-setup"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;Claude Code over SSH and OpenPBS workflow&lt;/a&gt;&#10;: a PBS job produced the archives, while separate remote commands and a human-operated transfer consumed them.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-filename-was-safe-before-the-collection-was-ready"&gt;The filename was safe before the collection was ready &lt;/h2&gt;&#10;&lt;p&gt;The packer used this pattern, with paths generalized:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -czf groups_00.tar.gz.part groups/part_00 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; mv groups_00.tar.gz.part groups_00.tar.gz&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A reader looking for &lt;code&gt;*.tar.gz&lt;/code&gt; would only see that archive after its successful creation and rename on the same filesystem. But the producer repeated the operation for several archives, then wrote sidecar files. Nothing prevented a consumer from enumerating the directory between those steps.&lt;/p&gt;&#10;&lt;p&gt;Transfer instructions went out while the packer was running. The user&amp;rsquo;s pasted output ended with &lt;code&gt;to-chk=0/16&lt;/code&gt;—the intended release contained seventeen archives plus a checksum manifest and a guide. A later source listing showed the complete set.&lt;/p&gt;&#10;&lt;p&gt;The count, transferred size, and packing timeline supported the explanation that the last archive and sidecars didn&amp;rsquo;t exist when the transfer enumerated its inputs. The earlier detailed rsync errors were not included, so this remained a supported diagnosis rather than a direct readback of each missing-path error.&lt;/p&gt;&#10;&lt;p&gt;Code 23 alone cannot identify that cause. The &lt;a&#10; href="https://download.samba.org/pub/rsync/rsync.1"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;rsync manual&lt;/a&gt;&#10; defines it as a partial transfer due to error; inspect the preceding errors before retrying mindlessly.&lt;/p&gt;&#10;&#10;&lt;h2 id="repeat-the-transfer-after-checking-the-producer"&gt;Repeat the transfer after checking the producer &lt;/h2&gt;&#10;&lt;p&gt;The recovery checked that packing had finished, repeated the transfer, and verified the archives at the destination. With server names and filenames generalized, run from the producer&amp;rsquo;s archive directory:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -rtv --partial --progress &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; SHA256SUMS guide.txt groups_*.tar.gz metadata.tar.gz &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; researcher@192.168.0.3:incoming/release-01/&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, on the destination:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/incoming/release-01 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum -c SHA256SUMS&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The user supplied seventeen &lt;code&gt;OK&lt;/code&gt; results. Those checks verified the listed archive bytes, including the archives transferred earlier. They did not verify recipient permissions or the guide&amp;rsquo;s contents. A separate archive-content check also lacked &lt;code&gt;bc&lt;/code&gt;, so its aggregate group-count calculation did not succeed; that check must not be presented as another passing validation.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;--partial&lt;/code&gt; retains partial transfers. It does not by itself promise efficient offset-based resumption under every transfer configuration.&lt;/p&gt;&#10;&#10;&lt;h2 id="add-a-release-complete-barrier"&gt;Add a release-complete barrier &lt;/h2&gt;&#10;&lt;p&gt;We did not implement a stronger publication protocol during this recovery. For a future run, use a fresh release directory that consumers cannot mistake for an earlier finished release. Publish a completion marker only after validating the expected archive set and sidecars. Consumers should require that marker before enumeration and check hashes after transfer.&lt;/p&gt;&#10;&lt;p&gt;A marker needs an immutable release behind it. Reusing a directory with an old marker can announce incomplete new contents as ready; writing the marker early merely moves the race.&lt;/p&gt;&#10;&lt;p&gt;Before sending a transfer command, check the producer&amp;rsquo;s completion evidence and the expected file set. Per-file renames protect readers of each file, but consumers still need a defined point at which the collection is ready.&lt;/p&gt;&#10;</description></item><item><title>"Ansible requires blocking IO on stdin/stdout/stderr" when an AI agent runs Ansible</title><link>https://eghuro.cz/posts/ansible-requires-blocking-io-agent-shell/</link><pubDate>Tue, 13 Oct 2026 22:34:00 +0200</pubDate><guid>https://eghuro.cz/posts/ansible-requires-blocking-io-agent-shell/</guid><description>&lt;p&gt;The first time my coding agent ran Ansible, every command failed before Ansible had read a single file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR: Ansible requires blocking IO on stdin/stdout/stderr. Non-blocking file handles detected: &amp;lt;stdout&amp;gt;, &amp;lt;stderr&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--syntax-check&lt;/code&gt; too, and even &lt;code&gt;ansible --version&lt;/code&gt;. If you see this from an agent, a CI job, or a wrapper script, Ansible isn&amp;rsquo;t broken. The process that started it handed it non-blocking file handles, and a few shell redirections replace them.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;ansible-core 2.20 (Ansible 13.3.0 from Homebrew) on macOS&lt;/li&gt;&#10;&lt;li&gt;A coding agent (Claude Code in VS Code) that runs commands in its own shell and captures their output&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The check has been in every Ansible CLI entry point since ansible-core 2.14 (2022).&lt;/p&gt;&#10;&#10;&lt;h2 id="what-went-wrong"&gt;What went wrong &lt;/h2&gt;&#10;&lt;p&gt;O_NONBLOCK is a flag on the open file, not on the process, so every child that inherits the file descriptor inherits the flag too. The agent&amp;rsquo;s harness connects the shell&amp;rsquo;s stdout and stderr to non-blocking pipes, and the shell passes them on unchanged to &lt;code&gt;ansible-playbook&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;At startup, Ansible calls &lt;code&gt;os.get_blocking()&lt;/code&gt; on each of the three handles and exits if any of them is non-blocking. The list at the end of the message names the handles that failed. That makes it a good diagnostic: the first failure in my session listed only &lt;code&gt;&amp;lt;stderr&amp;gt;&lt;/code&gt;, because the command was &lt;code&gt;ansible --version | head -3&lt;/code&gt;. The pipe to &lt;code&gt;head&lt;/code&gt; had already given stdout a fresh, blocking handle, while stderr still pointed at the agent&amp;rsquo;s pipe.&lt;/p&gt;&#10;&lt;p&gt;Run this in the shell you suspect to see which handles are affected:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -c &lt;span class="s1"&gt;&amp;#39;import os; print([os.get_blocking(fd) for fd in (0, 1, 2)])&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;False&lt;/code&gt; in any position means Ansible will refuse to start there.&lt;/p&gt;&#10;&#10;&lt;h2 id="fix"&gt;Fix &lt;/h2&gt;&#10;&lt;p&gt;Give Ansible three handles the shell creates itself:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ansible-playbook site.yml &amp;lt;/dev/null 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; cat&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;| cat&lt;/code&gt; gives stdout a new pipe, which is blocking.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; sends stderr into that same pipe. &lt;code&gt;| cat&lt;/code&gt; alone fixes only stdout.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;&amp;lt;/dev/null&lt;/code&gt; replaces stdin. My agent&amp;rsquo;s stdin was never flagged, but Ansible checks it too, and an agent can&amp;rsquo;t answer a prompt anyway.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The pipe has a side effect: the command&amp;rsquo;s exit status is now &lt;code&gt;cat&lt;/code&gt;&amp;rsquo;s, which is 0. A failed playbook looks like a success unless you turn on &lt;code&gt;pipefail&lt;/code&gt; (bash and zsh both have it):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -o pipefail&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ansible-playbook site.yml &amp;lt;/dev/null 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; cat&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To check the fix without touching your real shell, you can make all three handles non-blocking for a single command and try it:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;NB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;import os,sys; [os.set_blocking(fd, False) for fd in (0,1,2)]; os.execvp(sys.argv[1], sys.argv[1:])&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;python3 -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$NB&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; ansible --version&lt;span class="o"&gt;)&lt;/span&gt; 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &amp;lt;/dev/zero &lt;span class="p"&gt;|&lt;/span&gt; cat&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ERROR: ... Non-blocking file handles detected: &amp;lt;stdin&amp;gt;, &amp;lt;stdout&amp;gt;, &amp;lt;stderr&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;python3 -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$NB&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; sh -c &lt;span class="s1"&gt;&amp;#39;ansible --version &amp;lt;/dev/null 2&amp;gt;&amp;amp;1 | cat&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &amp;lt;/dev/zero &lt;span class="p"&gt;|&lt;/span&gt; cat&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ansible [core 2.20.1] ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The outer &lt;code&gt;| cat&lt;/code&gt; and &lt;code&gt;&amp;lt;/dev/zero&lt;/code&gt; keep the flag on throwaway handles, so your terminal stays blocking.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-ansible-insists"&gt;Why Ansible insists &lt;/h2&gt;&#10;&lt;p&gt;The &lt;a&#10; href="https://github.com/ansible/ansible/pull/77639"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;pull request that added the check&lt;/a&gt;&#10; explains it. On a non-blocking stdout, a large write can fail partway with &lt;code&gt;[Errno 11] write could not complete without blocking&lt;/code&gt;, and Ansible&amp;rsquo;s output gets cut off, or the run dies with an &amp;ldquo;Unexpected Exception&amp;rdquo;. The check turns those intermittent failures into one clear error at startup. The PR names another way to end up there: &lt;code&gt;ssh&lt;/code&gt; switches its handles to non-blocking while it runs and doesn&amp;rsquo;t always switch them back when it exits.&lt;/p&gt;&#10;&lt;p&gt;So the same error can turn up wherever a parent process hands down non-blocking pipes, CI runners included, or in a terminal after an &lt;code&gt;ssh&lt;/code&gt; that didn&amp;rsquo;t clean up.&lt;/p&gt;&#10;&#10;&lt;h2 id="make-the-agent-remember-it"&gt;Make the agent remember it &lt;/h2&gt;&#10;&lt;p&gt;The agent worked this out within two minutes, and later that night it wrote the fix into the project&amp;rsquo;s skill for provisioning VMs:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Run Ansible from Claude Code with &lt;span class="sb"&gt;`&amp;lt;/dev/null`&lt;/span&gt; and pipe its output through &lt;span class="sb"&gt;`cat`&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The next morning, a session about a different service ran &lt;code&gt;ansible-playbook --syntax-check&lt;/code&gt; without the redirections and hit the same error. It never loaded the VM skill, so it never saw the line. A rule about how to run a tool belongs in the file the agent reads in every session, not in the instructions for one task.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;&amp;ldquo;Ansible requires blocking IO&amp;rdquo; means the process that started Ansible handed it non-blocking file handles. &lt;code&gt;&amp;lt;/dev/null 2&amp;gt;&amp;amp;1 | cat&lt;/code&gt; gives it blocking ones, and &lt;code&gt;set -o pipefail&lt;/code&gt; keeps a failed run from looking like a success.&lt;/p&gt;&#10;&lt;p&gt;If your agent runs Ansible, put that line in the instructions file it reads every session (&lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt;).&lt;/p&gt;&#10;</description></item><item><title>A failed CUDA build over SSH still launched the old executable</title><link>https://eghuro.cz/posts/remote-failed-build-stale-executable/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/remote-failed-build-stale-executable/</guid><description>&lt;p&gt;A remote command returned a CUDA compilation error followed by a normal simulation startup banner:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;error: calling a __device__ function(&amp;#34;__sinf&amp;#34;) from a __host__ __device__ function(&amp;#34;inflowFluctuation&amp;#34;) is not allowed&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The simulation started, but it ran an older executable. Semicolons let the launch proceed after &lt;code&gt;nvcc&lt;/code&gt; failed. Correcting the source and rebuilding recovered the test; guarding the build-to-launch transition would prevent that misleading result.&lt;/p&gt;&#10;&lt;p&gt;The &lt;a&#10; href="https://eghuro.cz/posts/claude-code-ssh-openpbs-setup"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;Claude Code and OpenPBS setup&lt;/a&gt;&#10; explains the local-agent/remote-command boundary. This happened during an early SSH-driven test, before we moved the long-running work into scheduled jobs. The shell mistake also applies inside a PBS script.&lt;/p&gt;&#10;&#10;&lt;h2 id="diagnostics-did-not-control-execution"&gt;Diagnostics did not control execution &lt;/h2&gt;&#10;&lt;p&gt;The command had this structure, with project paths removed:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nvcc -o solver solver.cu &amp;gt; build.log 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -A3 error build.log &lt;span class="p"&gt;|&lt;/span&gt; head&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nohup ./solver &lt;span class="k"&gt;case&lt;/span&gt;.cfg &amp;gt; run.log 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &amp;lt; /dev/null &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The build wrote to an executable path that already existed. Compilation failed, leaving a runnable earlier build. Printing matching diagnostics didn&amp;rsquo;t stop the next command, and backgrounding the program didn&amp;rsquo;t establish whether the requested source had compiled.&lt;/p&gt;&#10;&lt;p&gt;The failed-build result included an ordinary banner. After replacing &lt;code&gt;__sinf&lt;/code&gt; with &lt;code&gt;sinf&lt;/code&gt; in the host/device function, the rebuilt program&amp;rsquo;s banner contained the newly added &lt;code&gt;inflowTurb=0.08&lt;/code&gt; parameter and subsequent progress lines. The earlier banner lacked that field. That difference provided evidence of which code path actually ran.&lt;/p&gt;&#10;&lt;p&gt;The transcript shows this operational recovery. Its retry still used semicolon-separated build and launch commands, so it did not repair the fail-open shell structure.&lt;/p&gt;&#10;&#10;&lt;h2 id="make-compiler-status-a-launch-prerequisite"&gt;Make compiler status a launch prerequisite &lt;/h2&gt;&#10;&lt;p&gt;For a future build, preserve the compiler&amp;rsquo;s status explicitly. This generalized Bash pattern builds to a temporary path and only publishes it after success:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="p"&gt;:?Set PROJECT_DIR to the remote project directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;candidate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp ./solver.build.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rm -f &amp;#34;$candidate&amp;#34;&amp;#39;&lt;/span&gt; EXIT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; nvcc -o &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; solver.cu &amp;gt; build.log 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; mv -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; ./solver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; cat build.log &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; ./solver &lt;span class="k"&gt;case&lt;/span&gt;.cfg&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is a recommended guard, not code installed during the incident. The named source and configuration must exist in the remote project. Use a single build owner: concurrent publishers to &lt;code&gt;./solver&lt;/code&gt; require a separate ownership or versioned-artifact design.&lt;/p&gt;&#10;&lt;p&gt;When running remotely, send the whole script through SSH so its directory selection and status checks happen in the same remote shell. Avoid embedding local path expansions accidentally. For batch work, run the payload in the allocated job rather than adding another unscheduled &lt;code&gt;nohup&lt;/code&gt; launch.&lt;/p&gt;&#10;&#10;&lt;h2 id="reproduce-the-shell-failure-without-cuda"&gt;Reproduce the shell failure without CUDA &lt;/h2&gt;&#10;&lt;p&gt;On a local machine with a POSIX shell, this harmless fixture leaves an existing command available after a simulated build failure:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh -c &lt;span class="s1"&gt;&amp;#39;solver() { printf &amp;#34;old executable ran\n&amp;#34;; }; false; solver&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It prints &lt;code&gt;old executable ran&lt;/code&gt; and exits successfully. The guarded form prevents execution:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh -c &lt;span class="s1"&gt;&amp;#39;solver() { printf &amp;#34;old executable ran\n&amp;#34;; }; false &amp;amp;&amp;amp; solver&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That form prints nothing and returns a failure status. For a real compiler test, deliberately introduce an error and verify that no new run starts. Also log a build identifier or other deliberate runtime identity; a healthy-looking startup line only proves that some executable ran.&lt;/p&gt;&#10;&lt;p&gt;Check your next remote build command for the exact status dependency between compilation and launch, especially where diagnostic pipelines sit between them.&lt;/p&gt;&#10;</description></item><item><title>A background agent Docker build: 'open Dockerfile: no such file or directory'</title><link>https://eghuro.cz/posts/background-terminal-jobs-do-not-inherit-cwd/</link><pubDate>Sun, 30 Aug 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/background-terminal-jobs-do-not-inherit-cwd/</guid><description>&lt;p&gt;A background Docker build failed immediately with:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The agent had supplied the project directory through the terminal tool&amp;rsquo;s &lt;code&gt;workdir&lt;/code&gt; argument. The retry put &lt;code&gt;cd&lt;/code&gt; inside the command itself. For an agent runner whose background path handling is uncertain, bind the directory in the command or make the Dockerfile and build context absolute.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-failed-call-already-supplied-workdir"&gt;The failed call already supplied workdir &lt;/h2&gt;&#10;&lt;p&gt;In the historical runner, the background invocation had this shape, with project and image names generalized:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;background&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;workdir&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/srv/app&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docker build --progress=plain -t local/app:dev -f Dockerfile .&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Docker could not open &lt;code&gt;Dockerfile&lt;/code&gt;. The retry started with an explicit &lt;code&gt;cd&lt;/code&gt; to the project before running the build. That corrected the launch form; whether the rest of that session succeeded is outside this lesson.&lt;/p&gt;&#10;&lt;p&gt;The error alone does not identify the actual launch directory. The record supports a runner-specific path-handling failure, not a rule that background processes generally discard their parent&amp;rsquo;s directory. Ordinary shell backgrounding with &lt;code&gt;&amp;amp;&lt;/code&gt; inherits the shell&amp;rsquo;s working directory. An agent tool can launch its jobs through a different execution path.&lt;/p&gt;&#10;&lt;p&gt;A later check on the runner used to prepare this post found that background jobs preserved the preceding foreground directory, and an explicit background &lt;code&gt;workdir&lt;/code&gt; was honored. The historical failure is therefore not a current universal behavior claim.&lt;/p&gt;&#10;&#10;&lt;h2 id="bind-both-docker-paths-deliberately"&gt;Bind both Docker paths deliberately &lt;/h2&gt;&#10;&lt;p&gt;For an existing project at &lt;code&gt;/srv/app&lt;/code&gt;, put the directory change and build in the same command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /srv/app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; docker build --progress&lt;span class="o"&gt;=&lt;/span&gt;plain -t local/app:dev -f Dockerfile .&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; prevents the build from running if the directory change fails. Alternatively, remove the directory dependency from both path arguments:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker build --progress&lt;span class="o"&gt;=&lt;/span&gt;plain -t local/app:dev &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -f /srv/app/Dockerfile /srv/app&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Making only &lt;code&gt;-f&lt;/code&gt; absolute still leaves &lt;code&gt;.&lt;/code&gt; as an accidental build context. Likewise, when using Compose, check its file selection and relative-path behavior rather than assuming a successful Docker build proves the next command has the right context.&lt;/p&gt;&#10;&#10;&lt;h2 id="probe-the-actual-execution-mode"&gt;Probe the actual execution mode &lt;/h2&gt;&#10;&lt;p&gt;Before a multi-minute job, use a harmless foreground command to change to a known scratch directory:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TMPDIR&lt;/span&gt;&lt;span class="p"&gt;:?Set TMPDIR to your scratch directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then submit &lt;code&gt;pwd&lt;/code&gt; as a separate background tool call without &lt;code&gt;workdir&lt;/code&gt;, and read that job&amp;rsquo;s captured output. Repeat with &lt;code&gt;workdir&lt;/code&gt; set to the project root. Those two checks distinguish remembered foreground state from an explicit launch directory without building an image or starting services.&lt;/p&gt;&#10;&lt;p&gt;For a real build, print &lt;code&gt;pwd&lt;/code&gt; and check &lt;code&gt;test -f Dockerfile&lt;/code&gt; inside the same command that launches it. A foreground check in an earlier call cannot establish the background job&amp;rsquo;s directory.&lt;/p&gt;&#10;&#10;&lt;h2 id="separate-service-startup-from-readiness-checks"&gt;Separate service startup from readiness checks &lt;/h2&gt;&#10;&lt;p&gt;The same historical session hit another runner boundary. A foreground command combining &lt;code&gt;docker compose up -d&lt;/code&gt; with a health-check loop was rejected with:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This foreground command appears to start a long-lived server/watch process.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The runner directed the agent to launch it with &lt;code&gt;background=true&lt;/code&gt;, verify readiness, and run tests separately. That refusal came from the runner, even though Compose&amp;rsquo;s &lt;code&gt;-d&lt;/code&gt; requests detached containers.&lt;/p&gt;&#10;&lt;p&gt;Keep service startup in its own call, following the runner&amp;rsquo;s execution rules. Check readiness in a separate bounded command, and fail explicitly if the deadline expires. Starting a service and proving it healthy are separate results.&lt;/p&gt;&#10;&lt;p&gt;Before your next background build, compare its reported directory with the intended build context using the harmless probe above.&lt;/p&gt;&#10;</description></item><item><title>Codex 401 in an agent shell: check credentials before blaming API key precedence</title><link>https://eghuro.cz/posts/codex-inherited-api-key/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/codex-inherited-api-key/</guid><description>&lt;p&gt;A regular terminal reported a ChatGPT login, but Codex requests inside an agent&amp;rsquo;s shell failed with:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP error: 401 Unauthorized&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An inherited &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; looked suspicious. The useful next step was a controlled credential-context comparison. The recorded investigation never proved that the key overrode the ChatGPT login, and it never showed a successful retry after removing it.&lt;/p&gt;&#10;&#10;&lt;h2 id="what-the-may-5-investigation-established"&gt;What the May 5 investigation established &lt;/h2&gt;&#10;&lt;p&gt;The installed CLI was &lt;code&gt;codex-cli 0.128.0&lt;/code&gt;. The user reported that &lt;code&gt;codex login status&lt;/code&gt; returned &lt;code&gt;Logged in using ChatGPT&lt;/code&gt; before launching the agent. Inside the agent, the same status command returned &lt;code&gt;Not logged in&lt;/code&gt;, and this request repeatedly failed:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;codex &lt;span class="nb"&gt;exec&lt;/span&gt; --skip-git-repo-check &lt;span class="s2"&gt;&amp;#34;echo hello&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An early environment check found &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; present. A separate request to the OpenAI models endpoint, explicitly using that variable, returned HTTP 401. That established that the credential failed that direct probe. It did not establish which credential Codex selected.&lt;/p&gt;&#10;&lt;p&gt;Later checks found the variable absent, yet Codex still failed. One retry explicitly supplied an empty &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; and also received 401 responses. The chain contains no controlled &lt;code&gt;env -u&lt;/code&gt; comparison that restored subscription access. The document-cleanup task continued with Python; Codex authentication was not shown to recover.&lt;/p&gt;&#10;&lt;p&gt;Calling this a confirmed case of an environment key shadowing OAuth would overstate the evidence. The login-status mismatch deserved investigation before declaring the subscription token expired or replacing it with an API key.&lt;/p&gt;&#10;&#10;&lt;h2 id="compare-the-launch-contexts-without-exposing-secrets"&gt;Compare the launch contexts without exposing secrets &lt;/h2&gt;&#10;&lt;p&gt;Run these checks locally in both the working terminal and the failing agent shell:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;command&lt;/span&gt; -v codex&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;codex --version&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;codex login status&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 - &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;PY&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;import os&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;for name in (&amp;#34;HOME&amp;#34;, &amp;#34;CODEX_HOME&amp;#34;, &amp;#34;OPENAI_API_KEY&amp;#34;, &amp;#34;CODEX_API_KEY&amp;#34;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; print(f&amp;#34;{name}: {&amp;#39;set&amp;#39; if name in os.environ else &amp;#39;unset&amp;#39;}&amp;#34;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;PY&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Keep login-status output private if it includes credential details. The Python check prints names and presence only. Do not dump the environment, search credential files, or paste tokens into a troubleshooting conversation.&lt;/p&gt;&#10;&lt;p&gt;Compare the actual &lt;code&gt;HOME&lt;/code&gt; and &lt;code&gt;CODEX_HOME&lt;/code&gt; paths privately, too. Presence alone cannot tell you whether two processes use the same directory. Check the executable and launch configuration for differences before changing authentication.&lt;/p&gt;&#10;&lt;p&gt;&lt;a&#10; href="https://developers.openai.com/codex/auth"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;OpenAI&amp;rsquo;s authentication documentation&lt;/a&gt;&#10; distinguishes ChatGPT subscription sign-in from API-key access. It also describes credential storage in a file under &lt;code&gt;CODEX_HOME&lt;/code&gt; or an OS credential store. A &lt;a&#10; href="https://eghuro.cz/posts/profile-home-tilde-paths"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;profile HOME mismatch&lt;/a&gt;&#10; or unavailable credential store is a separate explanation for a missing login.&lt;/p&gt;&#10;&lt;p&gt;To isolate the suspected variable on a Unix-like system, keep the same executable, working directory, &lt;code&gt;HOME&lt;/code&gt;, and &lt;code&gt;CODEX_HOME&lt;/code&gt;, then compare:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;codex login status&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;env -u OPENAI_API_KEY codex login status&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Removing one variable affects only the second process. It does not delete the saved login or change the parent shell. If you intend to make a model request, repeat the same minimal prompt with and without that variable and compare completion and exit status. A status check alone does not prove a request will succeed. This paired request test was not completed in the May investigation.&lt;/p&gt;&#10;&#10;&lt;h2 id="verification-on-october-31-avoid-a-universal-precedence-rule"&gt;Verification on October 31: avoid a universal precedence rule &lt;/h2&gt;&#10;&lt;p&gt;A separate check on 2026-10-31 used installed Codex &lt;code&gt;0.166.0&lt;/code&gt; with isolated, empty &lt;code&gt;HOME&lt;/code&gt; and &lt;code&gt;CODEX_HOME&lt;/code&gt; directories. &lt;code&gt;codex login status&lt;/code&gt; returned &lt;code&gt;Not logged in&lt;/code&gt; both without a key and with a dummy &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;. No model request or real credential was used. This checks status behavior without a saved login; it does not test precedence over OAuth.&lt;/p&gt;&#10;&lt;p&gt;The &lt;a&#10; href="https://github.com/openai/codex/blob/67f0994b532b8740f14b5b738af7b7b2a915b1d6/codex-rs/login/src/auth/manager.rs#L1571-L1593"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;upstream authentication loader&lt;/a&gt;&#10;, inspected that day, has an environment-key precedence branch for &lt;code&gt;CODEX_API_KEY&lt;/code&gt;, conditional on that mechanism being enabled and API-key authentication being allowed. That is not a universal rule about &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, nor proof of what version &lt;code&gt;0.128.0&lt;/code&gt; selected in May.&lt;/p&gt;&#10;&lt;p&gt;The earlier trusted-directory error was separate: &lt;code&gt;--skip-git-repo-check&lt;/code&gt; bypassed that check and exposed the authentication failure. Initializing unrelated directories as Git repositories would not repair credentials.&lt;/p&gt;&#10;&lt;p&gt;Before logging out or deleting a working login, compare the credential context of your two launch paths and change one variable at a time.&lt;/p&gt;&#10;</description></item><item><title>Tilde pointed at a profile home, so the export landed elsewhere</title><link>https://eghuro.cz/posts/profile-home-tilde-paths/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/profile-home-tilde-paths/</guid><description>&lt;p&gt;The export command finished, but the expected directory was empty. Then Python reported &lt;code&gt;[Errno 2] No such file or directory&lt;/code&gt; for a script that existed in the user&amp;rsquo;s home. The runner&amp;rsquo;s &lt;code&gt;HOME&lt;/code&gt; pointed at a profile-specific directory. Fixing this required explicit paths for both the export destination and the Python environment.&lt;/p&gt;&#10;&#10;&lt;h2 id="two-homes-two-plausible-directory-trees"&gt;Two homes, two plausible directory trees &lt;/h2&gt;&#10;&lt;p&gt;An initial attempt to read a helper resolved to a profile-home path where the file did not exist. During the later export, &lt;code&gt;cd ~/transfer&lt;/code&gt; succeeded because that directory existed too. A virtual environment had been created there, but the actual transfer script lived under the user&amp;rsquo;s expected home.&lt;/p&gt;&#10;&lt;p&gt;The terminal output settled the discrepancy: printing &lt;code&gt;HOME&lt;/code&gt; returned the profile-specific home. Comparing the two transfer directories found the script only under the expected user-home root. Comparing output directories found exported data under that root and an empty directory under the profile home. The report also existed only under the expected root.&lt;/p&gt;&#10;&lt;p&gt;Changing directories alone would still leave Python&amp;rsquo;s &lt;code&gt;Path(&amp;quot;~/data&amp;quot;).expanduser()&lt;/code&gt; following the process&amp;rsquo;s &lt;code&gt;HOME&lt;/code&gt;. The repair replaced tilde-based output defaults with explicit absolute destinations, including separate report and log paths. The script supported overrides such as &lt;code&gt;KB_OUT_ROOT&lt;/code&gt;. Running it from the intended transfer directory also selected that directory&amp;rsquo;s &lt;code&gt;.venv/bin/python&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;This was observed in a historical Hermes runner environment. It does not establish that every Hermes profile changes &lt;code&gt;HOME&lt;/code&gt;; consult the &lt;a&#10; href="https://hermes-agent.nousresearch.com/docs"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;current Hermes documentation&lt;/a&gt;&#10; for your setup. A configured profile directory and the home directory you expect are separate facts to check.&lt;/p&gt;&#10;&#10;&lt;h2 id="check-the-process-before-searching-for-files"&gt;Check the process before searching for files &lt;/h2&gt;&#10;&lt;p&gt;Run these inside the same terminal tool or wrapper that launches the job:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;HOME=%s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -c &lt;span class="s1"&gt;&amp;#39;import sys; from pathlib import Path; print(sys.executable); print(Path.home())&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Compare those paths with the expected project root. Limit directory checks to those known roots; a filesystem-wide search adds noise and can expose unrelated private files. A successful &lt;code&gt;cd&lt;/code&gt; proves only that a directory exists, not that it contains the intended script or environment.&lt;/p&gt;&#10;&#10;&lt;h2 id="shell-expansion-happens-before-the-child-starts"&gt;Shell expansion happens before the child starts &lt;/h2&gt;&#10;&lt;p&gt;In Bash, unquoted &lt;code&gt;~/data&lt;/code&gt; expands in the calling shell. In &lt;code&gt;env HOME=/somewhere python3 ... ~/data&lt;/code&gt;, the argument has already expanded before &lt;code&gt;env&lt;/code&gt; changes the child&amp;rsquo;s environment. Single-quoted &lt;code&gt;'~/data'&lt;/code&gt; stays literal; Python must expand it explicitly. On Unix, &lt;code&gt;Path.expanduser()&lt;/code&gt; uses &lt;code&gt;HOME&lt;/code&gt; when it is set.&lt;/p&gt;&#10;&lt;p&gt;This isolated example demonstrates both cases without touching real home directories:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SCRATCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TMPDIR&lt;/span&gt;&lt;span class="p"&gt;:?Set TMPDIR to your scratch directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/home-path-demo&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/caller-home&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/profile-home&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PYTHON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; -v python3&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; SCRATCH PYTHON&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/caller-home&amp;#34;&lt;/span&gt; bash --noprofile --norc &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;SH&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;env HOME=&amp;#34;$SCRATCH/profile-home&amp;#34; &amp;#34;$PYTHON&amp;#34; -c \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#39;import sys; print(sys.argv[1])&amp;#39; ~/data&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;env HOME=&amp;#34;$SCRATCH/profile-home&amp;#34; &amp;#34;$PYTHON&amp;#34; -c \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#39;import sys; from pathlib import Path; print(sys.argv[1]); print(Path(sys.argv[1]).expanduser())&amp;#39; &amp;#39;~/data&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SH&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first command prints the caller&amp;rsquo;s home path. The second prints the literal &lt;code&gt;~/data&lt;/code&gt;, then the profile-home path.&lt;/p&gt;&#10;&#10;&lt;h2 id="pin-the-interpreter-and-verify-the-artifact"&gt;Pin the interpreter and verify the artifact &lt;/h2&gt;&#10;&lt;p&gt;Here is a standalone local inventory example. The program reads its input directory as read-only; the output root is explicit. It requires Python&amp;rsquo;s &lt;code&gt;venv&lt;/code&gt; module and uses only scratch fixtures.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SCRATCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TMPDIR&lt;/span&gt;&lt;span class="p"&gt;:?Set TMPDIR to your scratch directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/home-path-demo&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/source&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/exports&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;fixture\n&amp;#39;&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/source/sample.txt&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m venv --without-pip &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/venv&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DATA_ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/source&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;KB_OUT_ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/exports&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH&lt;/span&gt;&lt;span class="s2"&gt;/venv/bin/python&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;PY&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;import os&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;import sys&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;from pathlib import Path&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;source = Path(os.environ[&amp;#34;DATA_ROOT&amp;#34;])&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;out = Path(os.environ[&amp;#34;KB_OUT_ROOT&amp;#34;])&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if not source.is_absolute() or not out.is_absolute():&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; raise ValueError(&amp;#34;Use absolute input and output roots&amp;#34;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;artifact = out / &amp;#34;inventory.txt&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;artifact.write_text(&amp;#34;\n&amp;#34;.join(sorted(p.name for p in source.glob(&amp;#34;*.txt&amp;#34;))) + &amp;#34;\n&amp;#34;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;assert artifact.read_text() == &amp;#34;sample.txt\n&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;print(&amp;#34;interpreter:&amp;#34;, sys.executable)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;print(&amp;#34;verified artifact:&amp;#34;, artifact.resolve())&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;PY&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For a real export, validate expected contents at the selected destination and check the report there. In the failed run, a pipeline ending in &lt;code&gt;tail&lt;/code&gt; returned zero even though Python could not open the script. Command status alone missed both the launch failure and the path mismatch. Check your job&amp;rsquo;s resolved interpreter and output root before its next run.&lt;/p&gt;&#10;</description></item></channel></rss>