<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Filesystem on Lessons learned</title><link>https://eghuro.cz/tags/filesystem/</link><description>Recent content in Filesystem 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, 26 Sep 2026 16:30:00 +0200</lastBuildDate><atom:link href="https://eghuro.cz/tags/filesystem/index.xml" rel="self" type="application/rss+xml"/><item><title>FSKit after mount -F: "ls: fts_read: Operation not permitted"</title><link>https://eghuro.cz/posts/fskit-mount-fts-read-operation-not-permitted/</link><pubDate>Sat, 26 Sep 2026 16:30:00 +0200</pubDate><guid>https://eghuro.cz/posts/fskit-mount-fts-read-operation-not-permitted/</guid><description>&lt;p&gt;My new FSKit filesystem mounted without complaint, but nothing inside it could be read:&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;$ ls -la ~/mnt/storage-test&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls: fts_read: Operation not permitted&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;total 0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ cat ~/mnt/storage-test/sub/a.txt&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat: /Users/me/mnt/storage-test/sub/a.txt: Operation not permitted&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;stat&lt;/code&gt; on the mount point worked. If your FSKit extension does the same after a &lt;code&gt;mount -F&lt;/code&gt; from the command line, check its sandbox before your code: the extension can see the source folder&amp;rsquo;s metadata but may not read its contents.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS 26, a file system extension built on FSKit&amp;rsquo;s &lt;code&gt;FSUnaryFileSystem&lt;/code&gt; that exposes a local folder through a mount point&lt;/li&gt;&#10;&lt;li&gt;A small host app, &lt;code&gt;StorageHost.app&lt;/code&gt;, in &lt;code&gt;/Applications&lt;/code&gt;; its executable runs &lt;code&gt;mount -F -t storagefs &amp;lt;source&amp;gt; &amp;lt;mount point&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;The extension &lt;code&gt;StorageFS.appex&lt;/code&gt; with the FSKit entitlement and the app sandbox&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Info.plist&lt;/code&gt; with &lt;code&gt;FSSupportsPathURLs&lt;/code&gt; set to true and &lt;code&gt;FSRequiresSecurityScopedPathURLResources&lt;/code&gt; set to false&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;h2 id="what-went-wrong"&gt;What went wrong &lt;/h2&gt;&#10;&lt;p&gt;The first mount was over &lt;code&gt;~/Downloads&lt;/code&gt;, so the obvious suspect was privacy protection (TCC) for that folder. A fresh &lt;code&gt;~/storage-test&lt;/code&gt; with one file in it failed the same way, which ruled TCC out and pushed the search elsewhere.&lt;/p&gt;&#10;&lt;p&gt;My coding agent&amp;rsquo;s first log queries came back empty, and it told me there were no log entries. That was wrong. In zsh, &lt;code&gt;log&lt;/code&gt; is a builtin, so &lt;code&gt;log show ... 2&amp;gt;/dev/null&lt;/code&gt; runs the builtin, sends its &lt;code&gt;too many arguments&lt;/code&gt; error to &lt;code&gt;/dev/null&lt;/code&gt;, and prints nothing. Call &lt;code&gt;/usr/bin/log&lt;/code&gt; by its full path. With that, the kernel had the answer:&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;kernel (Sandbox) 3 duplicate reports for Sandbox: StorageFS(83505) deny(1) file-read-data /Users/me/storage-test&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The extension&amp;rsquo;s own log showed the failures from FSKit&amp;rsquo;s side: &lt;code&gt;readDirectory:...reply:error:1&lt;/code&gt; and &lt;code&gt;readFrom:...reply:error:1&lt;/code&gt;. Error 1 is &lt;code&gt;EPERM&lt;/code&gt;, which &lt;code&gt;ls&lt;/code&gt; turns into &lt;code&gt;fts_read: Operation not permitted&lt;/code&gt;.&lt;/p&gt;&#10;&#10;&lt;h2 id="fix"&gt;Fix &lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;mount -F&lt;/code&gt; gives the extension a plain path, not a security-scoped URL, so a sandboxed process can&amp;rsquo;t read an arbitrary path. For development, grant temporary sandbox exceptions in the extension&amp;rsquo;s &lt;code&gt;.entitlements&lt;/code&gt; file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.temporary-exception.files.home-relative-path.read-write&lt;span class="nt"&gt;&amp;lt;/key&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="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/&lt;span class="nt"&gt;&amp;lt;/string&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="nt"&gt;&amp;lt;/array&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="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.temporary-exception.files.absolute-path.read-write&lt;span class="nt"&gt;&amp;lt;/key&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="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Volumes/&lt;span class="nt"&gt;&amp;lt;/string&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="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This covers your home folder and external drives. It doesn&amp;rsquo;t override privacy protection, so &lt;code&gt;~/Downloads&lt;/code&gt;, &lt;code&gt;~/Documents&lt;/code&gt;, and &lt;code&gt;~/Desktop&lt;/code&gt; may still be refused; test with a folder elsewhere. Temporary exceptions are for development on your own machine. A shipping extension should use security-scoped resources instead. &lt;code&gt;FSRequiresSecurityScopedPathURLResources&lt;/code&gt; in &lt;code&gt;Info.plist&lt;/code&gt; requests that; I haven&amp;rsquo;t managed a clean test yet (see below).&lt;/p&gt;&#10;&lt;p&gt;The same logs flagged two more problems that I fixed along the way:&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;-[FSModuleVolume(Project) getMaxFileSizeInBits]: Volume implements neither maxFileSizeInBits nor maxFileSize. One of them must be implemented.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;-[FSVolumeConnector getStandardItemAttributesForItem:replyHandler:]_block_invoke: Reported attributes are incomplete&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first needs &lt;code&gt;maximumFileSizeInBits&lt;/code&gt; on the volume. The second went away once the item attributes set &lt;code&gt;flags&lt;/code&gt;, which cleared the log entry.&lt;/p&gt;&#10;&#10;&lt;h2 id="reinstalling-switches-the-module-off"&gt;Reinstalling switches the module off &lt;/h2&gt;&#10;&lt;p&gt;To test each change, I replaced the app: &lt;code&gt;rm -rf /Applications/StorageHost.app&lt;/code&gt;, copied the new build, and launched it once. After that, &lt;code&gt;mount&lt;/code&gt; failed outright:&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;Module com.example.storage.fs is disabled!&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mount: Unable to invoke task&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;pluginkit&lt;/code&gt; disagreed. &lt;code&gt;pluginkit -e use -i com.example.storage.fs&lt;/code&gt; succeeded, and &lt;code&gt;pluginkit -m&lt;/code&gt; listed the extension with a &lt;code&gt;+&lt;/code&gt;, which means enabled. FSKit still refused. It keeps its own switch in System Settings → General → Login Items &amp;amp; Extensions → File System Extensions, and I didn&amp;rsquo;t find a command-line way to flip it. After I turned StorageFS back on there, the next mount listed and read files, and appends, renames, truncation, and an 8 MB write-and-readback all worked.&lt;/p&gt;&#10;&lt;p&gt;The disabled module also spoiled my one test of security-scoped resources. With &lt;code&gt;FSRequiresSecurityScopedPathURLResources&lt;/code&gt; set to true, &lt;code&gt;mount&lt;/code&gt; failed with &lt;code&gt;Unable to invoke task&lt;/code&gt;, but the reinstall had just switched the module off, so the failure says nothing about the setting.&lt;/p&gt;&#10;&#10;&lt;h2 id="why"&gt;Why &lt;/h2&gt;&#10;&lt;p&gt;An FSKit module is an app extension, and FSKit requires it to run in the app sandbox. When an app hands a folder to the extension, sandbox access can travel with it as a security-scoped URL. &lt;code&gt;mount -F&lt;/code&gt; on the command line passes only a string. The sandbox lets the extension look up metadata for that path, which is why the mount and &lt;code&gt;stat&lt;/code&gt; worked, and denies &lt;code&gt;file-read-data&lt;/code&gt;, which every directory listing and file read needs.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;If an FSKit mount works and every read returns &lt;code&gt;Operation not permitted&lt;/code&gt;, search the kernel log for &lt;code&gt;deny(1) file-read-data&lt;/code&gt; with &lt;code&gt;/usr/bin/log&lt;/code&gt;, then give the extension sandbox access to the source path. After every reinstall, check the File System Extensions switch before you trust a test result.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;ve got &lt;code&gt;FSRequiresSecurityScopedPathURLResources&lt;/code&gt; working with a command-line &lt;code&gt;mount -F&lt;/code&gt;, I&amp;rsquo;d like to hear how.&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>