<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
	<channel>
		<title>Nick Blokhin — Notes</title>
		<link>https://blokhin.us/notes/</link>
		<description>Short technical notes on software engineering, tools, and AI.</description>
		<language>en</language>
		<atom:link href="https://blokhin.us/rss.xml" rel="self" type="application/rss+xml"/>
		<lastBuildDate>Sat, 15 Aug 2026 00:00:00 +0000</lastBuildDate>
		<item>
			<title>Agents in the Background: No LangGraph, No LangChain Required</title>
			<link>https://blokhin.us/notes/background-agents-without-frameworks/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/background-agents-without-frameworks/</guid>
			<pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate>
			<description>How I actually run agents that watch folders and websites: a long-lived process, plain trigger scripts, cron — and no LangGraph or LangChain.</description>
			<content:encoded><![CDATA[<p>Someone recently asked me how I set up agents to run in the background — watching a folder, polling a website — and whether I use a framework like LangGraph or LangChain for it.</p>
<p>Short answer: no, neither. My setup is considerably more boring than that, and I think that is exactly why it keeps running for years instead of until the next major version of a framework.</p>
<h2>Three moving parts</h2>
<p>Everything I have running in the background comes down to three independent pieces.</p>
<p><strong>The agent</strong> is a long-lived process on one of my local machines or servers. It does not monitor anything on its own — it knows how to accept a task and carry it out with its tools. Which agent barely matters here: Claude Code in headless mode, Hermes, your own loop over the API, something else entirely. For this scenario any of them will do.</p>
<p><strong>The triggers</strong> are ordinary scripts and scheduled jobs. One watches a folder and fires when a new file appears; another polls a site or an API on a schedule and compares the response against the last one it saw. Nothing about this is AI-specific — it is cron and file watchers, tools that have been around for decades.</p>
<p><strong>The handoff</strong> is what happens when a trigger catches something: it sends the agent a task describing what happened and what to work on. From there the agent decides which of its tools to use. You can build the handoff any number of ways — a file dropped into a queue directory, a CLI call with a prompt, an HTTP request — and the choice comes down to what is convenient for you and what kind of system you are building.</p>
<p>The property that makes this work: intelligence is only needed in the third part. Noticing that a file appeared, or that a page changed, is a job Unix solves without spending a single token. Paying for LLM calls — or burning local GPU cycles — so that the agent can poll the world itself is the most expensive way ever devised to write <code>while true; do sleep 60; done</code>.</p>
<h2>What it looks like in practice</h2>
<p>The skeleton of a folder trigger is a few lines:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
# Watch the inbox folder, hand each new file to the agent
fswatch -0 --event Created ~/agent-inbox | while read -d &#39;&#39; file; do
  send-task &quot;New file dropped: $file. Process it.&quot;
done
</code></pre>
<p>Bash is not the point — the shape is. The same trigger in Python, when you want more structure and somewhere to put error handling:</p>
<pre><code class="language-python">#!/usr/bin/env python3
&quot;&quot;&quot;Watch the inbox folder, hand each new file to the agent.&quot;&quot;&quot;
import time
from pathlib import Path

INBOX = Path.home() / &quot;agent-inbox&quot;

def main():
    seen = set(INBOX.iterdir())
    while True:
        current = set(INBOX.iterdir())
        for path in current - seen:
            send_task(f&quot;New file dropped: {path}. Process it.&quot;)
        seen = current
        time.sleep(5)

if __name__ == &quot;__main__&quot;:
    main()
</code></pre>
<p>Watching a website is a cron line plus a comparison against the last snapshot:</p>
<pre><code class="language-bash">*/15 * * * * check-page.py https://example.com/releases
</code></pre>
<pre><code class="language-python">#!/usr/bin/env python3
&quot;&quot;&quot;Poll a URL, notify the agent when it changes. Run from cron.&quot;&quot;&quot;
import hashlib
import sys
import urllib.request
from pathlib import Path

STATE = Path(&quot;/tmp/page.hash&quot;)

def main(url):
    body = urllib.request.urlopen(url, timeout=30).read()
    new = hashlib.sha256(body).hexdigest()
    old = STATE.read_text().strip() if STATE.exists() else None
    if new != old:
        STATE.write_text(new)
        send_task(f&quot;Page {url} changed. Check what&#39;s new and summarize.&quot;)

if __name__ == &quot;__main__&quot;:
    main(sys.argv[1])
</code></pre>
<p>Either language gives you the same three parts, and neither version contains a single AI-specific line. Python mostly buys you a comfortable place for the logic to grow into: retries, HTML parsing when hashing the whole page turns out to be too blunt, a filter deciding which changes are even worth an agent&#39;s time.</p>
<p><code>send-task</code> / <code>send_task</code> stands in for the handoff. The point of keeping it a separate thing is that every piece can then be exercised on its own: run the trigger by hand and see what it catches, feed the agent a task directly and see what it does. When something breaks at three in the morning, there are only three places it can be.</p>
<h2>Why not LangGraph or LangChain</h2>
<p>I hold no ideological position that frameworks are evil. They simply solve a different problem than the one being asked about.</p>
<p>LangGraph and LangChain are about orchestration <em>inside</em> the agent process: graphs of steps, branching, state carried between nodes. &quot;Run an agent in the background and react to events&quot; is a question about the <em>infrastructure around</em> the agent — and for that the operating system already ships everything required: cron, systemd, file watchers, pipes. Those tools are older than most readers of this note, they do not get breaking changes every quarter, and their behavior does not depend on the version of a Python package.</p>
<p>Pulling in a framework to watch a folder means inserting a layer of dependencies between the event and the agent that adds nothing except surface area for failure. If one of my background jobs ever needs a genuine multi-step graph with branching and rollbacks, I will revisit the choice. So far none of them has.</p>
<h2>A background agent is an unsupervised agent</h2>
<p>One consequence goes past the original question, but leaving it out would be dishonest: an agent running in the background is by definition an agent nobody is watching. There is no &quot;I&#39;ll look at the diff before it applies&quot; — the event arrived at night and the agent worked at night.</p>
<p>Which puts the full weight on the principle I wrote about in <a href="/notes/claude-code-auto-mode/">the note on Auto Mode</a>: boundaries have to be mechanical, not statistical. A background agent of mine gets exactly the access its class of tasks requires — its own folder, its own restricted credentials, and no direct path to anything I cannot afford to lose. That isolation can be a separate system user or a container, or you can move the whole thing onto a separate machine.</p>
<p>It is the same approach as in <a href="/notes/agentic-coding-workflow/">my work with coding agents</a>: trust is configured by architecture — by what the agent can physically reach — not by a confirmation dialog.</p>
<h2>The bottom line</h2>
<p>The answer to the original question fits on one line: a long-lived process plus ordinary trigger scripts, zero frameworks. The event-driven half is a problem Unix solved decades ago, and intelligence is only needed once the event has been caught and has to be dealt with. The part of a background agent that genuinely deserves attention is not orchestration but boundaries — with no human in the loop, they have to be mechanical.</p>
<p>How I divide work with agents in general, and what I run them on, is on <a href="/ai/">the AI page</a>.</p>
]]></content:encoded>
		</item>
		<item>
			<title>Claude Code Auto Mode: The &quot;Allow&quot; Button Was Never Protecting You</title>
			<link>https://blokhin.us/notes/claude-code-auto-mode/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/claude-code-auto-mode/</guid>
			<pubDate>Mon, 10 Aug 2026 00:00:00 +0000</pubDate>
			<description>Anthropic's classifier blocks 89% of dangerous actions; humans approve almost everything. Why Auto Mode by default matters — and what it doesn't replace.</description>
			<content:encoded><![CDATA[<p>Anthropic is switching Claude Code to Auto Mode by default, and to me this is one of the biggest developer stories of the past few days. The change itself: the agent acts without constant permission prompts, and before each tool call a separate classifier decides whether the action is destructive, outside the scope of the task, or the result of a prompt injection.</p>
<p>The change has sparked a heated debate, and a lot of the reaction is negative. As I see it, the whole argument boils down to one question: are we ready to trust an agent with the terminal. But I think that framing is wrong. We already trust it. What changed is not the level of trust — it is who is accountable for it.</p>
<h2>The numbers that explain everything</h2>
<p>The most interesting part of the announcement is not the feature but the data Anthropic uses to justify it. According to the research being discussed, the classifier blocked about 89% of dangerous actions. Humans approving manually blocked 13.6%. And that is not even the worst case: as requests pile up, human approval degrades further due to approval fatigue.</p>
<p>Anthropic has long pointed to exactly this as the problem with the perpetual &quot;Allow&quot;: in practice, users approve about 93% of requests almost automatically. A confirmation dialog you click through nine times out of ten without looking is not control. It is a ritual of control. It creates the feeling of safety without creating safety — while faithfully eating the agent&#39;s main advantage: the ability to work while you are busy with something else.</p>
<p>Accept those numbers and the conclusion is uncomfortable but direct: replacing the human approver with a classifier is not lowering the security bar. It is raising it. From 13.6% to 89%.</p>
<h2>What this actually means</h2>
<p>Formally, nothing revolutionary happened. Coding agents have been running autonomously for a long time — simply because users have been mass-enabling the equivalents of <code>--dangerously-skip-permissions</code>. A flag with a self-explanatory name that people turn on because working any other way is impossible.</p>
<p>The real difference is the shift in responsibility. It is one thing to have &quot;a dangerous flag you enabled at your own risk,&quot; and quite another to have a default, first-class UX the vendor stakes its name on. Until now, autonomy was effectively the user&#39;s fault; now it becomes Anthropic&#39;s position.</p>
<p>To me this event matters more than another bump on SWE-bench. Benchmarks measure how well an agent writes code. What is changing here is the working model itself: coding agents are moving along the path &quot;assistant → supervised agent → semi-autonomous worker,&quot; and Auto Mode by default is an official acknowledgment that the industry is already on the third step. Not &quot;the model got smarter&quot; but &quot;the model is now expected to work unsupervised.&quot;</p>
<p>I see nothing frightening in this logic — <a href="/notes/agentic-coding-workflow/">my work with agents</a> was never built on the &quot;Allow&quot; button in the first place, but on boundaries written into the project itself: a spec the code can cite, rules that record expensive mistakes, tests on the critical spots, and diff review. The confirmation button was never a real line of defense in that setup — the 93% figure merely confirms what everyone who works with agents knows from experience.</p>
<h2>A classifier is not a sandbox</h2>
<p>That said, it is important to call things by their names: a classifier is not a sandbox and not a formal security guarantee. It is a probabilistic model that catches 89% of dangerous actions. The remaining 11% have not gone anywhere — and prompt injection as a class of attacks is specifically designed to hunt for exactly those gaps.</p>
<p>The difference is fundamental. A sandbox is a boundary defined by a mechanism: whatever the agent decides, it physically cannot get outside the container, the VM, or the restricted user. A classifier is a boundary defined by a prediction: a dangerous action will most likely be stopped. &quot;Most likely&quot; is not the level of guarantee you build on when production credentials and other people&#39;s data are involved.</p>
<p>So for serious work my position does not change: an isolated environment remains the last line of defense. The classifier is a good middle line — it catches the vast majority of problems cheaply and without friction. But the last line has to be mechanical, not statistical: a separate environment, restricted credentials, no direct access to anything you cannot afford to lose.</p>
<h2>The bottom line</h2>
<p>Auto Mode by default is the right move, backed by honest and uncomfortable data: a human with an &quot;Allow&quot; button protects worse than a classifier, and everyone has known it for a long time. But the right move does not cancel the main rule of working with autonomous agents: trust is configured not by a confirmation dialog and not by a classifier, but by architecture — by what the agent can physically reach. Anthropic removed the ritual. The real boundaries are still on us.</p>
]]></content:encoded>
		</item>
		<item>
			<title>Mirroring Your Repo Is Not a Backup Plan</title>
			<link>https://blokhin.us/notes/mirroring-is-not-a-backup-plan/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/mirroring-is-not-a-backup-plan/</guid>
			<pubDate>Sat, 08 Aug 2026 00:00:00 +0000</pubDate>
			<description>GitHub Actions and Pages went down and the vendor lock-in argument came back. I mirror every repo to three hosts — and still could not cut a release.</description>
			<content:encoded><![CDATA[<p>On August 6th, degraded GitHub Actions and Pages was actively discussed on Hacker News. The GitHub problems meant builds broke, deployments broke, project pages stopped publishing — and the discussion turned almost immediately from &quot;they are having an outage&quot; into &quot;how dependent on them are we, exactly?&quot;.</p>
<p>A reaction that size makes sense. GitHub stopped being just code hosting a long time ago: it is also the CI, the release process, the public face of a project, and often the project&#39;s website. When one company holds all four roles, any outage of theirs turns straight into a conversation about vendor lock-in.</p>
<p>I find that interesting from a practical angle. I have mirrored my repositories for years and assumed I was covered. An outage is a good excuse to check whether that is true.</p>
<h2>What I actually have</h2>
<p>Every one of my projects lives on three hosts at once: my own Gitea on my server, GitLab, and GitHub. <a href="/texodus/">Texodus</a> has four — the fourth is a Raspberry Pi in the next room.</p>
<pre><code>$ git remote -v
server   ssh://git@git.blokhin.us/nick/Texodus.git
gitlab   git@gitlab.com:w512/texodus.git
origin   git@github.com:w512/Texodus.git
rpi5     ssh://git@rpi5-local/nick/Texodus.git
</code></pre>
<p>This costs essentially nothing: <code>git remote add</code> once, plus the habit of pushing to all of them. If GitHub disappeared tomorrow along with my account, the commit history would not be affected.</p>
<p>It sounds like a finished answer to &quot;what if&quot;. It is an answer to the wrong question.</p>
<h2>The code is safe and I still cannot ship</h2>
<p>Users do not download a repository. They download a built <code>.dmg</code>, <code>.AppImage</code>, or installer — and those are produced by GitHub Actions.</p>
<p>In my case that is not simply &quot;run the build somewhere else&quot;. The Texodus release pipeline contains a non-obvious step: after the AppImage is packaged, the build machine&#39;s graphics libraries are stripped out of it, because otherwise the app <a href="/notes/tauri-three-platforms/">does not start on recent distributions</a>. That fix exists only inside the workflow file. Building the project locally &quot;the usual way&quot; would hand me an artifact with the old bug back in it — and I would not notice immediately.</p>
<p>That is the real single point of failure. Mirrors protect the history of the code. They do nothing at all for the ability to release and deliver an update.</p>
<p>There is a partial exception. macOS builds already run locally, from a separate script, because Developer ID signing and notarization need a certificate from the local keychain. So one of my three release paths does not depend on GitHub — but for an entirely unrelated reason. That is luck, not planning.</p>
<h2>What follows from this</h2>
<p>Leaving GitHub entirely is not rational: the audience is there, the issues are there, discovery is there. The question is not how to quit, but how to make an outage mean a delay rather than a standstill. The minimum set looks like this:</p>
<ul>
<li><strong>A repository mirror.</strong> The cheapest item and, judging by my own case, the most overrated: it takes a minute to set up and covers only part of the problem.</li>
<li><strong>The ability to run CI locally.</strong> Not &quot;in principle possible&quot; — actually verified: the pipeline runs on your own machine and produces the same artifact.</li>
<li><strong>Artifact export.</strong> Releases that exist only as GitHub Releases are unavailable to your users exactly when the platform is unavailable.</li>
<li><strong>A written emergency release procedure.</strong> One document: what to run, where the keys are, where to publish, when the normal path is closed.</li>
</ul>
<p>The last one I wrote down for myself: I do not have it. Until an outage actually happens, &quot;I roughly remember how this gets built&quot; feels like enough — and that feeling is precisely the dependency nobody closes.</p>
<h2>The boring counterexample</h2>
<p>The site you are reading did not notice the outage at all. It is built as static files on my laptop and published to my own nginx; GitHub is involved in its life only as one of three code mirrors. Neither Actions nor Pages sits anywhere between the source and the reader.</p>
<p>That is not foresight — it is just the kind of site I find comfortable to run. But the coincidence is instructive: the fewer moving parts belonging to someone else stand between your code and your user, the fewer questions someone else&#39;s outage raises.</p>
]]></content:encoded>
		</item>
		<item>
			<title>Kimi K3: When an Open Model No Longer Means a Compromise</title>
			<link>https://blokhin.us/notes/kimi-k3-open-weight/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/kimi-k3-open-weight/</guid>
			<pubDate>Wed, 05 Aug 2026 00:00:00 +0000</pubDate>
			<description>Kimi K3 matters less for its benchmark scores than for what it signals — the line between self-hosted and cloud models is starting to disappear.</description>
			<content:encoded><![CDATA[<p>Moonshot has released Kimi K3, and the release marks a notable step toward wider adoption of open-weight models — models whose parameters are available to developers. Interest turned out to be high: just 48 hours in, Moonshot had to temporarily suspend subscriptions due to a shortage of GPU capacity.</p>
<p>Across numerous tests, Kimi K3 is already approaching the best closed models from OpenAI and Anthropic in coding and tool-use tasks. What matters most is that, for the first time, an open-weight model shows a comparable level on the kinds of tasks typical for AI agents.</p>
<p>The infrastructure around such models is developing at the same time. vLLM added support for Kimi K3 on release day, including recommendations for running the enormous MoE model with a context of up to 1M tokens. DigitalOcean added K3 to its Inference Engine almost immediately, making it possible to use the model as a managed API without having to stand up complex infrastructure yourself.</p>
<p>NVIDIA also used the moment to publicly back the open-weight approach, calling it an important part of US leadership in AI, and took part in founding the Open Secure AI Alliance, which is meant to address the security of open models.</p>
<p>That is why, in my view, the significance of Kimi K3 lies not so much in its benchmark results as in the fact that the boundary between self-hosted and cloud models is starting to disappear. A developer can get the control of an open model while still using convenient managed infrastructure. The choice between local deployment and an API is gradually turning from a standalone infrastructure project into practically a matter of configuration.</p>
]]></content:encoded>
		</item>
		<item>
			<title>How I Split Work with Agentic Coding Tools: Architect, Reviewer, Agent</title>
			<link>https://blokhin.us/notes/agentic-coding-workflow/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/agentic-coding-workflow/</guid>
			<pubDate>Sun, 02 Aug 2026 00:00:00 +0000</pubDate>
			<description>An AI model test became a shipped app. How I split work with coding agents: trust boundaries, written guardrails, and what I never delegate.</description>
			<content:encoded><![CDATA[<p><a href="/texodus/">Texodus</a> started as a model test. My <a href="https://github.com/w512/Prompt-Vault">Prompt Vault</a> repository holds a spec called &quot;Markdown Editor — Tauri 2 Desktop App&quot; in the Hard category: forty-four lines of requirements I use to see what models do with a non-trivial task. One of those runs produced code good enough to be worth finishing. Today it is an editor for macOS, Windows, and Linux that has been through four major versions and <a href="/notes/tauri-three-platforms/">a Linux packaging saga</a> with nothing whatsoever to do with AI.</p>
<p>The question I get asked most often is not which tool I use. It is what this looks like over the long run. Here is the answer.</p>
<h2>Architect: the spec that is not in the prompt</h2>
<p>The public spec is forty-four lines with no section numbers anywhere. Yet the <a href="https://github.com/w512/Texodus">Texodus sources</a> still carry references like these:</p>
<pre><code class="language-ts">// ── Window close confirmation (§4.4) + Tauri drag-drop (§4.1) ──
/* Implements the 3-button flow required by spec §4.4 (Save / Don&#39;t Save / Cancel) */
// Debounce rendering (§6.2)
</code></pre>
<p>They point at a detailed, numbered specification — it runs to §12.8 — that I wrote myself, using that public prompt as the starting point. This is probably the clearest answer to what the human actually does here: not &quot;ask for a Markdown editor&quot;, but turn a page of requirements into a document the code can cite and that you can later check implementation against, line by line.</p>
<p>Those markers are still in the sources four major versions later, and a rule in the agent instructions requires keeping them: &quot;<code>§N.N</code> comments reference the original product spec — keep them intact&quot;. A <code>§4.4</code> next to the unsaved-changes dialog is not decoration. It is an address: it takes a second to find how the behavior was meant to work, instead of reconstructing the intent from the code.</p>
<h2>The boundary follows the cost of a mistake</h2>
<p>The agent does not get the same freedom everywhere in the codebase. The line is drawn not by type of task but by what an error costs. The Texodus rules mark it in plain text:</p>
<blockquote>
<p><code>services/markdownSanitizer.ts</code> — <strong>single source of <code>marked</code> options + the DOMPurify whitelist</strong>, shared by live preview and export so sanitization can&#39;t drift. A regression here = XSS.</p>
</blockquote>
<blockquote>
<p>The cold-start race (macOS <code>Opened</code> fires before <code>main</code> exists) is handled in <code>main_window_is_empty</code> — read its comments before touching.</p>
</blockquote>
<p>Where a mistake is cheap — layout, refactoring, routine changes — the agent works on its own. Where it is expensive — the sanitizer, cryptography, saving files — the freedom ends.</p>
<h2>The rules file is an archive of mistakes already made</h2>
<p>The most useful thing I have learned from all of this: agent instructions consist almost entirely of prohibitions, and every prohibition is the trace of something that once broke. Not an abstract style guide — a list of expensive lessons written in the imperative.</p>
<p>From <a href="https://github.com/w512/Kivarion">Kivarion</a>, my password manager:</p>
<blockquote>
<p><code>onDragOver</code> must decide with <code>isEntryDrag</code>, <strong>never</strong> with <code>getData</code>: the drag data store is in protected mode for the whole drag, so <code>getData</code> returns <code>&#39;&#39;</code> there — which is exactly how entry→group drag came to be silently dead.</p>
</blockquote>
<blockquote>
<p>Do not move it back into the webview, and do not reintroduce <code>argon2-browser</code>.</p>
</blockquote>
<p>The second rule comes with a full paragraph of reasoning: the WASM call is synchronous no matter what the promise looks like, and kdbxweb re-derives the key on every save — so the interface froze for the length of the KDF on every auto-save.</p>
<p>From <a href="https://github.com/w512/Texodus">Texodus</a>:</p>
<blockquote>
<p><code>AppState.pending_files</code> is a <strong>queue per window label</strong> — several &quot;Open With&quot; files can target one window before the frontend drains; don&#39;t collapse it back to a single slot.</p>
</blockquote>
<p>Every one of these lines was written after something went wrong. Their value is that they stop anyone from returning to the &quot;simpler&quot; solution that already turned out to be the wrong one — the agent, and me six months later.</p>
<h2>Reviewer: I look at everything, I read selectively</h2>
<p>I go through every diff. But I do not read all of it closely — only the parts that touch critical and important code. That is the only honest way to work at the speed agents give you: reading every line would eat the entire gain, and reading nothing means eventually shipping a regression in the sanitizer.</p>
<p>So the other half of review is automated. The rules contain a one-line gate:</p>
<blockquote>
<p>Run typecheck + test + lint before declaring frontend work done.</p>
</blockquote>
<p>Work is not done until those three commands pass. The tests cover exactly the dangerous places — the sanitizer is tested for stripping XSS, and the rules even record why the test environment is jsdom rather than happy-dom: happy-dom&#39;s parser mishandles siblings after a <code>&lt;script&gt;</code>, which would have made the test useless.</p>
<h2>This is a system, not one project&#39;s setup</h2>
<p><code>CLAUDE.md</code> lives in many of my repositories. In <a href="https://github.com/w512/Texodus">Texodus</a> and <a href="https://github.com/w512/Kivarion">Kivarion</a> an <code>AGENTS.md</code> sits next to it — I work with two agents in parallel, and each one needs its own file carrying the same knowledge about the project. Those files never reach the public repositories: they are in <code>.gitignore</code> and stay local.</p>
<p>Rules pay off best where things break quietly. For example, the <code>CLAUDE.md</code> for this site records that routes are linked by <code>name</code>, so renaming one silently breaks both the links and the active-menu highlight, and that the sitemap generator parses the routes file with a regex — which is why the one-line route format must not be touched. Neither tests nor reading a diff catches that.</p>
<h2>Where it breaks: the line between JS and Rust</h2>
<p>The most expensive problem I have hit was not code quality. It was that the agent has no memory between sessions.</p>
<p>In a Tauri app, almost every task has two places it could be solved: the JavaScript frontend or the Rust backend. The agent kept getting this wrong — and it decided differently in different sessions and different tasks. Not because the choices were bad: each individual one was defensible. The problem is that every new session starts from zero, so any decision not written down in the project gets made again, differently. Architecture does not drift because of one big mistake. It drifts because of a dozen small ones, each perfectly reasonable on its own.</p>
<p>There is exactly one cure: draw the boundary explicitly, in writing. In Texodus the rules list all five Rust commands and state what belongs to the backend and what to the frontend. In Kivarion the line is drawn harder, with the price attached:</p>
<blockquote>
<p>KDBX 4&#39;s KDF runs in the <strong>backend</strong>, not the webview. […] Do not move it back into the webview.</p>
</blockquote>
<blockquote>
<p>The frontend has <strong>no direct filesystem access</strong>; <code>saveDatabase</code> serializes the db and calls the Rust <code>save_database</code> command.</p>
</blockquote>
<p>Which closes the loop on the archive of mistakes: the rules I quoted earlier exist because of exactly this problem. The agent does not remember architectural decisions. The repository does.</p>
<h2>What it comes down to</h2>
<p>The split is simple. I own the spec, the boundaries — both trust and architectural — and the critical parts of the code. The agent owns the volume of work inside those boundaries. Tests and the linter make sure I never have to take its word for it.</p>
<p>All of it reduces to one principle. An agent has no memory between sessions, so the project&#39;s memory has to live in the project: a numbered spec the code can cite, and a rules file that grows out of every expensive mistake. Whatever is not written down will be decided again — and differently next time.</p>
<p>How I pick models for this kind of work, and why I do not trust leaderboards, is on <a href="/ai/">the AI page</a> and in <a href="https://github.com/w512/Prompt-Vault">Prompt Vault</a>.</p>
]]></content:encoded>
		</item>
		<item>
			<title>&quot;Cross-Platform&quot; Means Three Separate Platforms: Shipping a Tauri 2 App</title>
			<link>https://blokhin.us/notes/tauri-three-platforms/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/tauri-three-platforms/</guid>
			<pubDate>Sat, 25 Jul 2026 00:00:00 +0000</pubDate>
			<description>My Tauri 2 AppImage died with EGL_BAD_PARAMETER on recent Mesa: bundled graphics libraries. The fix lives in the release pipeline — and wasn't the only one.</description>
			<content:encoded><![CDATA[<p><a href="/texodus/">Texodus</a> is a Markdown editor built with Tauri 2 and Vue 3, running from one codebase on macOS, Windows, and Linux. The cross-platform promise holds perfectly — right up to the point where the code is written. Then comes delivery, and there is no shared platform there at all: there are three independent sets of problems, each with its own way of breaking on a user&#39;s machine rather than on mine.</p>
<p>Here is what shipping Texodus taught me about the distance between &quot;it builds&quot; and &quot;they downloaded it and it started&quot;.</p>
<h2>Linux: it did not start for everyone</h2>
<p>I did not find this one myself. It arrived as a bug report on GitHub: the AppImage would not launch at all, dying on graphics initialization. On my machine, and on plenty of distributions, everything worked — exactly the kind of symptom you cannot reproduce where you develop.</p>
<pre><code>Could not create default EGL display: EGL_BAD_PARAMETER
</code></pre>
<p>The cause was not in the application code but in the packaging. <code>linuxdeploy</code> faithfully bundles the graphics client libraries of the machine that built the app — in my case Ubuntu 22.04 on GitHub Actions. At launch they are loaded ahead of the host&#39;s own copies, and then talk to the <em>host&#39;s</em> Mesa driver. As long as the versions stay close, this works. On recent Mesa — the report came from Arch with RDNA4 — WebKitGTK cannot initialize EGL through that mismatched pair, and the window simply never opens.</p>
<p>The reasoning is the same as behind AppImage&#39;s standard excludelist: the graphics stack has to come from the host, not from the bundle. Which means those libraries have to leave the bundle.</p>
<h2>The fix belongs in the pipeline</h2>
<p>Fixing this by hand once would be useless — the next release would rebuild it exactly the same way. So the GitHub Actions workflow got a step that runs after the build: unpack the AppImage, delete what should not be there, and pack it back up.</p>
<pre><code class="language-bash">find squashfs-root \( \
  -name &#39;libwayland-*.so*&#39; -o \
  -name &#39;libgbm.so*&#39; -o \
  -name &#39;libdrm*.so*&#39; -o \
  -name &#39;libEGL.so*&#39; -o \
  -name &#39;libGL.so*&#39; -o \
  -name &#39;libGLX*.so*&#39; -o \
  -name &#39;libGLdispatch.so*&#39; \
\) -print -delete
</code></pre>
<p>Then came three details, each of which cost its own round trip:</p>
<ul>
<li><strong>No FUSE on the runner.</strong> Extraction goes through <code>--appimage-extract</code>, and <code>appimagetool</code> itself runs with <code>--appimage-extract-and-run</code>.</li>
<li><strong>The <code>appimagetool</code> download was dead.</strong> Assets on the old AppImageKit releases return 404, so the step pulls from the maintained <code>AppImage/appimagetool</code> repository instead, at a pinned version.</li>
<li><strong>The asset was already uploaded.</strong> <code>tauri-action</code> publishes the AppImage before this step runs, so the finished file has to be swapped through the API: delete the old release asset, upload the repacked one.</li>
</ul>
<p>None of this follows from the Tauri documentation. All three showed up on live releases.</p>
<h2>macOS: not even in CI</h2>
<p>The Texodus build matrix has two runners: <code>ubuntu-22.04</code> and <code>windows-latest</code>. macOS is not there at all — those builds run locally, from a separate script.</p>
<p>The reason is signing. A shippable DMG needs a Developer ID certificate from the local keychain and notarization with Apple, which wants an app-specific password and a team ID. On top of that, a universal binary means two Rust targets (<code>x86_64-apple-darwin</code> and <code>aarch64-apple-darwin</code>) built and merged. All of this can be moved into CI — at the price of spreading certificates and secrets across someone else&#39;s infrastructure. For a project I run alone, a local script turned out to be the honest trade.</p>
<p>The result is one application with two different release paths: Linux and Windows build in the cloud at the push of a button, macOS builds on my Mac.</p>
<h2>One feature, three mechanisms</h2>
<p>The most underrated part of cross-platform work is not packaging. It is that identical behavior has to be implemented differently.</p>
<p>Take opening a file by double-clicking it in the file manager. On macOS the path arrives as a <code>RunEvent::Opened</code> event. On Windows and Linux it arrives as a command-line argument, plus a single-instance plugin so that a second launch hands the file to the running process instead of starting a second application.</p>
<p>The macOS branch also has a race that does not physically exist on the others: <code>Opened</code> can fire before the main window has been created. Until that was handled, double-clicking a <code>.md</code> file while the editor was closed opened two windows — one empty, one with the file. The routing logic saw no window and read that as a signal to spawn one.</p>
<p>The same category includes features that simply do not exist elsewhere. In <a href="/kivarion/">Kivarion</a>, Touch ID unlock is macOS-only, and reporting an honest &quot;not supported&quot; on Windows and Linux is as much a part of cross-platform work as the feature itself.</p>
<h2>What I took away</h2>
<ul>
<li><strong>Test the release artifact, not the build.</strong> The EGL problem lived neither in the code nor in a dev build — only in the packaged AppImage, on someone else&#39;s system. If you do not test that, a user will, and then they will write to you about it.</li>
<li><strong>&quot;Works on my machine&quot; means nothing on Linux.</strong> Distributions differ in graphics stack versions more than you expect, and it is the newest environment that breaks, not the oldest.</li>
<li><strong>A platform fix has to be a pipeline step.</strong> Anything repaired by hand after the build is lost on the next release.</li>
<li><strong>Plan for different release paths.</strong> Apple&#39;s signing requirements differ enough that a single pipeline for all three platforms stops being an obvious goal.</li>
</ul>
<p>&quot;Cross-platform&quot; is true about the code. About delivery, it is three separate projects that happen to share a repository.</p>
<p>Texodus is open source: <a href="https://github.com/w512/Texodus">repository</a> and <a href="https://github.com/w512/Texodus/releases">releases</a>.</p>
]]></content:encoded>
		</item>
		<item>
			<title>FAANG Experience Is No Longer an Elite Engineer Badge</title>
			<link>https://blokhin.us/notes/faang-experience/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/faang-experience/</guid>
			<pubDate>Sun, 05 Jul 2026 00:00:00 +0000</pubDate>
			<description>Narrow roles, closed internal stacks, and golden handcuffs: why a Big Tech line on a résumé stopped being an automatic quality signal for hiring managers.</description>
			<content:encoded><![CDATA[<p>Ten years ago, a line saying Google, Meta, or Apple on a résumé was an absolute quality signal — a free pass into almost any company in the world. Today, when I see that line as the person doing the hiring, what I feel is more complicated. Judging by conversations with other hiring managers, I am not alone.</p>
<p>Let me be upfront about where I am standing. I have never worked at a FAANG company myself. But in twenty-plus years in this industry I have worked alongside plenty of people who did, a good number of my friends and former colleagues built careers there, and I have interviewed and hired candidates coming out of those companies. This is the view from the other side of the table — the side where the brand on the résumé stopped answering the only question that matters: what can this engineer actually build?</p>
<h2>The role narrows to a single component</h2>
<p>At a company with hundreds of thousands of employees, work has to be split into very small pieces. That is not a flaw in anyone&#39;s character — it is what scale demands. But instead of designing a system or taking a service from zero to production, an engineer can spend years maintaining one narrow component of one internal platform. Depth in that component grows. The view of the system as a whole quietly atrophies.</p>
<p>The result for a career is the same either way: after a few years you know something brilliantly, and that something exists only inside one company.</p>
<h2>A closed stack does not transfer</h2>
<p>The largest companies have always built their own ecosystems: their own frameworks, build systems, version control, deployment tooling. Inside, these are often excellent — years ahead of what the rest of the market has. The problem is not quality. It is portability. Expertise in an internal platform is worth very little the moment you walk off the campus.</p>
<p>There is an honest complication here worth stating. A significant part of today&#39;s open stack consists of public descendants of exactly those internal systems: Kubernetes grew out of the experience of Borg, Bazel is the open version of Blaze. The argument is not that Big Tech has bad tools. The argument is that the market runs on the public versions, and an engineer who spent four or five years on the internal ones tends to discover that the ecosystem moved on without them — from cloud providers to orchestrators to frameworks.</p>
<h2>Golden handcuffs</h2>
<p>High salaries and equity grants build a standard of living that is hard to maintain anywhere else. The price for that comfort is often routine work wrapped in process. Colleagues describe simple changes spending weeks in approvals that a small team would not need at all. Engineering initiative does not die suddenly in that environment. It just stops paying off.</p>
<h2>What Big Tech still teaches better than anyone</h2>
<p>I am not interested in building a straw man, so let me be clear about the other side. Some things scale teaches better than any startup can: operating systems where a failure reaches millions of people, a real code review culture and the discipline that comes with it, the skill of changing a running system without knocking it over. And for researchers and R&amp;D engineers pushing fundamental work, those labs are still the best places on the planet.</p>
<p>Choosing stability is a legitimate choice too. Trading some speed of professional growth for comfort and predictability is an adult decision, not a mistake — as long as it is actually a decision. The failure mode is the cargo cult: treating &quot;getting into the corporate elite&quot; as the goal itself.</p>
<h2>What to do about it</h2>
<p>If you are an engineer inside a big company, do not let the internal platform become the only thing you know. Side projects, open source, a public trail — anything that lives outside the campus and can be checked from outside it.</p>
<p>If you are hiring, look past the brand at what the person has built and what you can verify: shipped products, breadth of stack, a visible trail in open code. The brand on a résumé answers the question of where someone was once hired. It stopped answering the question of what they can build a while ago.</p>
]]></content:encoded>
		</item>
	</channel>
</rss>
