<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Agents on Lessons learned</title><link>https://eghuro.cz/tags/agents/</link><description>Recent content in Agents 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/agents/index.xml" rel="self" type="application/rss+xml"/><item><title>Adapt security skills to your project's risks: from Go to OWASP LLM and iOS</title><link>https://eghuro.cz/posts/adapt-security-skills-to-project-risks/</link><pubDate>Sun, 25 Oct 2026 21:50:00 +0100</pubDate><guid>https://eghuro.cz/posts/adapt-security-skills-to-project-risks/</guid><description>&lt;p&gt;A security skill written for Go covers different questions than one written for an LLM application or an iOS app. I started with &lt;code&gt;golang-security&lt;/code&gt; from &lt;a&#10; href="https://github.com/samber/cc-skills-golang"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;samber/cc-skills-golang&lt;/a&gt;&#10;, asked Claude to adapt it using the OWASP Top 10 for LLM Applications, and later carried that approach into production iOS development.&lt;/p&gt;&#10;&lt;p&gt;The most useful part of the adaptation was aligning the review questions with the project&amp;rsquo;s risks and the company&amp;rsquo;s values.&lt;/p&gt;&#10;&#10;&lt;h2 id="start-with-a-review-method"&gt;Start with a review method &lt;/h2&gt;&#10;&lt;p&gt;I was working in Go, a language I was unfamiliar with. I used my security skills alongside other Go skills to keep the implementation in check.&lt;/p&gt;&#10;&lt;p&gt;The security skill provided a repeatable method: separate modes for coding, reviewing changes, and auditing a codebase; questions about trust boundaries and attacker-controlled input; and instructions to trace data through surrounding code before reporting a finding. Its references and checklist gave the agent somewhere to look beyond the main instructions.&lt;/p&gt;&#10;&#10;&lt;h2 id="adapt-owasp-categories-to-the-application"&gt;Adapt OWASP categories to the application &lt;/h2&gt;&#10;&lt;p&gt;I asked Claude to create a new skill using the &lt;a&#10; href="https://genai.owasp.org/llm-top-10"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;OWASP Top 10 for LLM Applications (2025)&lt;/a&gt;&#10;, adapted to the project&amp;rsquo;s LLM and Model Context Protocol (MCP) surfaces.&lt;/p&gt;&#10;&lt;p&gt;The resulting &lt;code&gt;llm-security&lt;/code&gt; skill kept the review modes and evidence requirements. It mapped the categories to tools, resources, prompts, and content returned to the model. The Go checklist stayed active for the code underneath.&lt;/p&gt;&#10;&lt;p&gt;For &lt;a&#10; href="https://genai.owasp.org/llmrisk/llm01-prompt-injection"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;prompt injection&lt;/a&gt;&#10;, the reviewer needs to identify whose text reaches the model and what an attacker can control. A returned document body or filename can carry instructions. For &lt;a&#10; href="https://genai.owasp.org/llmrisk/llm062025-excessive-agency"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;excessive agency&lt;/a&gt;&#10;, the questions concern tool permissions, reachable side effects, and authorization enforced outside the model.&lt;/p&gt;&#10;&lt;p&gt;Those questions give the agent a path through the repository. It can follow a tool result back to its data source, then follow a tool call forward to the operation it performs. The skill also asks it to account for existing protections when assessing impact.&lt;/p&gt;&#10;&#10;&lt;h2 id="repeat-the-adaptation-for-ios"&gt;Repeat the adaptation for iOS &lt;/h2&gt;&#10;&lt;p&gt;Later, I applied the same approach to an iOS app using the &lt;a&#10; href="https://owasp.github.io/www-project-mobile-top-10"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;OWASP Mobile Top 10 (2024)&lt;/a&gt;&#10;. That mobile security skill became part of production development.&lt;/p&gt;&#10;&lt;p&gt;The review method carried over. The mapping changed to platform APIs and the code responsible for them:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Mobile risk&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Concrete review question&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Improper credential usage&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Where are tokens stored, and which Keychain access settings apply?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Insufficient input/output validation&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;What can a deep link or share-extension input cause the app to do?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Insecure data storage&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Which protection classes apply to cached and temporary user files?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Inadequate privacy controls&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Where can sensitive data appear outside its intended screen, including logs and notifications?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Repository context matters here. If a shared SDK owns authentication or networking, the skill needs to direct the reviewer into that SDK. Stopping at the changed Swift view would leave the relevant behavior unexamined.&lt;/p&gt;&#10;&lt;p&gt;For a mobile adaptation, &lt;a&#10; href="https://mas.owasp.org/MASVS"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;MASVS&lt;/a&gt;&#10; supplies verification controls, and &lt;a&#10; href="https://mas.owasp.org/MASTG"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;MASTG&lt;/a&gt;&#10; provides testing guidance for checking them.&lt;/p&gt;&#10;&#10;&lt;h2 id="turn-company-values-into-review-rules"&gt;Turn company values into review rules &lt;/h2&gt;&#10;&lt;p&gt;Company values need concrete instructions too. For example, a company that prioritizes privacy can require reviewers to trace user data into logging, analytics, and crash reporting. That turns a broad preference into something an agent can inspect.&lt;/p&gt;&#10;&lt;p&gt;The mobile skill also records settled product decisions. An intentional tradeoff needs a rationale and the protections that still apply so that each review can assess the implementation against that decision. New evidence or changed assumptions can justify reopening it.&lt;/p&gt;&#10;&lt;p&gt;Severity needs the same local reasoning. OWASP provides risk categories; the product&amp;rsquo;s data, attacker access, and existing controls determine a finding&amp;rsquo;s impact.&lt;/p&gt;&#10;&lt;p&gt;A fresh adaptation request could be:&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;Use the existing security skill as a template. Keep its review workflow&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;and evidence requirements. Adapt the relevant OWASP categories to this&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;repository&amp;#39;s entry points, data flows, and platform APIs. Link each check&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;to the responsible code and a primary reference. Record product decisions&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;with their rationale. Preserve the language-level checks alongside it.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tailoring a security skill gives the agent concrete questions about the product it is building. Take one OWASP category in your current skill and map it to the code, data, and product decision the reviewer should examine.&lt;/p&gt;&#10;</description></item><item><title>A repeatable agentic development workflow: context, tickets, implementation, review</title><link>https://eghuro.cz/posts/agentic-development-workflow/</link><pubDate>Sun, 25 Oct 2026 18:25:00 +0100</pubDate><guid>https://eghuro.cz/posts/agentic-development-workflow/</guid><description>&lt;p&gt;Giving a coding agent a feature request and accepting the first working diff is easy. Repeating that across projects is where things get messy: the agent has to rediscover the domain, invent missing requirements, decide what “done” means, and review its own assumptions.&lt;/p&gt;&#10;&lt;p&gt;Here is the setup I now reuse across stacks. It has supported ten iOS app releases this year, helped clean up an existing app built with loose agent guidance, and helped a Go implementation recover after its first approach fell apart - to name a few examples. The Go work also involved a language stack and MCP technology I had not used before. The process didn&amp;rsquo;t provide that expertise on its own; it made the gaps visible early enough to address them.&lt;/p&gt;&#10;&#10;&lt;h2 id="give-the-agent-durable-context"&gt;Give the agent durable context &lt;/h2&gt;&#10;&lt;p&gt;Start with &lt;a&#10; href="https://github.com/mattpocock/skills"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;mattpocock/skills&lt;/a&gt;&#10; and run its repository setup. Keep a short domain vocabulary in &lt;code&gt;CONTEXT.md&lt;/code&gt; (or the current upstream convention, &lt;code&gt;GLOSSARY.md&lt;/code&gt;). Record consequential design choices as ADRs. The agent can then use the same terms in discussions, tickets, code, and reviews.&lt;/p&gt;&#10;&lt;p&gt;For a change whose shape is uncertain, use &lt;code&gt;/grill-with-docs&lt;/code&gt; to work through the decisions and write them down. Run &lt;code&gt;/to-spec&lt;/code&gt; once the conversation has enough detail. Break the spec into small, ordered tickets with &lt;code&gt;/to-issues&lt;/code&gt; in older installations or &lt;code&gt;/to-tickets&lt;/code&gt; in the current upstream skills. Then use &lt;code&gt;/implement&lt;/code&gt; against those tickets.&lt;/p&gt;&#10;&lt;p&gt;The sequence is:&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;shared vocabulary + ADRs → grill → spec → tickets → implement → review&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can enter the sequence at whichever step the work needs. A small, well-specified fix may already have a ticket. An uncertain feature should be grilled before an agent starts writing code.&lt;/p&gt;&#10;&#10;&lt;h2 id="put-tickets-where-the-project-works"&gt;Put tickets where the project works &lt;/h2&gt;&#10;&lt;p&gt;The tracker is an adapter for the process. One project keeps issues in a local &lt;code&gt;docs/TODO.txt&lt;/code&gt;; others use Vikunja or Taiga. What matters is that the agent knows where the authoritative spec and tickets live, how to record blockers, and how to update their status. Write those rules into the repository&amp;rsquo;s agent documentation. A tracker integration skill can handle the API details without changing the development sequence.&lt;/p&gt;&#10;&lt;p&gt;This also makes the setup portable. Moving to a new project means teaching the agent that project&amp;rsquo;s domain, tracker, and checks, rather than rebuilding the workflow.&lt;/p&gt;&#10;&#10;&lt;h2 id="make-implementation-answer-to-the-spec"&gt;Make implementation answer to the spec &lt;/h2&gt;&#10;&lt;p&gt;Implementation includes tests and a diff review. Review the result against two different questions: does the code follow the project&amp;rsquo;s standards, and does it actually satisfy the originating spec? The latter catches plausible code that solves the wrong problem. &lt;a&#10; href="https://eghuro.cz/posts/standards-vs-spec-review"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;A previous review&lt;/a&gt;&#10; found duplicate-write paths after a large test suite passed because it checked a safety claim against the call path.&lt;/p&gt;&#10;&lt;p&gt;Where the project&amp;rsquo;s rules allow it, use a different model family for review. A typical split is Claude writing the code and GPT reviewing it, since a different family brings different blind spots. Claude reviewing Claude has also proven decent, so one family is still a workable setup when you can only use one provider. For larger changes, a &lt;code&gt;multi-review&lt;/code&gt; skill can run distinct axes such as correctness, quality, tests and performance, security, and, for frontend, accessibility. Give each reviewer the same bounded diff and the actual spec; collect actionable findings back into the tracker.&lt;/p&gt;&#10;&lt;p&gt;Attach stack-specific skills where they help. Go work can load guidance for concurrency, error handling, testing, and security. iOS work can use Swift and mobile guidance. For exposed web APIs, mobile clients, or MCP tools, apply relevant OWASP-based web, mobile, and LLM security skills during implementation and review. A checklist gives the agent concrete questions to ask; it does not turn a code review into a security certification.&lt;/p&gt;&#10;&lt;p&gt;Start with one change: write down the vocabulary and decision, turn the request into a spec and tickets, implement one ticket, and review the diff against the spec and its security boundaries. Adjust the tracker, skills, and review axes to fit the next project.&lt;/p&gt;&#10;</description></item><item><title>'Downgrading is unsupported' after a config restore: the update guard was right</title><link>https://eghuro.cz/posts/update-guard-refusal-after-config-restore-was-correct/</link><pubDate>Fri, 16 Oct 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/update-guard-refusal-after-config-restore-was-correct/</guid><description>&lt;p&gt;After a configuration restore, an updater refused 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;that release predates the gateway runtime these session stores now use. Downgrading is unsupported.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The operator requested an update. The restored configuration selected an older code line than the databases required. Restoring the intended per-install channel corrected target selection; bypassing the guard would have put incompatible code over newer state.&lt;/p&gt;&#10;&#10;&lt;h2 id="a-newer-release-number-can-select-older-capabilities"&gt;A newer release number can select older capabilities &lt;/h2&gt;&#10;&lt;p&gt;The checkout followed a development branch. Its session databases already contained runtime-ledger history. The restored configuration backup lacked the install&amp;rsquo;s channel record, so release selection fell back to the stable channel.&lt;/p&gt;&#10;&lt;p&gt;The stable release had a higher patch version, but it came from a release branch without those runtime-ledger tables. Version ordering did not describe the relevant capability ordering.&lt;/p&gt;&#10;&lt;p&gt;The historical guard checked two things: whether the proposed target descended from the running commit, and whether a non-descendant target contained the runtime schema marker. It also read the session stores for ledger rows. Its refusal named the affected stores and stopped before moving the checkout.&lt;/p&gt;&#10;&lt;p&gt;The implementation explained the consequence: older code could not delete or prune sessions with that runtime history. This was a specific compatibility check, not proof that every forward commit or every future schema change would be safe.&lt;/p&gt;&#10;&#10;&lt;h2 id="repair-target-selection-before-changing-code"&gt;Repair target selection before changing code &lt;/h2&gt;&#10;&lt;p&gt;The diagnosis followed the configuration lookup:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Identify the actual install path and the channel record associated with it.&lt;/li&gt;&#10;&lt;li&gt;Compare that record with the backup that was restored. Check defaults when the record is absent.&lt;/li&gt;&#10;&lt;li&gt;Resolve the updater&amp;rsquo;s proposed target to a commit, rather than relying on its release label.&lt;/li&gt;&#10;&lt;li&gt;Compare the target&amp;rsquo;s ancestry and required state capabilities with the running checkout.&lt;/li&gt;&#10;&lt;li&gt;Reassert the intended channel through the updater&amp;rsquo;s supported configuration command, then run its check-only mode.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The corrected check selected the development branch and reported available forward commits—that verified channel selection. The update itself was deliberately not run, so the record does not establish a completed runtime upgrade.&lt;/p&gt;&#10;&lt;p&gt;For a Git-based installation, this read-only check is useful once &lt;code&gt;TARGET&lt;/code&gt; is set to the exact resolved commit. Run it inside that installation&amp;rsquo;s checkout:&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;git merge-base --is-ancestor HEAD &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;:?Set TARGET to the resolved update commit&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Exit status 0 means the target descends from the current checkout. Status 1 means it does not; other failures require investigation. A non-descendant can be a valid release-branch target, so this command alone is not a compatibility verdict. Check the particular schema capability named by the refusal.&lt;/p&gt;&#10;&#10;&lt;h2 id="a-config-only-restore-can-create-a-mixed-generation"&gt;A config-only restore can create a mixed generation &lt;/h2&gt;&#10;&lt;p&gt;The database kept its newer state while the restored configuration lost an additive setting. In this implementation, channel records were keyed by install path identity. Restoring an older backup, or moving an install without recreating its record, could therefore change how later updates were selected.&lt;/p&gt;&#10;&lt;p&gt;Deployment automation should assert the intended channel after install or restore, then read it back through the supported interface. Do not hard-code a path-derived identifier from another machine. Following a development branch is appropriate only when that is the deliberate operating policy; it is not a universal fix for update errors.&lt;/p&gt;&#10;&lt;p&gt;For an intentional rollback, follow the application&amp;rsquo;s documented procedure for compatible code and pre-upgrade data together. Preserve the current state first, and account for work absent from the older backup. Editing a schema-version value or removing ledger rows merely to silence the guard would discard the evidence it uses to protect the store.&lt;/p&gt;&#10;&lt;p&gt;Add an update-target check to your restore drill before allowing the restored installation to update itself.&lt;/p&gt;&#10;</description></item><item><title>Cyber classifiers can interrupt offline pentest summaries</title><link>https://eghuro.cz/posts/agent-security-transcripts-need-provenance-and-checkpoints/</link><pubDate>Fri, 09 Oct 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/agent-security-transcripts-need-provenance-and-checkpoints/</guid><description>&lt;p&gt;An agent spent three minutes reading local transcripts from a finished, authorized security exercise, then stopped 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 request triggered cyber-related safeguards.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No live target was involved. The job was offline summarization, yet the classifier interrupted it before it could write the requested report. The practical fix was to treat security-history analysis as a resumable data pipeline: retain message provenance, checkpoint extracted evidence, and keep the handoff usable by another approved model or provider.&lt;/p&gt;&#10;&#10;&lt;h2 id="a-flag-shaped-string-is-not-a-captured-flag"&gt;A flag-shaped string is not a captured flag &lt;/h2&gt;&#10;&lt;p&gt;The first problem appeared before the classifier fired. A search through about 25 request dumps found dozens of flag-shaped strings. About 20 were spelling, capitalization, spacing, or hyphenation variants of one value.&lt;/p&gt;&#10;&lt;p&gt;Most variants appeared only in assistant reasoning and progress notes. The model repeatedly retyped a remembered value and changed it. A checkpoint then copied one mutation into later sessions.&lt;/p&gt;&#10;&lt;p&gt;Only strings preserved in tool results represented observed server output. To separate evidence from generation, use this origin-based distinction:&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;user request instruction or supplied context&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;assistant text generated claim; unverified&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;assistant reasoning generated hypothesis; unverified&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tool call attempted action&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tool result observed output from that action&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;provider error evidence about the run, not the target&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This applies beyond CTF flags. Treat IDs, hashes, error codes, hostnames, and quoted responses in model prose as leads until the first tool result corroborates them. Exact-looking text is still generated text.&lt;/p&gt;&#10;&#10;&lt;h2 id="offline-processing-still-triggers-cyber-classifiers"&gt;Offline processing still triggers cyber classifiers &lt;/h2&gt;&#10;&lt;p&gt;The retained run used Opus 5.5. It parsed local request dumps, separated real and decoy-looking values, and extracted an existing SMB investigation summary. After that, the next model response was refused under the provider&amp;rsquo;s real-time cyber safeguard. The session recorded a &lt;code&gt;cyber&lt;/code&gt; refusal and pointed to its Cyber Verification Program. The output file did not exist afterward.&lt;/p&gt;&#10;&lt;p&gt;Some source dumps also contained earlier &lt;code&gt;cyber_policy&lt;/code&gt; failures from another provider along with a prompt to apply for Daybreak access. Because these records were historical input, they still increased the concentration of offensive-security language the summarizer saw.&lt;/p&gt;&#10;&lt;p&gt;A related authorized test had a similar shape: generate enough traffic against a non-production environment to trigger a WAF, then verify that the application displays and retries a CAPTCHA correctly. In a newer Opus session, the controlled load-test work was refused. The retained successful session used Opus 4.6 and built the test client, while a later session corrected its cookie handling and the request used to observe the challenge.&lt;/p&gt;&#10;&lt;p&gt;The awkward lesson is that task intent and classifier input are different things. In practice, &amp;ldquo;Summarize these old logs&amp;rdquo; can contain exploit commands, credentials, flags, and attack narratives. Likewise, &amp;ldquo;Test the CAPTCHA&amp;rdquo; can require traffic that resembles denial of service when reduced to request counts.&lt;/p&gt;&#10;&#10;&lt;h2 id="make-the-audit-resumable-and-model-neutral"&gt;Make the audit resumable and model-neutral &lt;/h2&gt;&#10;&lt;p&gt;Do deterministic work before asking a model to synthesize:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Parse each transcript into typed events and preserve the source file, timestamp, role, tool-call ID, and tool-result ID.&lt;/li&gt;&#10;&lt;li&gt;Extract exact-match artifacts only from tool results. Store assistant claims separately as unverified leads.&lt;/li&gt;&#10;&lt;li&gt;Write incremental JSON or Markdown after each source file. Never leave the only useful state in the model&amp;rsquo;s context window.&lt;/li&gt;&#10;&lt;li&gt;Produce a sanitized handoff containing the authorization scope, completed files, pending questions, and evidence pointers.&lt;/li&gt;&#10;&lt;li&gt;Verify the final report exists and reconcile its artifact counts against the extraction index.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Provider access programs are worth the setup when this work recurs. In my experience, GPT Daybreak Blue justified the hassle for authorized security-related tasks. Kimi, GLM, and older model versions have also completed useful work when a newer classifier blocked the same class of jobs. That is a fallback, not a bypass: preserve the same authorization boundary, do not weaken the requested scope, and use models approved for the work.&lt;/p&gt;&#10;&lt;p&gt;Before trusting your next transcript summary, choose one exact token from the report and trace it back to the tool result that observed it.&lt;/p&gt;&#10;</description></item><item><title>Inherited agent notes can send the next agent down the same dead end</title><link>https://eghuro.cz/posts/distrust-inherited-agent-notes/</link><pubDate>Fri, 09 Oct 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/distrust-inherited-agent-notes/</guid><description>&lt;p&gt;A new agent can go through a failed investigation again if its first job is to absorb the conclusions of the previous agent. In the course of an authorised security exercise the user was repeatedly asking for a clean start as the prior progress files could be somewhat misleading. The useful reset was to establish a small verified baseline before using the inherited notes to select the next experiment.&lt;/p&gt;&#10;&#10;&lt;h2 id="a-hint-survived-longer-than-its-evidence"&gt;A hint survived longer than its evidence &lt;/h2&gt;&#10;&lt;p&gt;The conversation history had instructions to go it alone after a number of failed attempts at the task. Another handoff stated the prior agent had got lost and that its notes could be misleading to the replacement.&lt;/p&gt;&#10;&lt;p&gt;One concrete example concerned an environment hint favoring IPv6. A later &lt;code&gt;SUMMARY.md&lt;/code&gt; excerpt said:&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;IPv4 and IPv6 behave identically everywhere tested&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It was very explicit that the IPv6 hint in &lt;code&gt;AGENTS.md&lt;/code&gt; didn&amp;rsquo;t apply in that case. That&amp;rsquo;s a useful clarification, with a caveat: this is a recorded summary of the checks, not an independent re-run of every network probe. It does support the removal of that hint for that particular surface. It doesn&amp;rsquo;t in any way establish equivalence for all services or a successful resolution of the problem. The outcome is still unknown.&lt;/p&gt;&#10;&#10;&lt;h2 id="rebuild-only-enough-context-to-choose-a-test"&gt;Rebuild only enough context to choose a test &lt;/h2&gt;&#10;&lt;p&gt;On a restart read the current task and the mandatory operating instructions. Observe the authorization boundaries and safety rules. Be very clear in your mind about the difference between the instructions and the factual observations about the environment. A line in &lt;code&gt;AGENTS.md&lt;/code&gt; could be a binding rule, it could be a fact about the setup or it could be a hypothesis that you need to go out and check.&lt;/p&gt;&#10;&lt;p&gt;Then establish the facts required to inform the next decision. In a network investigation that might involve confirming the target endpoint, checking that it&amp;rsquo;s reachable and then making a baseline request to it. In a code issue scenario you&amp;rsquo;d want to reproduce the failure that was reported and then have a look at the current code before accepting any diagnosis from a previous developer.&lt;/p&gt;&#10;&lt;p&gt;Only then do you compare the inherited notes against that baseline. That way you don&amp;rsquo;t re-run the whole investigation and the new agent has its own baseline to work from.&lt;/p&gt;&#10;&lt;p&gt;A handoff instruction can be as short as:&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;Establish a minimal baseline from the current tool output.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Treat inherited diagnoses as hypotheses.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;For each claim you make, you need to be able to locate the evidence or at the very least be able to run a very cheap check.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Keep the required operating and safety instructions in force.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;h2 id="preserve-the-test-behind-a-negative-result"&gt;Preserve the test behind a negative result &lt;/h2&gt;&#10;&lt;p&gt;A note such as &amp;ldquo;IPv6 works better&amp;rdquo; is completely devoid of the endpoint, the request and the comparison that would actually make it useful. &amp;ldquo;No difference on these tested endpoints&amp;rdquo; is much more specific and much easier to verify.&lt;/p&gt;&#10;&lt;p&gt;For future handoffs, record:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The claim and its scope (including when it was checked).&lt;/li&gt;&#10;&lt;li&gt;The command or test and a pointer to its output.&lt;/li&gt;&#10;&lt;li&gt;What remains is an interpretation or an unproven lead.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Keep the failed attempts too and state what each one ruled out. A failed request with one set of credentials doesn&amp;rsquo;t rule out the whole protocol. The next agent needs to be able to make the judgement as to whether repeating that test would in fact add any evidence.&lt;/p&gt;&#10;&lt;p&gt;Copying an assertion into successive summaries doesn&amp;rsquo;t constitute independent corroboration. If the raw output is missing then unverified is the correct designation rather than promoting it to a fact.&lt;/p&gt;&#10;&lt;p&gt;Prior to your next agent handoff, pick the assumption that will drive the next action and attach the relevant tool output in support of that assumption. If you can&amp;rsquo;t find it then that will be the restart task.&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 second launch of a resumable worker: 'OSError: [Errno 39] Directory not empty'</title><link>https://eghuro.cz/posts/resumable-worker-concurrent-launch/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/resumable-worker-concurrent-launch/</guid><description>&lt;p&gt;A resumable remote dataset driver failed while cleaning an unfinished task directory:&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;OSError: [Errno 39] Directory not empty&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Another worker was still writing there. The second launch treated an unfinished task as safe to restart, even though the first process already owned it. Checking the surviving worker and validating its affected output recovered that work unit; no locking fix was installed during the session.&lt;/p&gt;&#10;&lt;p&gt;This was an early detached-run failure 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, SSH, and OpenPBS workflow&lt;/a&gt;&#10;. Scheduler-managed execution later improved job tracking, but an application still needs to prevent conflicting owners.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-waiting-wrapper-had-not-disappeared"&gt;The waiting wrapper had not disappeared &lt;/h2&gt;&#10;&lt;p&gt;The first launch created a detached wrapper that waited for a pilot, then became the production driver. Its structure was:&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="k"&gt;until&lt;/span&gt; grep -q &lt;span class="s2"&gt;&amp;#34;finished&amp;#34;&lt;/span&gt; pilot.log&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; sleep 30&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&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; python -u run_dataset.py dataset &amp;gt;&amp;gt; production.log 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before the transition, a search for the production process would not find that driver. After &lt;code&gt;exec&lt;/code&gt;, a search for the wrapper&amp;rsquo;s old command name would no longer identify it. Neither snapshot alone described the complete lifecycle.&lt;/p&gt;&#10;&lt;p&gt;A later check did not establish that the waiting launch was gone. Another detached production command started. The resulting log contained two identical pending-work counts followed by a traceback from &lt;code&gt;shutil.rmtree(gdir)&lt;/code&gt;. Both drivers had selected the same unfinished task, and one attempted cleanup while the other&amp;rsquo;s solver wrote files.&lt;/p&gt;&#10;&lt;p&gt;The resume logic used completion markers to skip finished groups. It offered no exclusive claim on an unfinished directory.&lt;/p&gt;&#10;&#10;&lt;h2 id="recovery-required-checking-the-survivor"&gt;Recovery required checking the survivor &lt;/h2&gt;&#10;&lt;p&gt;The follow-up inspected running processes, restored the affected task&amp;rsquo;s configuration, and checked the surviving computation. Its original log file had been lost during cleanup. A later driver result reported that the task was accepted.&lt;/p&gt;&#10;&lt;p&gt;A broad grep for success also matched text in the traceback. Tightening the check to a result-line pattern distinguished the actual accepted task from an error message that happened to contain matching characters.&lt;/p&gt;&#10;&lt;p&gt;That evidence supports recovery of the affected work unit. It does not prove that no intermediate data was lost or that the session did not add a lock. Eventual production completion checked in a later session also cannot establish that overlapping launches became safe.&lt;/p&gt;&#10;&#10;&lt;h2 id="claim-ownership-before-destructive-resume"&gt;Claim ownership before destructive resume &lt;/h2&gt;&#10;&lt;p&gt;For a single-host driver using a filesystem with working advisory locks, a whole-run lock is one possible improvement. On a system with util-linux &lt;code&gt;flock&lt;/code&gt;, run from the project 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;flock -n .production.lock python -u run_dataset.py dataset&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every launcher must use the same stable lock path. Do not delete and recreate the lock file while work is running. The payload named here is the application&amp;rsquo;s driver, not a standard tool.&lt;/p&gt;&#10;&lt;p&gt;This guard is a new recommendation. A small local check can verify contention without running a dataset:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;fcntl&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tempfile&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&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;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/worker.lock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;w&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;first&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="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_EX&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_NB&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;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/worker.lock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;w&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;second&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;try&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="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_EX&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_NB&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;except&lt;/span&gt; &lt;span class="ne"&gt;BlockingIOError&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;second owner rejected&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;else&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;raise&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;overlapping owner accepted&amp;#34;&lt;/span&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;Cluster filesystems and multi-node workers need an ownership mechanism tested under their actual locking semantics. Per-task claims can allow parallelism that a whole-run lock forbids. &lt;a&#10; href="https://eghuro.cz/posts/openpbs-resume-stable-shard-ids"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;Stable PBS shard assignment&lt;/a&gt;&#10; solves a different problem: preserving each worker&amp;rsquo;s task set across starts and restarts. It cannot exclude two copies of the same shard.&lt;/p&gt;&#10;&lt;p&gt;Before retrying unfinished work, establish whether a live process already owns it. A missing completion marker only says that completion has not been recorded.&lt;/p&gt;&#10;</description></item><item><title>Claude Code over SSH: submit long-running work to OpenPBS</title><link>https://eghuro.cz/posts/claude-code-ssh-openpbs-setup/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/claude-code-ssh-openpbs-setup/</guid><description>&lt;p&gt;Claude Code can prepare a computation on your laptop and operate a remote cluster through ordinary SSH commands. In this setup, Claude Code ran locally on macOS; source uploads, compilation, queue submissions, and checks ran on a Linux server. OpenPBS handled the long-running computation after submission, so the laptop didn&amp;rsquo;t need to keep an SSH session open.&lt;/p&gt;&#10;&lt;p&gt;The operating rule was explicit: work estimated at one hour or more belonged in PBS. That threshold was a local policy, not an OpenPBS default. Use your site&amp;rsquo;s rules, including its restrictions on short tests and compilation on login nodes.&lt;/p&gt;&#10;&#10;&lt;h2 id="establish-the-ssh-connection-yourself"&gt;Establish the SSH connection yourself &lt;/h2&gt;&#10;&lt;p&gt;Configure authentication and verify the server&amp;rsquo;s host key before asking an agent to connect. For example, in your local &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-sshconfig" data-lang="sshconfig"&gt;Host compute-login&#10; HostName 192.168.0.2&#10; User researcher&#10; BatchMode yes&#10; ConnectTimeout 10&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The address and account are placeholders. With your site&amp;rsquo;s actual values and authentication configured, run this from the laptop:&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;ssh compute-login &lt;span class="s1"&gt;&amp;#39;hostname; pwd; command -v qsub; command -v python3&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;BatchMode&lt;/code&gt; makes missing authentication fail instead of waiting for a password. It does not approve the connection or replace host-key verification. Give Claude permission for the intended remote actions through your normal tool-approval workflow; never paste a private key into the conversation.&lt;/p&gt;&#10;&#10;&lt;h2 id="make-the-execution-boundary-explicit"&gt;Make the execution boundary explicit &lt;/h2&gt;&#10;&lt;p&gt;A useful project instruction is:&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;Claude runs on the laptop. Use SSH for remote operations.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Keep this project&amp;#39;s remote files in ~/batch-demo.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Submit tasks estimated at one hour or more through OpenPBS.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Follow site policy for shorter tests; do not bypass the scheduler.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Record the job ID after submission. Check logs and expected outputs before reporting completion. Ask before cancellation or deletion.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Save this with the project&amp;rsquo;s agent instructions. A prose rule still needs checking: our early sessions launched substantial work directly before the PBS rule was captured. Also, having a GPU on a login host does not grant permission to use it.&lt;/p&gt;&#10;&#10;&lt;h2 id="submit-a-small-smoke-test-first"&gt;Submit a small smoke test first &lt;/h2&gt;&#10;&lt;p&gt;These are generalized setup examples, not a copy of the original scientific workload. Create &lt;code&gt;run.py&lt;/code&gt; locally:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;socket&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;worker host: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gethostname&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="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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;python: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="si"&gt;}&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="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;smoke test complete&amp;#34;&lt;/span&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;Create &lt;code&gt;run.pbs&lt;/code&gt; beside 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="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="c1"&gt;#PBS -N agent-smoke&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -l select=1:ncpus=1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -l walltime=00:05:00&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -j oe&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="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;PBS_O_WORKDIR&lt;/span&gt;&lt;span class="p"&gt;:?PBS submission directory missing&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="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python&amp;#34;&lt;/span&gt; -u run.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From the laptop, prepare and submit it:&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;ssh compute-login &lt;span class="s1"&gt;&amp;#39;mkdir -p ~/batch-demo &amp;amp;&amp;amp; cd ~/batch-demo &amp;amp;&amp;amp; python3 -m venv .venv&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scp run.py run.pbs compute-login:batch-demo/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh compute-login &lt;span class="s1"&gt;&amp;#39;cd ~/batch-demo &amp;amp;&amp;amp; qsub run.pbs&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Record the exact job ID that &lt;code&gt;qsub&lt;/code&gt; returns. The batch script selects its directory and interpreter without depending on an activated interactive shell. Install real dependencies according to site policy and check that the environment and project directory are accessible on the execution node. Shared paths and compatible Python installations are prerequisites, not promises made by SSH.&lt;/p&gt;&#10;&lt;p&gt;For GPU work, request the site&amp;rsquo;s documented GPU resources. The original array used &lt;code&gt;select=1:ncpus=2:ngpus=1&lt;/code&gt; per element. GPU visibility variables and binding depend on cluster configuration; inspect them inside an allocation rather than choosing an arbitrary device from the login host.&lt;/p&gt;&#10;&#10;&lt;h2 id="follow-the-job-through-its-outputs"&gt;Follow the job through its outputs &lt;/h2&gt;&#10;&lt;p&gt;On the login host, use &lt;code&gt;qstat -f&lt;/code&gt; with the recorded ID for detailed status. Read the output file named in that status. Sites retaining finished-job history may also support &lt;code&gt;qstat -xf&lt;/code&gt; for accounting. The &lt;a&#10; href="https://github.com/openpbs/openpbs/blob/master/doc/man1/qsub.1B"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;upstream qsub manual&lt;/a&gt;&#10; documents submission options and &lt;code&gt;PBS_O_WORKDIR&lt;/code&gt;; the &lt;a&#10; href="https://github.com/openpbs/openpbs/blob/master/doc/man1/qstat.1B"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;qstat manual&lt;/a&gt;&#10; covers status queries.&lt;/p&gt;&#10;&lt;p&gt;An empty queue is insufficient evidence of success. For this smoke test, check the reported worker and Python path and the completion line. For production, also verify expected task outcomes and output contents.&lt;/p&gt;&#10;&lt;p&gt;The subsequent failures were about execution ownership and verification:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/remote-failed-build-stale-executable"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;A failed remote build launched an old executable&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/resumable-worker-concurrent-launch"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;A second detached launch collided with resumable work&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/openpbs-resume-stable-shard-ids"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;PBS array resume needed stable task IDs&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/rsync-atomic-archives-incomplete-release"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;Archive transfer started before the release was complete&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Before handing over a long run, submit the smoke test and check its output on the execution node&amp;rsquo;s actual environment.&lt;/p&gt;&#10;</description></item><item><title>max_retries_exhausted in a scheduled agent: alert on overdue outcomes, not request dumps</title><link>https://eghuro.cz/posts/failed-unattended-agent-jobs-die-silently-into-a-request-dump-graveyard/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/failed-unattended-agent-jobs-die-silently-into-a-request-dump-graveyard/</guid><description>&lt;p&gt;A scheduled agent produced 67 failed-request dumps over roughly two months, yet the archive contains no successful-turn record. The failure reports 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;max_retries_exhausted&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;non_retryable_client_error&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Saving the request made diagnosis possible later, but it did nothing to alert an operator to the failed work. To surface unattended jobs, add an independent watcher that alerts when a verified outcome becomes overdue, even if the agent cannot produce a response.&lt;/p&gt;&#10;&#10;&lt;h2 id="what-the-failure-archive-actually-proves"&gt;What the failure archive actually proves &lt;/h2&gt;&#10;&lt;p&gt;The records span five error shapes: a provider-side 503 circuit breaker, a broken pipe, an access-verification service error, a quota rejection, and an invalid-credential response. Counting the dump metadata gives 66 retry-exhaustion failures and one non-retryable client error.&lt;/p&gt;&#10;&lt;p&gt;That is evidence of repeated failed requests. It cannot establish that every scheduled invocation failed: successful work might have been recorded elsewhere, and a request can fail after earlier tool actions succeeded. Nor does the absence of a notification in a dump prove that no external monitor existed.&lt;/p&gt;&#10;&lt;p&gt;The operational problem exposed by this archive is narrower and useful: failure artifacts alone cannot answer, &amp;ldquo;When did the job last finish its work?&amp;rdquo; Because this corpus contains no success receipt to reconcile the errors, it demonstrates no monitoring repair.&lt;/p&gt;&#10;&#10;&lt;h2 id="watch-the-outcome-from-outside-the-agent"&gt;Watch the outcome from outside the agent &lt;/h2&gt;&#10;&lt;p&gt;For each scheduled job, define a durable success receipt tied to its actual acceptance condition. For a capture job, that might mean the destination readback confirms the captured items. A model response saying &amp;ldquo;done&amp;rdquo; is not sufficient.&lt;/p&gt;&#10;&lt;p&gt;Record at least a run ID, start time, terminal status, and the last verified success time. Keep partial progress separate so a retry can reconcile completed writes instead of duplicating them.&lt;/p&gt;&#10;&lt;p&gt;Then run a watcher independently of the model call:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Alert on a terminal failure, including a normalized error class and a run ID.&lt;/li&gt;&#10;&lt;li&gt;Alert when the last verified success exceeds the allowed interval plus a bounded grace period.&lt;/li&gt;&#10;&lt;li&gt;Treat a missing first success as overdue too. A watcher that only checks an existing timestamp can overlook a job that never worked.&lt;/li&gt;&#10;&lt;li&gt;Send the alert through a channel that does not require the failing model provider.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Deduplicate repeated alerts for the same incident, but keep an overdue-success alert active until someone acknowledges or resolves it. If alert delivery fails, track that failure as its own observable status.&lt;/p&gt;&#10;&#10;&lt;h2 id="classify-before-retrying"&gt;Classify before retrying &lt;/h2&gt;&#10;&lt;p&gt;The archive&amp;rsquo;s final credential rejection was classified as non-retryable. That classification is a reason to stop automatic retries and request an authentication/configuration check. It does not prove exactly which configuration change caused the rejection.&lt;/p&gt;&#10;&lt;p&gt;A broken pipe or service outage can justify bounded retries. Quota failures may require waiting for a published reset time. Preserve the specific provider error class: an HTTP status alone does not show whether the next attempt can succeed.&lt;/p&gt;&#10;&lt;p&gt;Pausing should produce an explicit incident state with a resume condition. Otherwise, &amp;ldquo;automatically disabled&amp;rdquo; becomes another silent failure.&lt;/p&gt;&#10;&#10;&lt;h2 id="test-the-monitor-with-deliberately-broken-runs"&gt;Test the monitor with deliberately broken runs &lt;/h2&gt;&#10;&lt;p&gt;In a disposable job, inject a retry-exhaustion failure and a non-retryable credential failure. Confirm both reach the independent alert channel. Suppress the runner entirely and confirm that the overdue-success check still fires. Finally, make the model report success without completing the destination write; that must not refresh the success timestamp.&lt;/p&gt;&#10;&lt;p&gt;Keep raw dumps access-controlled and retention-limited. They can contain the complete outgoing prompt and operational context; send a redacted summary and a restricted evidence reference in alerts.&lt;/p&gt;&#10;&lt;p&gt;Before leaving a job unattended, deliberately prevent one run from starting and check whether anyone notices.&lt;/p&gt;&#10;</description></item><item><title>Hourly agent cron vs. '429 usage_limit_reached': persist the provider's quota reset</title><link>https://eghuro.cz/posts/hourly-agent-cron-vs-provider-quota/</link><pubDate>Sat, 19 Sep 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/hourly-agent-cron-vs-provider-quota/</guid><description>&lt;p&gt;An hourly scheduled agent repeatedly received &lt;code&gt;429 usage_limit_reached&lt;/code&gt; while its provider response already supplied a future reset time. One recorded delay was &lt;code&gt;resets_in_seconds: 202825&lt;/code&gt;, about 2.35 days. The scheduler needed a durable not-before deadline checked before making another model call.&lt;/p&gt;&#10;&lt;p&gt;A failure archive contained 62 quota-related failed-request dumps, plus five failures of other kinds. Those are dump counts, not counts of individual HTTP attempts: a single invocation could retry several times before saving a dump. The archive does not establish that every scheduled invocation failed, and it contains no demonstrated scheduling repair or successful recovery.&lt;/p&gt;&#10;&#10;&lt;h2 id="a-cron-tick-should-consult-shared-cooldown-state"&gt;A cron tick should consult shared cooldown state &lt;/h2&gt;&#10;&lt;p&gt;A retry loop inside one invocation cannot govern the next invocation. When a process exits, an in-memory backoff disappears; the next cron tick can start the same futile sequence.&lt;/p&gt;&#10;&lt;p&gt;Persist the cooldown at the scope of the exhausted quota, such as the provider account and quota bucket. Every worker sharing that bucket must consult it before the first request. A different job name does not create a different quota.&lt;/p&gt;&#10;&lt;p&gt;The &lt;a&#10; href="https://www.rfc-editor.org/rfc/rfc9110.html#section-10.2.3"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;HTTP Retry-After specification&lt;/a&gt;&#10; permits either an HTTP date or a delay in seconds. Provider JSON fields such as &lt;code&gt;resets_at&lt;/code&gt; and &lt;code&gt;resets_in_seconds&lt;/code&gt; are provider-specific; normalize them in an adapter rather than assuming their units or presence. Use receipt time for relative delays. If multiple applicable constraints exist, do not schedule before any of them.&lt;/p&gt;&#10;&lt;p&gt;An illustrative gate, after parsing and validating those fields:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;math&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;next_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&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="n"&gt;quota_reset_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&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="n"&gt;deadlines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quota_reset_at&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;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;deadlines&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;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;no usable deadline; use bounded fallback policy&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="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;deadlines&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&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;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;invalid deadline&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;return&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;deadlines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;margin&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;&#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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;may_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;not_before&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;return&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;not_before&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Synthetic clock values; these are not recorded provider timestamps.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quota_reset_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;202825&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;assert&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;203830&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;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;may_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&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;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;may_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&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;assert&lt;/span&gt; &lt;span class="n"&gt;may_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&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="c1"&gt;# A later response cannot shorten an existing cooldown.&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;assert&lt;/span&gt; &lt;span class="n"&gt;next_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Both applicable constraints survive normalization.&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;assert&lt;/span&gt; &lt;span class="n"&gt;next_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_after_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&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="n"&gt;quota_reset_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;9005&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cooldown boundary assertions passed&amp;#34;&lt;/span&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;The gate checks timing only. Production code also needs atomic storage updates, a lock or lease to admit one recovery probe, and clock-skew handling. On restart, read the saved deadline; do not silently treat unreadable state as permission to call. Add bounded jitter after the deadline so workers do not all resume together.&lt;/p&gt;&#10;&#10;&lt;h2 id="give-long-waits-an-operational-policy"&gt;Give long waits an operational policy &lt;/h2&gt;&#10;&lt;p&gt;A near-term transient rate limit can justify a short bounded retry. A multi-day quota exhaustion should explicitly defer the job, with its next eligible probe time visible to an operator. If a provider advertises an implausibly distant reset, escalate instead of silently sleeping forever or truncating the wait and hammering the endpoint.&lt;/p&gt;&#10;&lt;p&gt;At the deadline, admit one probe. A new quota rejection updates the cooldown. An authentication rejection needs intervention rather than the same timed retry. A permitted model response still does not prove the scheduled task completed: reconcile destination writes before replaying work that may have partially succeeded.&lt;/p&gt;&#10;&#10;&lt;h2 id="test-across-process-boundaries"&gt;Test across process boundaries &lt;/h2&gt;&#10;&lt;p&gt;The executable example verifies the early, boundary, and monotonic-deadline cases. To verify a real scheduler, inject a quota response, terminate the worker, and start a new invocation before reset. Assert that no provider call occurs. Start competing workers at reset and assert that only one obtains the recovery lease. Then inject another quota rejection and confirm that it persists the next deadline.&lt;/p&gt;&#10;&lt;p&gt;Stopping repeated calls can hide an overdue job unless monitoring remains independent of the cooldown. The companion post on &lt;a&#10; href="https://eghuro.cz/posts/failed-unattended-agent-jobs-die-silently-into-a-request-dump-graveyard"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;unattended failure alerts&lt;/a&gt;&#10; covers that separate requirement. The disappearance of failure dumps after a reset is not a success receipt.&lt;/p&gt;&#10;&lt;p&gt;Before the next scheduled run, check whether a provider reset deadline survives a worker restart.&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>Standards vs Spec review: a second agent found duplicate writes past 1,572 green tests</title><link>https://eghuro.cz/posts/standards-vs-spec-review/</link><pubDate>Wed, 22 Jul 2026 21:58:00 +0200</pubDate><guid>https://eghuro.cz/posts/standards-vs-spec-review/</guid><description>&lt;p&gt;This is what the review agent reported, just before it listed its findings:&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;The test evidence is clean: Ruff passes, Python reports 1,572 passed / 56 skipped /&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1 expected failure, and the GUI reports 418 passed / 4 skipped. The review nevertheless&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;found safety gaps the tests do not exercise — most importantly, two write clients retry&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;500-after-commit&amp;#34; before their advertised idempotency/verification guard can run.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A coding agent had built the branch: 53 commits, 147 files. A different model reviewed it and found two paths that could put the same invoice in the ERP twice and report plain success. No test covered them, and no linter could. They came from a reviewer with one job: check the code against what the branch said it does.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-setup"&gt;The setup &lt;/h2&gt;&#10;&lt;p&gt;The repository vendors the two-axis &lt;code&gt;code-review&lt;/code&gt; skill from &lt;a&#10; href="https://github.com/mattpocock/skills"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;mattpocock/skills&lt;/a&gt;&#10;. It pins a fixed point and diffs against the merge base, &lt;code&gt;git diff master...HEAD&lt;/code&gt;, then forks two subagents in parallel:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Standards: does the diff follow the repo&amp;rsquo;s documented rules (glossary, ADRs, a baseline of Fowler code smells)?&lt;/li&gt;&#10;&lt;li&gt;Spec: does the diff do what the originating issue or PRD asked for?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The parent agent runs lint and tests meanwhile, &amp;ldquo;as supporting evidence, not as a substitute for the diff review&amp;rdquo;. It then reports the two axes under separate headings and doesn&amp;rsquo;t rerank them, so a pile of naming nits can&amp;rsquo;t bury a P1.&lt;/p&gt;&#10;&lt;p&gt;The Spec brief is short:&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;Report: (a) requirements the spec asked for that are missing or partial;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(b) behavior in the diff that wasn&amp;#39;t asked for (scope creep);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(c) requirements that look implemented but where the implementation looks wrong.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Quote the spec line for each finding, under 400 words.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;h2 id="what-each-axis-found"&gt;What each axis found &lt;/h2&gt;&#10;&lt;p&gt;There was no PRD for the branch, so the Spec reviewer used the commit messages and the ADRs. One commit claimed that the MCP server&amp;rsquo;s idempotency check made it safe to retry a write that got a 5xx. The reviewer traced the call: the check ran before &lt;code&gt;_request()&lt;/code&gt;, the key was recorded after success, and the retry happened inside &lt;code&gt;_request()&lt;/code&gt;, in between. A webhook worker had the same shape, with a verify-by-GET that ran only if the request finally raised. A retry that succeeds never raises. (&lt;a&#10; href="https://eghuro.cz/posts/http-500-ambiguous-write-retry"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;The full HTTP 500 story is here&lt;/a&gt;&#10;.)&lt;/p&gt;&#10;&lt;p&gt;The Standards reviewer found something else. Two functions called &lt;code&gt;urlopen()&lt;/code&gt; directly, past the shared transport that an ADR says owns all retries. It also found an import that went through a module the ADRs forbid as a re-export facade, and an unfinished vocabulary migration.&lt;/p&gt;&#10;&lt;p&gt;None of the Standards findings overlapped with the Spec ones. All six were fixed the same evening, in one commit.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-the-tests-stayed-green"&gt;Why the tests stayed green &lt;/h2&gt;&#10;&lt;p&gt;The tests came from the same author as the claim. No test let a 500 commit the write, let the retry succeed, and counted the writes, because the commit said the idempotency check covered that case. A suite tests what its author thought could break.&lt;/p&gt;&#10;&lt;p&gt;The Spec reviewer starts from the claim instead. A commit message says the code is safe, so it goes looking for the line that makes it safe, and here that line ran on the wrong side of the retry.&lt;/p&gt;&#10;&#10;&lt;h2 id="two-more-runs-same-pattern"&gt;Two more runs, same pattern &lt;/h2&gt;&#10;&lt;p&gt;In the next review, a few hours later on a six-commit range, a guard was undone downstream. Every write had to carry exactly one confidence label, and &lt;code&gt;create_*&lt;/code&gt; checked that. But when label setup had failed, a fallback further down stripped all labels from the payload. The reviewer reproduced a labeled invoice leaving without its label. It also found a write that went through a raw &lt;code&gt;put()&lt;/code&gt; and skipped the scope check that limits which record types may be written at all.&lt;/p&gt;&#10;&lt;p&gt;The third review had nothing to go on but commit messages. The branch added a gate: if an invoice is addressed to another company, warn and refuse to post it. The reviewer said it was inferring the spec from the commits, so I pasted the original request and the Spec subagent re-checked against that. The request held a second requirement the commit summary had left out, which turned out to be implemented. The review also found that a replay of staged payloads called the push function directly and skipped the new gate. And while company registration numbers were checksum-validated, VAT IDs relied on regex alone, so one misread digit could block a legitimate invoice. Both failed the first test, then got fixed.&lt;/p&gt;&#10;&#10;&lt;h2 id="how-to-use-it"&gt;How to use it &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Run Standards and Spec as separate subagents, so neither sees the other&amp;rsquo;s findings, and report them under separate headings.&lt;/li&gt;&#10;&lt;li&gt;Give the Spec reviewer the authoritative spec. If you only have commit messages, say so, and swap in the real request the moment you have it.&lt;/li&gt;&#10;&lt;li&gt;Treat claims in commit messages and ADRs (&amp;ldquo;X makes this safe&amp;rdquo;, &amp;ldquo;Y owns all retries&amp;rdquo;) as requirements. They are the cheapest spec you have, and the author&amp;rsquo;s tests already agree with them.&lt;/li&gt;&#10;&lt;li&gt;If you can, review with a different model than the one that wrote the code. It is less likely to share the writer&amp;rsquo;s blind spots.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;A green suite tells you the code does what its author expected. A Spec review by a second agent tells you whether that matches what the author promised. The duplicate writes were in the gap between the two.&lt;/p&gt;&#10;&lt;p&gt;Take the last branch your agent wrote, and have a second agent check each &amp;ldquo;this makes X safe&amp;rdquo; in its commit messages against the code.&lt;/p&gt;&#10;</description></item><item><title>Before building a reference catalog, check whether the attribute reaches the computation</title><link>https://eghuro.cz/posts/check-the-attribute-reaches-the-computation/</link><pubDate>Sun, 21 Jun 2026 21:00:00 +0200</pubDate><guid>https://eghuro.cz/posts/check-the-attribute-reaches-the-computation/</guid><description>&lt;p&gt;A tax engine for investment accounts has to put every instrument into a category, because the category decides the rules. A real share sold after the holding period can be exempt. A CFD on the same stock is a derivative and is taxed no matter how long you hold it. Brokers don&amp;rsquo;t make this easy. One reports CFDs under &amp;ldquo;stocks&amp;rdquo;. A position can be a fractional share or a whole one. An ETF&amp;rsquo;s ISIN doesn&amp;rsquo;t say whether the fund pays out dividends or reinvests them.&lt;/p&gt;&#10;&lt;p&gt;During the design session, the coding agent recommended a central instrument catalog: ISIN → {category, asset class, accumulating/distributing, derivative, fractional, …}, seeded from a market data provider and maintained over time. Per-broker adapters would propose a category and reconcile it against the catalog, and anything unresolved would go to a review queue. It argued that the accumulating/distributing flag in particular had to come from the catalog.&lt;/p&gt;&#10;&lt;p&gt;I rejected it in two sentences:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;If we get the dividend, we have to deal with it. If the fund gets the dividend, it&amp;rsquo;s non-taxable for the individual.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&#10;&lt;h2 id="the-attribute-decides-whether-an-event-exists"&gt;The attribute decides whether an event exists &lt;/h2&gt;&#10;&lt;p&gt;A distributing fund pays the investor a dividend, and the broker reports it as a dividend event, which is taxed. An accumulating fund keeps the dividend inside the fund. The investor receives nothing, and the broker reports nothing, so nothing is taxed until the units are sold, and the sale is an ordinary sell event.&lt;/p&gt;&#10;&lt;p&gt;The flag the catalog was meant to supply only decides whether an event exists, and the transaction ledger already records whether one did. Removing the flag from the catalog changes no input to the engine. It was also the catalog&amp;rsquo;s main justification.&lt;/p&gt;&#10;&lt;p&gt;The reusable check: for every attribute you are about to maintain as reference data, flip its value and ask which computation input changes.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Attribute&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;What changes if it flips&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Already in the events?&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Source&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Accumulating / distributing&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Whether a dividend event exists&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Yes: the event is present or absent&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;None needed&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CFD / real share&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;The rules applied to the same sell event&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;No&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;The broker&amp;rsquo;s own export (instrument type, leverage)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Fractional / whole share&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Whether an exemption is arguable&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;No&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;The broker&amp;rsquo;s idiom&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Crypto / token / derivative on crypto&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Which rules and caps apply&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;No&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;The broker&amp;rsquo;s idiom&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;The remaining attributes do change the computation. All of them can be read from the broker that produced the record, so classification belongs in the per-broker adapter. The adapter already parses that broker&amp;rsquo;s export and knows its quirks. A global catalog would only reconcile against them afterward.&lt;/p&gt;&#10;&#10;&lt;h2 id="classify-where-the-source-idiom-lives-and-refuse-to-guess"&gt;Classify where the source idiom lives, and refuse to guess &lt;/h2&gt;&#10;&lt;p&gt;The adapter for the broker that labels CFDs as stocks classifies by the type and leverage columns of its own export:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leverage&lt;/span&gt;: &lt;span class="kt"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Category&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;explanation&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;type&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/cfd|contract for difference/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;derivative&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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leverage&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/x$/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;explanation&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Type or Leverage is missing or invalid.&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;crypto&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&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;return&lt;/span&gt; &lt;span class="nx"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;crypto&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;explanation&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`Crypto Type conflicts with leveraged position (&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leverage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;).`&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="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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/(stock|etf|real|cfd|contract)/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;derivative&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/(stock|etf|real|share|security)/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;security&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;explanation&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`Type &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt; and Leverage &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leverage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt; cannot be classified.`&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="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No default branch picks the most likely category. A row the rules cannot classify becomes a review item (&lt;code&gt;AMBIGUOUS_CATEGORY&lt;/code&gt; with the raw row reference) and stays out of the tax computation. A misclassified instrument produces a wrong filing with no warning sign, which is worse than an incomplete one.&lt;/p&gt;&#10;&lt;p&gt;The ISIN and ticker stay on the record for cross-checking and audit, but nothing uses them to decide a category.&lt;/p&gt;&#10;&#10;&lt;h2 id="what-dropping-the-catalog-removed"&gt;What dropping the catalog removed &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A licensed data feed and the work of keeping up with new listings, delistings, and corrections.&lt;/li&gt;&#10;&lt;li&gt;A reconciliation layer between adapter output and catalog values, plus the conflict UI that goes with it.&lt;/li&gt;&#10;&lt;li&gt;A global table keyed by ISIN that every user&amp;rsquo;s trades would look up. In this design, real instrument identity is visible only inside the adapter at ingestion, and the stored record carries a pseudonymous ID. That design is described in &lt;a&#10; href="https://eghuro.cz/posts/split-confidential-data-by-reader"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;When the server must compute on confidential data, split the data by who may read it&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The cost moved into the adapters: classification is now only as correct as each adapter. Every adapter has its own test suite for its broker&amp;rsquo;s export format, and adding a broker means writing its classification rules and tests.&lt;/p&gt;&#10;&#10;&lt;h2 id="caveat-some-tax-systems-tax-events-that-never-happened"&gt;Caveat: some tax systems tax events that never happened &lt;/h2&gt;&#10;&lt;p&gt;The shortcut holds where tax follows money the investor actually receives. Some tax systems tax deemed income on fund holdings that paid nothing out. Germany&amp;rsquo;s &lt;em&gt;Vorabpauschale&lt;/em&gt;, UK reporting funds&amp;rsquo; excess reportable income and the US PFIC regime are examples. There, the accumulating flag produces an income line that no event carries, and you need the attribute from somewhere. That is exactly the case the flip test catches: the computation input changes even though no event changes.&lt;/p&gt;&#10;&#10;&lt;h2 id="takeaway"&gt;Takeaway &lt;/h2&gt;&#10;&lt;p&gt;Before you maintain reference data for an attribute, flip it and see which input of your computation changes. If the only effect is whether an event happens, and your event stream already records that, you don&amp;rsquo;t need the attribute. If an input does change, get the value from whoever produced the record, and send anything it can&amp;rsquo;t classify to review.&lt;/p&gt;&#10;&lt;p&gt;The agent&amp;rsquo;s recommendation was internally consistent and sounded responsible. It took someone who knew where the taxable event comes from to see that the attribute changed nothing. When an agent proposes reference data, run the flip test on every attribute before accepting it.&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>