<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>zemnanet</title><link>https://zemna.net/</link><description>Recent content on zemnanet</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sat, 10 Oct 2026 07:00:00 +0700</lastBuildDate><atom:link href="https://zemna.net/index.xml" rel="self" type="application/rss+xml"/><item><title>A Withheld Plugin Result Is Not the Tool Never Ran</title><link>https://zemna.net/blog/withheld-plugin-result-is-not-the-tool-never-ran/</link><pubDate>Sat, 10 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/withheld-plugin-result-is-not-the-tool-never-ran/</guid><description>&lt;p>The junior pastes a chat. The agent asked for &lt;code>Bash(git status)&lt;/code>. A plugin line then says Denied. The tool result is empty. They file the ticket: “the tool never ran. always-ask blocked it.”&lt;/p>
&lt;p>I stop the run there. A withheld plugin result is not the tool never ran. Claude Code’s public notes that named this field are blunt: when a mod denies a tool call after the tool ran, Claude and you now read that the tool ran and a plugin withheld its result. The tool executed. The result was held. The empty pane is not a skip. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.295] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a UNC path read as a local allow in &lt;a href="https://zemna.net/blog/unc-path-read-is-not-a-local-allow/">A UNC Path Read Is Not a Local Allow&lt;/a>, a leftover &lt;code>allowed-tools&lt;/code> rule as you still in plan mode in &lt;a href="https://zemna.net/blog/leftover-allowed-tools-is-not-plan-mode-stuck/">A Leftover Allowed-Tools Rule Is Not You Still in Plan Mode&lt;/a>, and a repeated reply ID as a second approval in &lt;a href="https://zemna.net/blog/repeated-reply-id-is-not-a-second-approval/">A Repeated Reply ID Is Not a Second Approval&lt;/a>. This post is the same desk rule for a denied tool with no result. Print withheld versus never ran. Name who owns the Claude Code answers.&lt;/p>
&lt;p>The question is not whether the pane stayed empty. The question is whether the named owner can still tell a withheld plugin result from a tool that never ran.&lt;/p></description></item><item><title>A UNC Path Read Is Not a Local Allow</title><link>https://zemna.net/blog/unc-path-read-is-not-a-local-allow/</link><pubDate>Fri, 09 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/unc-path-read-is-not-a-local-allow/</guid><description>&lt;p>The junior approved &lt;code>Read&lt;/code> on &lt;code>C:\work\app\config.yaml&lt;/code>. The next turn reads &lt;code>\\fileserver\share\app\config.yaml&lt;/code> with no dialog. Chat files the ticket: “always-ask is off. the sandbox already allowed it.”&lt;/p>
&lt;p>I stop the run there. A UNC path read is not a local allow. Claude Code’s public notes that named this field are blunt: PreToolUse hook approvals and auto mode used to bypass the permission prompt for file reads from network (UNC) paths. Official permissions docs already say a command whose arguments include a network (UNC) path prompts, because looking that path up can send Windows credentials to the host it names. The human approved a local spelling. The second path is not that allow. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.292] [Source: https://code.claude.com/docs/en/permissions]&lt;/p>
&lt;p>I already refused to treat a leftover &lt;code>allowed-tools&lt;/code> rule as you still in plan mode in &lt;a href="https://zemna.net/blog/leftover-allowed-tools-is-not-plan-mode-stuck/">A Leftover Allowed-Tools Rule Is Not You Still in Plan Mode&lt;/a>, a repeated reply ID as a second approval in &lt;a href="https://zemna.net/blog/repeated-reply-id-is-not-a-second-approval/">A Repeated Reply ID Is Not a Second Approval&lt;/a>, and sandbox auto-allow as a retry tax on equals in &lt;a href="https://zemna.net/blog/sandbox-auto-allow-is-not-a-retry-tax-on-equals/">Sandbox Auto-Allow Is Not a Retry Tax on Equals in Inline Scripts&lt;/a>. This post is the same desk rule for a network-shaped read. Print the path class. Print whether the allow was local. Name who owns the Claude Code answers.&lt;/p>
&lt;p>The question is not whether the prompt stayed quiet. The question is whether the named owner can still tell a UNC path read from a local allow.&lt;/p></description></item><item><title>A Leftover Allowed-Tools Rule Is Not You Still in Plan Mode</title><link>https://zemna.net/blog/leftover-allowed-tools-is-not-plan-mode-stuck/</link><pubDate>Thu, 08 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/leftover-allowed-tools-is-not-plan-mode-stuck/</guid><description>&lt;p>The junior hits Shift+Tab. The status bar leaves plan mode. The next turn still runs &lt;code>Bash&lt;/code> from a skill with no prompt. Chat files the ticket: “plan mode is still on. always-ask is off.”&lt;/p>
&lt;p>I stop the run there. A leftover &lt;code>allowed-tools&lt;/code> rule is not you still in plan mode. Claude Code’s public notes that named this field are blunt: a skill’s or slash command’s &lt;code>allowed-tools&lt;/code> rule used to come back in a later turn when you leave auto mode or plan mode partway through that turn. Official skills docs already say the grant is for the invoking turn and clears when you send the next message. The human left the mode. The second turn is not a stuck plan. [Source: https://code.claude.com/docs/en/changelog] [Source: https://code.claude.com/docs/en/skills]&lt;/p>
&lt;p>I already refused to treat a repeated reply ID as a second approval in &lt;a href="https://zemna.net/blog/repeated-reply-id-is-not-a-second-approval/">A Repeated Reply ID Is Not a Second Approval&lt;/a>, a nested deny as a mod approval in &lt;a href="https://zemna.net/blog/nested-deny-is-not-a-mod-approval/">A Nested Deny Is Not a Mod Approval&lt;/a>, and an allowed-mail list as a Cloudflare account in &lt;a href="https://zemna.net/blog/allowed-mail-is-not-a-cloudflare-account/">An Allowed-Mail List Is Not a Cloudflare Account&lt;/a>. This post is the same desk rule for a leftover skill pass. Print the &lt;code>allowed-tools&lt;/code> line. Print whether the session already left plan or auto. Name who owns the Claude Code answers.&lt;/p>
&lt;p>The question is not whether the status bar looked quiet. The question is whether the named owner can still tell a leftover &lt;code>allowed-tools&lt;/code> rule from a stuck plan.&lt;/p></description></item><item><title>A Repeated Reply ID Is Not a Second Approval</title><link>https://zemna.net/blog/repeated-reply-id-is-not-a-second-approval/</link><pubDate>Wed, 07 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/repeated-reply-id-is-not-a-second-approval/</guid><description>&lt;p>The junior pastes a phone screenshot. They already answered &lt;code>yes kqmpw&lt;/code> on Telegram. A second Bash row ran. Chat files the ticket: “always-ask is off. I never approved that command.”&lt;/p>
&lt;p>I stop the run there. A repeated reply ID is not a second approval. Claude Code’s public notes that named this field are blunt: permission relay on &lt;code>--channels&lt;/code> used to treat a reply ID that repeats inside one session as a verdict on a different prompt. The field now ignores that repeat. The human still answered one ID. The second tool is not a second allow. [Source: https://code.claude.com/docs/en/changelog] [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.290]&lt;/p>
&lt;p>I already refused to treat an allowed-mail list as a Cloudflare account in &lt;a href="https://zemna.net/blog/allowed-mail-is-not-a-cloudflare-account/">An Allowed-Mail List Is Not a Cloudflare Account&lt;/a>, a nested deny as a mod approval in &lt;a href="https://zemna.net/blog/nested-deny-is-not-a-mod-approval/">A Nested Deny Is Not a Mod Approval&lt;/a>, and a 40-character &lt;code>ghs_&lt;/code> check as a valid App token in &lt;a href="https://zemna.net/blog/ghs-length-is-not-a-valid-app-token/">A 40-Character ghs_ Check Is Not a Valid App Token&lt;/a>. This post is the same desk rule for a channel yes. Print the reply ID. Print whether that ID is still an open request. Name who owns the Claude Code answers.&lt;/p>
&lt;p>The question is not whether the phone said yes. The question is whether the named owner can still tell a repeated reply ID from a second approval.&lt;/p></description></item><item><title>An Allowed-Mail List Is Not a Cloudflare Account</title><link>https://zemna.net/blog/allowed-mail-is-not-a-cloudflare-account/</link><pubDate>Tue, 06 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/allowed-mail-is-not-a-cloudflare-account/</guid><description>&lt;p>The junior pastes a screenshot. The coding agent shared a &lt;code>trycloudflare.com&lt;/code> URL. The visitor typed an email, hit a Cloudflare Access page, and stalled. Chat files the ticket: “the visitor needs a Cloudflare account. Access is misconfigured.”&lt;/p>
&lt;p>I stop the run there. An allowed-mail list is not a Cloudflare account. Cloudflare’s public notes that named this field are blunt: &lt;code>--allowed-mail&lt;/code> on &lt;code>cloudflared&lt;/code> restricts a Quick Tunnel to the email addresses and domains you list. Visitors authenticate with a one-time PIN. Nobody on either side needs a Cloudflare account. Access ends when you stop the process. That is email OTP on a Quick Tunnel. It is not production Access on a named Tunnel. It is not a dashboard login. [Source: https://blog.cloudflare.com/protected-quick-tunnels/] [Source: https://developers.cloudflare.com/changelog/post/2026-10-02-protected-quick-tunnels/]&lt;/p>
&lt;p>I already refused to treat a nested deny as a mod approval in &lt;a href="https://zemna.net/blog/nested-deny-is-not-a-mod-approval/">A Nested Deny Is Not a Mod Approval&lt;/a>, a 40-character &lt;code>ghs_&lt;/code> check as a valid App token in &lt;a href="https://zemna.net/blog/ghs-length-is-not-a-valid-app-token/">A 40-Character ghs_ Check Is Not a Valid App Token&lt;/a>, and a redirected dangerous &lt;code>rm&lt;/code> as always-ask off in &lt;a href="https://zemna.net/blog/print-the-redirect-before-you-file-always-ask-off/">Print the Redirect Before You File Always-Ask Off&lt;/a>. This post is the same desk rule for a shared preview URL. Print the guest list. Print whether a Cloudflare account exists. Name who owns the tunnel answers.&lt;/p>
&lt;p>The question is not whether the Access page looked official. The question is whether the named owner can still tell an allowed-mail list from a Cloudflare account.&lt;/p></description></item><item><title>A Nested Deny Is Not a Mod Approval</title><link>https://zemna.net/blog/nested-deny-is-not-a-mod-approval/</link><pubDate>Mon, 05 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/nested-deny-is-not-a-mod-approval/</guid><description>&lt;p>The junior pastes a session log. The managed machine has &lt;code>Bash(git clean *)&lt;/code> on deny, or &lt;code>Bash(rm *)&lt;/code> on ask. The agent ran &lt;code>cd /tmp &amp;amp;&amp;amp; git clean -fd&lt;/code>. A user-installed mod printed Approved on the inner command. They file the ticket: “deny is off. The managed allow won.”&lt;/p>
&lt;p>I stop the run there. A nested deny is not a mod approval. Anthropic’s public notes that named this field are blunt: a deny or ask rule on a nested part of a compound shell command did not hold over a user-installed mod’s approval on managed machines. The inner command still had a rule. The approval source was a user-installed mod, not the named owner of Claude Code answers. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.289] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a 40-character &lt;code>ghs_&lt;/code> check as a valid App token in &lt;a href="https://zemna.net/blog/ghs-length-is-not-a-valid-app-token/">A 40-Character ghs_ Check Is Not a Valid App Token&lt;/a>. I already refused to treat a redirected dangerous &lt;code>rm&lt;/code> as always-ask off in &lt;a href="https://zemna.net/blog/print-the-redirect-before-you-file-always-ask-off/">Print the Redirect Before You File Always-Ask Off&lt;/a>. This post is the same desk rule for a compound shell. Print the nested rule. Print the approval source. Name who owns the Claude Code answers.&lt;/p>
&lt;p>The question is not whether the inner command ran. The question is whether the named owner can still tell a nested deny from a plugin that approved the inner piece.&lt;/p></description></item><item><title>A 40-Character ghs_ Check Is Not a Valid App Token</title><link>https://zemna.net/blog/ghs-length-is-not-a-valid-app-token/</link><pubDate>Sun, 04 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/ghs-length-is-not-a-valid-app-token/</guid><description>&lt;p>The junior pastes a CI log. The GitHub App minted a &lt;code>ghs_&lt;/code> string. The helper printed &lt;code>invalid token: expected 40 characters&lt;/code>. They file the ticket: “the App token is broken.”&lt;/p>
&lt;p>I stop the run there. A 40-character &lt;code>ghs_&lt;/code> check is not a valid App token. GitHub’s public notes that named this field are blunt: newly minted GitHub App installation tokens still start with &lt;code>ghs_&lt;/code>, and they are now about 520 characters instead of 40. Permissions, repository scoping, the one-hour expiry, and the installation-token REST endpoint did not change. [Source: https://github.blog/changelog/2026-10-02-stateless-github-app-installation-tokens-rolled-out/] [Source: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app]&lt;/p>
&lt;p>I already refused to treat a redirected dangerous &lt;code>rm&lt;/code> as always-ask off in &lt;a href="https://zemna.net/blog/print-the-redirect-before-you-file-always-ask-off/">Print the Redirect Before You File Always-Ask Off&lt;/a>. I already refused to treat a laravel/ai lockfile line as a patched fetch client in &lt;a href="https://zemna.net/blog/a-laravel-ai-lockfile-line-is-not-a-patched-fetch-client/">A Laravel/AI Lockfile Line Is Not a Patched Fetch Client&lt;/a>. I already refused to treat Copilot’s ready-to-approve line as a required merge vote in &lt;a href="https://zemna.net/blog/leave-copilot-approve-off/">Leave Copilot Approve Off&lt;/a>. This post is the same desk rule for a GitHub App installation token. Print the length check. Print whether the string was treated as opaque. Name who owns the App answers.&lt;/p>
&lt;p>The question is not whether the string is 40 characters. The question is whether the named owner can still tell a length check written for the legacy format from a token that GitHub already minted.&lt;/p></description></item><item><title>Print the Redirect Before You File Always-Ask Off</title><link>https://zemna.net/blog/print-the-redirect-before-you-file-always-ask-off/</link><pubDate>Sat, 03 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/print-the-redirect-before-you-file-always-ask-off/</guid><description>&lt;p>The junior pastes a terminal screenshot. The coding agent ran a delete. There was no always-ask prompt. They file the ticket: “always-ask is off, so this rm is allowed.”&lt;/p>
&lt;p>I stop the run there. Print the full command, including the redirect, before you file always-ask off. The public Claude Code notes that named this field are blunt: a dangerous &lt;code>rm&lt;/code> on &lt;code>/&lt;/code> or the home directory lost its always-ask safeguard when the same command also redirected output to a &lt;code>~&lt;/code> or wildcard path. That is a redirect on the same line, not a named allow and not a missing setting. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.287] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a NUL byte in a permission rule as a wildcard allow in &lt;a href="https://zemna.net/blog/a-nul-byte-in-a-permission-rule-is-not-a-wildcard/">A NUL Byte in a Permission Rule Is Not a Wildcard Allow&lt;/a>. I already refused to treat sandbox auto-allow as a retry tax on equals in &lt;a href="https://zemna.net/blog/sandbox-auto-allow-is-not-a-retry-tax-on-equals/">Sandbox Auto-Allow Is Not a Retry Tax on Equals in Inline Scripts&lt;/a>. I already refused to treat Copilot’s ready-to-approve line as a required merge vote in &lt;a href="https://zemna.net/blog/leave-copilot-approve-off/">Leave Copilot Approve Off&lt;/a>. This post is the same desk rule for always-ask on a dangerous delete. Print the redirect. Print whether always-ask still fired. Name who owns the always-ask answers.&lt;/p>
&lt;p>The question is not whether the prompt appeared. The question is whether the named owner can still tell a tilde redirect from a setting that is actually off.&lt;/p></description></item><item><title>A Laravel/AI Lockfile Line Is Not a Patched Fetch Client</title><link>https://zemna.net/blog/a-laravel-ai-lockfile-line-is-not-a-patched-fetch-client/</link><pubDate>Fri, 02 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/a-laravel-ai-lockfile-line-is-not-a-patched-fetch-client/</guid><description>&lt;p>The junior pastes &lt;code>composer.lock&lt;/code>. The file contains &lt;code>laravel/ai&lt;/code>. Dependabot is green. The ticket says “AI package is patched.”&lt;/p>
&lt;p>I stop the run there. A laravel/ai lockfile line is not a patched fetch client. The public advisory that named this field is blunt: the Vercel adapter and the AG-UI adapter in laravel/ai 1.0.0 accepted a client-supplied file URL and fetched it on the server without a guard. That is one package, one pair of adapters, one fetch client. It is not laravel/mcp. It is not framework 13.34. It is not “Composer found a line, so the chat endpoint is safe.” [Source: https://github.com/laravel/ai/security/advisories/GHSA-6qhr-3g93-pxhw] [Source: https://laravel-news.com/laravel-ai-mcp-security-advisories]&lt;/p>
&lt;p>I already refused to treat a 2,500+ Actions count as an exact run inventory in &lt;a href="https://zemna.net/blog/a-2500-plus-actions-count-is-not-an-exact-run-inventory/">A 2,500+ Actions Count Is Not an Exact Run Inventory&lt;/a>. I already refused to treat a NUL byte in a permission rule as a wildcard allow in &lt;a href="https://zemna.net/blog/a-nul-byte-in-a-permission-rule-is-not-a-wildcard/">A NUL Byte in a Permission Rule Is Not a Wildcard Allow&lt;/a>. I already refused to treat Copilot’s ready-to-approve line as a required merge vote in &lt;a href="https://zemna.net/blog/leave-copilot-approve-off/">Leave Copilot Approve Off&lt;/a>. This post is the same desk rule for a Composer lock line. Print the package. Print the locked version. Name who owns the Laravel answers.&lt;/p>
&lt;p>The question is not whether the lockfile looks patched. The question is whether the named owner can tell this fetch client from a different advisory and from a framework bump.&lt;/p></description></item><item><title>Sandbox Auto-Allow Is Not a Retry Tax on Equals in Inline Scripts</title><link>https://zemna.net/blog/sandbox-auto-allow-is-not-a-retry-tax-on-equals/</link><pubDate>Thu, 01 Oct 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/sandbox-auto-allow-is-not-a-retry-tax-on-equals/</guid><description>&lt;p>The junior pastes a terminal screenshot. Sandbox is on. Auto-allow is on. The coding agent ran &lt;code>python3 -c &amp;quot;x=1&amp;quot;&lt;/code>. Claude Code asked for approval. They file the ticket: “auto-allow is off.”&lt;/p>
&lt;p>I stop the run there. Sandbox auto-allow is not a retry tax on equals in inline scripts. The public Claude Code notes that named this field are blunt: sandbox auto-allow asked for approval on every run of many inline scripts (&lt;code>python3 -c&lt;/code>, &lt;code>node -e&lt;/code>) just because they contain &lt;code>=&lt;/code>. That is a matcher on the equals sign, not a missing allow. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.285] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a NUL byte in a permission rule as a wildcard allow in &lt;a href="https://zemna.net/blog/a-nul-byte-in-a-permission-rule-is-not-a-wildcard/">A NUL Byte in a Permission Rule Is Not a Wildcard Allow&lt;/a>. I already refused to treat a present &lt;code>AGENTS.md&lt;/code> as the project instructions while &lt;code>CLAUDE.md&lt;/code> exists in &lt;a href="https://zemna.net/blog/agents-md-is-not-the-project-instructions/">A Present AGENTS.md Is Not the Project Instructions While CLAUDE.md Exists&lt;/a>. I already refused to treat Copilot’s ready-to-approve line as a required merge vote in &lt;a href="https://zemna.net/blog/leave-copilot-approve-off/">Leave Copilot Approve Off&lt;/a>. This post is the same desk rule for sandbox auto-allow. Print the command. Print whether it contains &lt;code>=&lt;/code>. Name who owns the auto-allow answers.&lt;/p>
&lt;p>The question is not whether the prompt looks like a broken allow. The question is whether the named owner can tell an equals matcher from a setting that is actually off.&lt;/p></description></item><item><title>A Moved Self-Hosted Runner Deadline Is Not an All-Clear</title><link>https://zemna.net/blog/a-moved-runner-deadline-is-not-an-all-clear/</link><pubDate>Wed, 30 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/a-moved-runner-deadline-is-not-an-all-clear/</guid><description>&lt;p>Standup hears “the date moved.” Someone pasted the GitHub Changelog from 28 September 2026. The coding agent pasted the headline and filed all-clear: “we have extra time; skip the fleet this week.”&lt;/p>
&lt;p>I stop the run there. A moved self-hosted runner deadline is not an all-clear. Official GitHub changelog for 28 September 2026 is blunt: the enforcement date for GitHub Actions minimum version requirements for self-hosted runners on GitHub Enterprise Cloud changed. The change ships Monday 28 September 2026. Full enforcement begins Tuesday 29 September 2026. That is instead of the date previously announced. The requirements themselves are unchanged. Today is Wednesday 30 September 2026. The extra day already ended. [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/]&lt;/p>
&lt;p>I already refused to treat a 2,500+ Actions badge as an exact run inventory in &lt;a href="https://zemna.net/blog/a-2500-plus-actions-count-is-not-an-exact-run-inventory/">A 2,500+ Actions Count Is Not an Exact Run Inventory&lt;/a>. I already refused to treat &lt;code>ubuntu-latest&lt;/code> as a runner image I already tested in &lt;a href="https://zemna.net/blog/ubuntu-latest-is-not-a-tested-runner-image/">Ubuntu-latest Is Not a Runner Image You Already Tested&lt;/a>. I already refused to treat a timed-out Laravel job as a killed worker pool in &lt;a href="https://zemna.net/blog/a-timed-out-job-is-not-a-killed-worker/">A Timed-Out Job Is Not a Killed Worker&lt;/a>. This post is the same desk rule for a moved date. Print the moved date. Print both floors. Name who owns the self-hosted fleet.&lt;/p>
&lt;p>The question is not whether a moved date demos well in a screenshot. The question is whether the named owner can tell a moved date from a refused job before paging CI.&lt;/p></description></item><item><title>A 2,500+ Actions Count Is Not an Exact Run Inventory</title><link>https://zemna.net/blog/a-2500-plus-actions-count-is-not-an-exact-run-inventory/</link><pubDate>Tue, 29 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/a-2500-plus-actions-count-is-not-an-exact-run-inventory/</guid><description>&lt;p>Standup hears “we have 2,500 runs.” Someone pasted the Actions tab. The coding agent pasted &lt;code>total_count&lt;/code> from &lt;code>GET /repos/{owner}/{repo}/actions/runs&lt;/code>. The junior files missing-runs: “the rest of the history vanished.”&lt;/p>
&lt;p>I stop the run there. A 2,500+ Actions count is not an exact run inventory. Official GitHub changelog for 25 September 2026 is blunt: queries for workflow runs in the Actions API and UI still paginate up to 1,000 items, and if the number of found records exceeds 2,500, GitHub reports “2,500+” instead of an exact number. Larger queries often timed out and returned however many records had been counted before the timeout. The cap is the honest number. The exact inventory is a different ticket. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/]&lt;/p>
&lt;p>I already refused to treat a timed-out Laravel job as a killed worker pool in &lt;a href="https://zemna.net/blog/a-timed-out-job-is-not-a-killed-worker/">A Timed-Out Job Is Not a Killed Worker&lt;/a>. I already refused to treat &lt;code>ubuntu-latest&lt;/code> as a runner image I already tested in &lt;a href="https://zemna.net/blog/ubuntu-latest-is-not-a-tested-runner-image/">Ubuntu-latest Is Not a Runner Image You Already Tested&lt;/a>. I already refused to treat an auto-started Codex background server as compatible settings in &lt;a href="https://zemna.net/blog/auto-start-is-not-compatible-settings/">An Auto-Started Codex Background Server Is Not Compatible Settings&lt;/a>. This post is the same desk rule for Actions history. Print the cap. Print the page. Name who owns the workflow-run query.&lt;/p>
&lt;p>The question is not whether a dashboard demos a big number. The question is whether the named owner can tell a capped count from a missing run before paging CI.&lt;/p></description></item><item><title>An Auto-Started Codex Background Server Is Not Compatible Settings</title><link>https://zemna.net/blog/auto-start-is-not-compatible-settings/</link><pubDate>Mon, 28 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/auto-start-is-not-compatible-settings/</guid><description>&lt;p>The junior pastes a terminal. &lt;code>codex&lt;/code> opened. Chat is quiet. They file an all-clear: “the background server is compatible.”&lt;/p>
&lt;p>I stop the run there. An auto-started Codex background server is not compatible settings. The public Codex CLI notes that named this field are blunt: automatic background-server startup is enabled for eligible interactive sessions, with recovery choices when server settings are incompatible. That is a launch path. It is not a signed match between this session and the shared server. [Source: https://github.com/openai/codex/releases/tag/rust-v0.157.0] [Source: https://github.com/openai/codex/pull/47318]&lt;/p>
&lt;p>I already refused to treat a green wrangler deploy as an immediate Durable Object cutover in &lt;a href="https://zemna.net/blog/green-deploy-is-not-do-cutover/">A Green Wrangler Deploy Is Not an Immediate Durable Object Code Cutover&lt;/a>, a &lt;code>/readyz&lt;/code> 200 through a Postgres blip as a healthy store in &lt;a href="https://zemna.net/blog/a-readyz-200-is-not-a-healthy-store/">A /readyz 200 Through a Postgres Blip Is Not a Healthy Store&lt;/a>, and a timed-out job as a killed worker in &lt;a href="https://zemna.net/blog/a-timed-out-job-is-not-a-killed-worker/">A Timed-Out Job Is Not a Killed Worker&lt;/a>. This post is the same desk rule for a Codex prompt that opened. Print auto-start. Print the recovery choice. Name who owns the Codex session.&lt;/p>
&lt;p>The question is not whether the CLI looks open. The question is whether the named owner can still tell “eligible launch started a shared server” from “this session’s required settings match that server.”&lt;/p></description></item><item><title>A Green Wrangler Deploy Is Not an Immediate Durable Object Code Cutover</title><link>https://zemna.net/blog/green-deploy-is-not-do-cutover/</link><pubDate>Sun, 27 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/green-deploy-is-not-do-cutover/</guid><description>&lt;p>The junior pastes a terminal. &lt;code>npx wrangler deploy&lt;/code> exits 0. Chat is quiet. They file an all-cutover: “Durable Object code is live.”&lt;/p>
&lt;p>I stop the run there. A green wrangler deploy is not an immediate Durable Object code cutover. The public Workers SDK notes that named this field are blunt: Durable Objects now have code update strategies on &lt;code>wrangler deploy&lt;/code>, &lt;code>wrangler versions deploy&lt;/code>, and &lt;code>wrangler rollback&lt;/code>. When the strategy is unset, it defaults to deferred with a 5-minute maximum delay. Delays cannot exceed 24 hours. That is a Worker bundle that uploaded. It is not every live object already running the new class. [Source: https://github.com/cloudflare/workers-sdk/releases/tag/wrangler%404.141.0] [Source: https://releasebot.io/updates/cloudflare/workers-sdk]&lt;/p>
&lt;p>I already refused to treat a &lt;code>/readyz&lt;/code> 200 through a Postgres blip as a healthy store in &lt;a href="https://zemna.net/blog/a-readyz-200-is-not-a-healthy-store/">A /readyz 200 Through a Postgres Blip Is Not a Healthy Store&lt;/a>. I already refused to treat a timed-out job as a killed worker in &lt;a href="https://zemna.net/blog/a-timed-out-job-is-not-a-killed-worker/">A Timed-Out Job Is Not a Killed Worker&lt;/a>. I already refused to treat every-turn HTTP 400 as a dead gateway in &lt;a href="https://zemna.net/blog/every-turn-http-400-is-not-a-dead-gateway/">Every-Turn HTTP 400 on ANTHROPIC_BASE_URL Is Not a Dead Gateway&lt;/a>. This post is the same desk rule for a green deploy. Print deploy. Print the strategy. Name who owns the Workers cutover.&lt;/p>
&lt;p>The question is not whether the CLI looks green. The question is whether the named owner can still tell “Worker version uploaded” from “Durable Object instances left the old class.”&lt;/p></description></item><item><title>A /readyz 200 Through a Postgres Blip Is Not a Healthy Store</title><link>https://zemna.net/blog/a-readyz-200-is-not-a-healthy-store/</link><pubDate>Sat, 26 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/a-readyz-200-is-not-a-healthy-store/</guid><description>&lt;p>The junior pastes a curl. &lt;code>/readyz&lt;/code> returns 200. Chat is quiet. They file an all-clear: “Postgres is healthy.”&lt;/p>
&lt;p>I stop the run there. A &lt;code>/readyz&lt;/code> 200 through a Postgres blip is not a healthy store. The public Claude Code notes that named this field are blunt: the Claude apps gateway gained &lt;code>store.readiness_grace_seconds&lt;/code> so &lt;code>/readyz&lt;/code> can stay ready through a short Postgres outage such as a database failover. That is a ready bit that held. It is not a store that answered &lt;code>SELECT 1&lt;/code>. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.282] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a NUL byte in a permission rule as a wildcard allow in &lt;a href="https://zemna.net/blog/a-nul-byte-in-a-permission-rule-is-not-a-wildcard/">A NUL Byte in a Permission Rule Is Not a Wildcard Allow&lt;/a>. I already refused to treat a timed-out job as a killed worker in &lt;a href="https://zemna.net/blog/a-timed-out-job-is-not-a-killed-worker/">A Timed-Out Job Is Not a Killed Worker&lt;/a>. I already refused to treat every-turn HTTP 400 as a dead gateway in &lt;a href="https://zemna.net/blog/every-turn-http-400-is-not-a-dead-gateway/">Every-Turn HTTP 400 on ANTHROPIC_BASE_URL Is Not a Dead Gateway&lt;/a>. This post is the same desk rule for a green ready check. Print ready. Print the store. Name who owns the gateway check.&lt;/p>
&lt;p>The question is not whether curl looks green. The question is whether the named owner can still tell “ready to take traffic” from “the store answered.”&lt;/p></description></item><item><title>A NUL Byte in a Permission Rule Is Not a Wildcard Allow</title><link>https://zemna.net/blog/a-nul-byte-in-a-permission-rule-is-not-a-wildcard/</link><pubDate>Fri, 25 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/a-nul-byte-in-a-permission-rule-is-not-a-wildcard/</guid><description>&lt;p>The junior pastes a &lt;code>/permissions&lt;/code> screenshot. A deny rule is in the list. The coding agent still ran the tool. They file an allow-all ticket: “the rule allowed everything.”&lt;/p>
&lt;p>I stop the run there. A NUL byte in a permission rule is not a wildcard allow. The public Claude Code release that named this field is blunt: a permission rule containing a NUL byte was expanded into a wildcard match; such a rule now matches nothing. That is a matcher bug, not proof that deny lost. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.281] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a timed-out job as a killed worker in &lt;a href="https://zemna.net/blog/a-timed-out-job-is-not-a-killed-worker/">A Timed-Out Job Is Not a Killed Worker&lt;/a>. I already refused to treat one matching glob as a sandbox exemption for the rest of the line in &lt;a href="https://zemna.net/blog/one-matching-glob-is-not-a-whole-line-exemption/">One Matching Glob Is Not a Sandbox Exemption for the Rest of the Line&lt;/a>. I already refused to treat a present &lt;code>AGENTS.md&lt;/code> as the project instructions while &lt;code>CLAUDE.md&lt;/code> exists in &lt;a href="https://zemna.net/blog/agents-md-is-not-the-project-instructions/">A Present AGENTS.md Is Not the Project Instructions While CLAUDE.md Exists&lt;/a>. This post is the same desk rule for permission bytes. Print the bytes. Print the match. Name who owns the permission list.&lt;/p>
&lt;p>The question is not whether a deny line looks present in JSON. The question is whether the named owner can tell a wildcard from a rule that matches nothing.&lt;/p></description></item><item><title>A Timed-Out Job Is Not a Killed Worker</title><link>https://zemna.net/blog/a-timed-out-job-is-not-a-killed-worker/</link><pubDate>Thu, 24 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/a-timed-out-job-is-not-a-killed-worker/</guid><description>&lt;p>The junior pastes a Supervisor screenshot. One &lt;code>queue:work&lt;/code> line vanished. The failed_jobs table is empty. Horizon still shows a running supervisor. They file a pool outage: “the workers are dead.”&lt;/p>
&lt;p>I stop the run there. A timed-out job is not a killed worker. Official Laravel queue docs are blunt: the default timeout is 60 seconds, and if a job runs longer than that number, the worker processing that job exits with an error. A process manager on the server then starts another worker. That is one job hitting a clock, not a pool that died. [Source: https://laravel.com/docs/13.x/queues]&lt;/p>
&lt;p>I already refused to treat one matching glob as a sandbox exemption for the rest of the line in &lt;a href="https://zemna.net/blog/one-matching-glob-is-not-a-whole-line-exemption/">One Matching Glob Is Not a Sandbox Exemption for the Rest of the Line&lt;/a>. I already refused to treat a hostname handed to a forward proxy as a missing host in &lt;a href="https://zemna.net/blog/hostname-handed-to-proxy-is-not-a-missing-host/">Handing the Hostname to the Forward Proxy Is Not a Missing Host&lt;/a>. I already refused to treat &lt;code>ubuntu-latest&lt;/code> as a runner image I already tested in &lt;a href="https://zemna.net/blog/ubuntu-latest-is-not-a-tested-runner-image/">Ubuntu-latest Is Not a Runner Image You Already Tested&lt;/a>. This post is the same desk rule for queues. Print the timeout. Print the kill. Name who owns &lt;code>queue:work&lt;/code>.&lt;/p>
&lt;p>The question is not whether a long job demos on your laptop. The question is whether the named owner can tell a job clock from a worker death before paging the pool.&lt;/p></description></item><item><title>One Matching Glob Is Not a Sandbox Exemption for the Rest of the Line</title><link>https://zemna.net/blog/one-matching-glob-is-not-a-whole-line-exemption/</link><pubDate>Wed, 23 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/one-matching-glob-is-not-a-whole-line-exemption/</guid><description>&lt;p>The junior pastes a settings.json screenshot. &lt;code>sandbox.excludedCommands&lt;/code> has &lt;code>docker *&lt;/code>. The coding agent ran &lt;code>npm ci &amp;amp;&amp;amp; docker build .&lt;/code>. The first token looks like a match. They file a sandbox hole: “the whole line ran outside the box.”&lt;/p>
&lt;p>I stop the run there. One matching glob is not a sandbox exemption for the rest of the line. Official settings docs are blunt: your entries take a Bash call out of the sandbox only when they cover every command in it. A &lt;code>docker *&lt;/code> entry alone does not take &lt;code>npm ci &amp;amp;&amp;amp; docker build .&lt;/code> out of the sandbox. [Source: https://code.claude.com/docs/en/settings-reference]&lt;/p>
&lt;p>I already refused to treat a failed local DNS lookup as a missing host in &lt;a href="https://zemna.net/blog/hostname-handed-to-proxy-is-not-a-missing-host/">Handing the Hostname to the Forward Proxy Is Not a Missing Host&lt;/a>. I already refused to treat a present &lt;code>AGENTS.md&lt;/code> as the project instructions while &lt;code>CLAUDE.md&lt;/code> exists in &lt;a href="https://zemna.net/blog/agents-md-is-not-the-project-instructions/">A Present AGENTS.md Is Not the Project Instructions While CLAUDE.md Exists&lt;/a>. I already refused to treat &lt;code>ubuntu-latest&lt;/code> as a runner image I already tested in &lt;a href="https://zemna.net/blog/ubuntu-latest-is-not-a-tested-runner-image/">Ubuntu-latest Is Not a Runner Image You Already Tested&lt;/a>. This post is the same desk rule for compound Bash. Print the glob. Split the parts. Name who owns &lt;code>sandbox.excludedCommands&lt;/code>.&lt;/p>
&lt;p>The question is not whether &lt;code>docker *&lt;/code> demos on a single command. The question is whether the named owner can prove every part of the line matched.&lt;/p></description></item><item><title>Handing the Hostname to the Forward Proxy Is Not a Missing Host</title><link>https://zemna.net/blog/hostname-handed-to-proxy-is-not-a-missing-host/</link><pubDate>Mon, 21 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/hostname-handed-to-proxy-is-not-a-missing-host/</guid><description>&lt;p>The junior SSHs into the gateway box. &lt;code>nslookup api.anthropic.com&lt;/code> times out. &lt;code>getent hosts&lt;/code> is empty. They paste that into the incident channel and call DNS down. Standup treats it as a missing host.&lt;/p>
&lt;p>I stop the run there. Handing the hostname to the forward proxy is not a missing host. On a Claude apps gateway whose only egress is that proxy, the field is whether the gateway hands the proxy the name. Local resolve on the pod is the wrong screenshot.&lt;/p>
&lt;p>Official config docs put the switch in one environment variable: &lt;code>CLAUDE_GATEWAY_PROXY_IS_EGRESS_BOUNDARY=1&lt;/code>, next to &lt;code>HTTPS_PROXY&lt;/code>, when the pod reaches other hosts only through that forward proxy and cannot resolve public DNS names itself, or when the proxy refuses &lt;code>CONNECT&lt;/code> to an IP address. [Source: https://code.claude.com/docs/en/claude-apps-gateway-config]&lt;/p>
&lt;p>I already refused to treat a present &lt;code>AGENTS.md&lt;/code> as the project instructions while &lt;code>CLAUDE.md&lt;/code> exists in &lt;a href="https://zemna.net/blog/agents-md-is-not-the-project-instructions/">A Present AGENTS.md Is Not the Project Instructions While CLAUDE.md Exists&lt;/a>. I already refused to treat &lt;code>ubuntu-latest&lt;/code> as a runner image I already tested in &lt;a href="https://zemna.net/blog/ubuntu-latest-is-not-a-tested-runner-image/">Ubuntu-latest Is Not a Runner Image You Already Tested&lt;/a>. I already refused to treat a missing Wrangler &lt;code>previews&lt;/code> block as production bindings in &lt;a href="https://zemna.net/blog/missing-previews-block-is-not-production-bindings/">A Missing Previews Block Is Not a License to Reuse Production Bindings&lt;/a>. This post is the same desk rule for gateway egress. Print the env. Read the boot &lt;code>network:&lt;/code> line. Name who owns the boundary.&lt;/p>
&lt;p>The question is not whether the pod can resolve the provider. The question is whether the named owner can prove the hostname left the box through the proxy.&lt;/p></description></item><item><title>A Present AGENTS.md Is Not the Project Instructions While CLAUDE.md Exists</title><link>https://zemna.net/blog/agents-md-is-not-the-project-instructions/</link><pubDate>Sun, 20 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/agents-md-is-not-the-project-instructions/</guid><description>&lt;p>The junior opens the repo root. &lt;code>AGENTS.md&lt;/code> is there. Codex already reads it. Copilot already reads it. They added Claude Code last week. The agent still ships a Laravel migration without the test command they wrote in that file. Standup calls it a model miss.&lt;/p>
&lt;p>I stop the run there. A present &lt;code>AGENTS.md&lt;/code> is not the project instructions while &lt;code>CLAUDE.md&lt;/code> exists. Default load is the Claude files only when &lt;code>CLAUDE.md&lt;/code> or &lt;code>CLAUDE.local.md&lt;/code> sits in the working directory or above it. The shared file stays on disk. The session never treats it as the brief.&lt;/p>
&lt;p>Official memory docs put that in one table. An &lt;code>AGENTS.md&lt;/code> with no Claude file in the tree loads. An &lt;code>AGENTS.md&lt;/code> next to a &lt;code>CLAUDE.md&lt;/code> or &lt;code>CLAUDE.local.md&lt;/code> does not. Change the mix under &lt;strong>Project instructions&lt;/strong> in &lt;code>/config&lt;/code>. [Source: https://code.claude.com/docs/en/memory]&lt;/p>
&lt;p>I already refused to treat &lt;code>ubuntu-latest&lt;/code> as a runner image I already tested in &lt;a href="https://zemna.net/blog/ubuntu-latest-is-not-a-tested-runner-image/">Ubuntu-latest Is Not a Runner Image You Already Tested&lt;/a>. I already refused to treat a missing Wrangler &lt;code>previews&lt;/code> block as production bindings in &lt;a href="https://zemna.net/blog/missing-previews-block-is-not-production-bindings/">A Missing Previews Block Is Not a License to Reuse Production Bindings&lt;/a>. This post is the same desk rule for an instruction file. Print the load line. Open &lt;code>/config&lt;/code>. Name who owns the file Claude actually read.&lt;/p>
&lt;p>The question is not whether &lt;code>AGENTS.md&lt;/code> exists on disk. The question is whether the named owner can prove which file this session loaded.&lt;/p></description></item><item><title>Ubuntu-latest Is Not a Runner Image You Already Tested</title><link>https://zemna.net/blog/ubuntu-latest-is-not-a-tested-runner-image/</link><pubDate>Sat, 19 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/ubuntu-latest-is-not-a-tested-runner-image/</guid><description>&lt;p>Standup hears “CI is green.” Someone pasted a GitHub Actions log. The job used &lt;code>runs-on: ubuntu-latest&lt;/code>. PHPUnit passed. Vite built. The coding agent says the pipeline is already on the new runner because the changelog said Ubuntu 26 is generally available.&lt;/p>
&lt;p>I stop the run there. &lt;code>ubuntu-latest&lt;/code> is not a runner image you already tested. Official GitHub notes for 17 September 2026 say the Ubuntu 26.04 runner image left public preview and is fully supported for production workflows on x64 and arm64. The same note says the &lt;code>ubuntu-latest&lt;/code> label will migrate from Ubuntu 24.04 to Ubuntu 26.04 gradually between 19 October and 19 November 2026. Until that window, a green job on &lt;code>ubuntu-latest&lt;/code> is still the current label, not a signed test of the next image. [Source: https://github.blog/changelog/2026-09-17-ubuntu-26-generally-available-and-latest-migration/]&lt;/p>
&lt;p>The announcement issue in &lt;code>actions/runner-images&lt;/code> is public. You can select the new image today with &lt;code>runs-on: ubuntu-26.04&lt;/code> or &lt;code>runs-on: ubuntu-26.04-arm&lt;/code>. GitHub’s own table of selected software differences is not an exhaustive list. Workflows that depend on a package version, system library, compiler, prebuilt binary, or architecture-specific dependency still need a job on the new image. [Source: https://github.com/actions/runner-images/issues/14747]&lt;/p>
&lt;p>I already refused to treat a missing Wrangler &lt;code>previews&lt;/code> block as permission to reuse production bindings in &lt;a href="https://zemna.net/blog/missing-previews-block-is-not-production-bindings/">A Missing Previews Block Is Not a License to Reuse Production Bindings&lt;/a>. I already refused to treat an old MCP handshake as a dead Laravel server in &lt;a href="https://zemna.net/blog/initialize-is-not-a-broken-laravel-mcp-server/">A Client That Still Sends Initialize Is Not a Broken Laravel MCP Server&lt;/a>. This post is the same desk rule for a moving CI label. Print the label. Print the named pin. Name who owns the workflow file.&lt;/p>
&lt;p>The question is not whether Ubuntu 26 demos well on a personal fork. The question is whether the named owner can still tell a moving label from an image the team already ran.&lt;/p></description></item><item><title>A Missing Previews Block Is Not a License to Reuse Production Bindings</title><link>https://zemna.net/blog/missing-previews-block-is-not-production-bindings/</link><pubDate>Fri, 18 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/missing-previews-block-is-not-production-bindings/</guid><description>&lt;p>Standup hears “preview is using production.” Someone pasted a Wrangler log. There is no &lt;code>previews&lt;/code> block in the local config. The CLI wrote a Preview Base into the file. When no Preview Base existed, it printed a placeholder derived from production bindings and warned against reusing that configuration. The junior treats the placeholder as a filled-in answer. They tell the coding agent to copy the production KV id, the production D1 id, and the production secret names into the new block so the preview “just works.”&lt;/p>
&lt;p>I stop the run there. A missing &lt;code>previews&lt;/code> block is not a license to reuse production bindings. Official Wrangler notes for the 16 September 2026 GitHub tag, not a prerelease, name the field: when a local &lt;code>previews&lt;/code> block is absent, Wrangler writes the Preview Base configuration to the local config file. When no Preview Base configuration exists, Wrangler prints a placeholder configuration derived from production bindings and warns against reusing production binding configuration. [Source: https://github.com/cloudflare/workers-sdk/releases/tag/wrangler%404.133.0]&lt;/p>
&lt;p>The pull request behind that note is public. When you run &lt;code>npx wrangler preview&lt;/code> with no local &lt;code>previews&lt;/code> block, Wrangler recommends a snippet. The expected snippet uses &lt;code>REPLACE_ME&lt;/code> placeholders. Two origins, same rule: dashboard Preview Base, or first-time preview from production settings. Both paths are supposed to land on placeholders, not on live production ids. [Source: https://github.com/cloudflare/workers-sdk/pull/15600]&lt;/p>
&lt;p>I already refused to treat an old MCP handshake as a dead Laravel server in &lt;a href="https://zemna.net/blog/initialize-is-not-a-broken-laravel-mcp-server/">A Client That Still Sends Initialize Is Not a Broken Laravel MCP Server&lt;/a>. I already refused to treat a five-minute WebFetch fail as a hung model in &lt;a href="https://zemna.net/blog/a-300-second-webfetch-fail-is-not-a-hung-model/">A 300-Second WebFetch Fail Is Not a Hung Model&lt;/a>. This post is the same desk rule for a config write. Print the block. Print the placeholder. Name who owns &lt;code>wrangler.jsonc&lt;/code>.&lt;/p>
&lt;p>The question is not whether &lt;code>wrangler preview&lt;/code> demos well on a personal account. The question is whether the named owner can still tell an onboarding warning from permission to point preview at production data.&lt;/p></description></item><item><title>A Client That Still Sends Initialize Is Not a Broken Laravel MCP Server</title><link>https://zemna.net/blog/initialize-is-not-a-broken-laravel-mcp-server/</link><pubDate>Thu, 17 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/initialize-is-not-a-broken-laravel-mcp-server/</guid><description>&lt;p>Standup hears “Laravel MCP is broken.” Someone pasted a JSON-RPC log. The first method is &lt;code>initialize&lt;/code>. The package pin on the host is the new stable line. The junior files a server-outage ticket. They ping the person who shipped the bump. They tell the coding agent to wait for a rollback.&lt;/p>
&lt;p>I stop the run there. A client that still opens with &lt;code>initialize&lt;/code> is not a broken Laravel MCP server. Official upgrade notes for the 14 September 2026 GitHub tag, published 14:39 UTC, not a prerelease, name the field: the server now speaks protocol revision &lt;code>2026-07-28&lt;/code> by default, and &lt;code>server/discover&lt;/code> replaces the old handshake for modern clients. The same notes say clients that still open with &lt;code>initialize&lt;/code> continue to work. The server answers that handshake with &lt;code>2025-11-25&lt;/code> or &lt;code>2025-06-18&lt;/code>, whichever the client requested. [Source: https://github.com/laravel/mcp/releases/tag/v1.0.0] [Source: https://github.com/laravel/mcp/blob/v1.0.0/UPGRADE.md]&lt;/p>
&lt;p>Laravel News, 15 September 2026, repeats the same desk rule in one sentence: clients that still connect with &lt;code>initialize&lt;/code> continue to work. [Source: https://laravel-news.com/laravel-mcp-1-0]&lt;/p>
&lt;p>I already refused to treat an every-turn HTTP 400 as a dead gateway in &lt;a href="https://zemna.net/blog/every-turn-http-400-is-not-a-dead-gateway/">Every-Turn HTTP 400 on ANTHROPIC_BASE_URL Is Not a Dead Gateway&lt;/a>. I already refused to treat a five-minute WebFetch fail as a hung model in &lt;a href="https://zemna.net/blog/a-300-second-webfetch-fail-is-not-a-hung-model/">A 300-Second WebFetch Fail Is Not a Hung Model&lt;/a>. This post is the same desk rule for an old handshake on a new MCP server. Print the method. Print the pin. Name who owns both.&lt;/p>
&lt;p>The question is not whether the new handshake demos well in Inspector. The question is whether the named owner can still tell an old client from a dead server.&lt;/p></description></item><item><title>A 300-Second WebFetch Fail Is Not a Hung Model</title><link>https://zemna.net/blog/a-300-second-webfetch-fail-is-not-a-hung-model/</link><pubDate>Wed, 16 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/a-300-second-webfetch-fail-is-not-a-hung-model/</guid><description>&lt;p>Standup hears “the model hung.” Someone pasted a terminal. The line sat on Fetching. Five minutes later it went red. The junior files a model-outage ticket. They ping the vendor channel. They tell chat to wait for a new brain.&lt;/p>
&lt;p>I stop the run there. A red WebFetch at five minutes is not a hung model. Official notes from 10 September 2026 name the field: WebFetch used to hang indefinitely on a server that keeps the response open without finishing. A fetch now fails after 300 seconds. The env that overrides that deadline is &lt;code>CLAUDE_CODE_WEBFETCH_DEADLINE_MS&lt;/code>. Zero turns the limit off. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.268] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a wait line as a finished background agent in &lt;a href="https://zemna.net/blog/waiting-for-input-is-not-finished/">Waiting for Your Input Is Not a Finished Background Agent&lt;/a>. I already refused to treat an every-turn HTTP 400 as a dead gateway in &lt;a href="https://zemna.net/blog/every-turn-http-400-is-not-a-dead-gateway/">Every-Turn HTTP 400 on ANTHROPIC_BASE_URL Is Not a Dead Gateway&lt;/a>. This post is the same desk rule for a fetch that never closed. Print the pin. Print the deadline. Name who owns both.&lt;/p>
&lt;p>The question is not whether chat looks stuck. The question is whether the named owner can still tell a download deadline from a model that stopped producing tokens.&lt;/p></description></item><item><title>Every-Turn HTTP 400 on ANTHROPIC_BASE_URL Is Not a Dead Gateway</title><link>https://zemna.net/blog/every-turn-http-400-is-not-a-dead-gateway/</link><pubDate>Mon, 14 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/every-turn-http-400-is-not-a-dead-gateway/</guid><description>&lt;p>Standup hears “the gateway is down.” Someone pasted a terminal. Every turn is HTTP 400. The junior files a status ticket. They ping the network channel. They tell chat to wait for the vendor.&lt;/p>
&lt;p>I stop the run there. A wall of 400s on a named &lt;code>ANTHROPIC_BASE_URL&lt;/code> is not a dead gateway. Official notes from 10 September 2026 name the field: every turn failed with HTTP 400 on third-party Anthropic-compatible endpoints since a mid-week CLI pin, because those endpoints rejected a regex in the Artifact tool’s input schema. The later pin fixes the schema. The host was answering. The request was illegal for that endpoint. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.268] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a wait line as a finished background agent in &lt;a href="https://zemna.net/blog/waiting-for-input-is-not-finished/">Waiting for Your Input Is Not a Finished Background Agent&lt;/a>. I already refused to treat a Node API miss as a new module registry in &lt;a href="https://zemna.net/blog/nodejs-apis-on-is-not-the-new-module-registry/">Node.js APIs On Is Not the New Module Registry&lt;/a>. This post is the same desk rule for a 400 wall. Print the body. Print the named base URL. Name who owns the CLI pin.&lt;/p>
&lt;p>The question is not whether chat looks red. The question is whether the named owner can still tell a schema reject from a host that does not answer.&lt;/p></description></item><item><title>Waiting for Your Input Is Not a Finished Background Agent</title><link>https://zemna.net/blog/waiting-for-input-is-not-finished/</link><pubDate>Sun, 13 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/waiting-for-input-is-not-finished/</guid><description>&lt;p>Standup hears “the agent is waiting.” Someone pasted a remote or headless screenshot. The line says waiting for your input. The junior treats that as a stop. They close the laptop. They tell the chat the overnight job is done.&lt;/p>
&lt;p>I stop the run there. A wait line is not a finished background agent. On a remote or headless host, that sentence used to print while other agents were still running. The work was live. The status lied.&lt;/p>
&lt;p>Official notes from 11 September 2026 name the split. Remote and headless sessions reported “waiting for your input” while background agents were still running. Status now reports those tasks as running. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.269] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a Node API miss as a new module registry in &lt;a href="https://zemna.net/blog/nodejs-apis-on-is-not-the-new-module-registry/">Node.js APIs On Is Not the New Module Registry&lt;/a>. I already refused to treat an unreadable managed allow-list as a company lockdown in &lt;a href="https://zemna.net/blog/treat-unreadable-managed-settings-as-empty/">Treat Unreadable Managed Settings as Empty&lt;/a>. This post is the same desk rule for a wait line. Read the agent list. Split Needs input from Working. Name who owns the remote-headless host and the CLI pin.&lt;/p>
&lt;p>The question is not whether chat printed a polite pause. The question is whether the named owner can still see the work that is running.&lt;/p></description></item><item><title>Node.js APIs On Is Not the New Module Registry</title><link>https://zemna.net/blog/nodejs-apis-on-is-not-the-new-module-registry/</link><pubDate>Sat, 12 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/nodejs-apis-on-is-not-the-new-module-registry/</guid><description>&lt;p>Standup hears “Node compat is off.” Someone pasted a red agent log. &lt;code>import.meta.url&lt;/code> is undefined. A package that worked on Node 22 throws in &lt;code>wrangler dev&lt;/code>. The junior opens the Cloudflare status page. The coding agent offers to “turn Node.js compatibility back on” by rewriting &lt;code>compatibility_flags&lt;/code> until chat works.&lt;/p>
&lt;p>I stop the run there. A missing &lt;code>import.meta.url&lt;/code> is a module-registry ticket until proven otherwise. It is not “Workers turned Node off.”&lt;/p>
&lt;p>On 9 September 2026 Cloudflare published the field note that names the split. Node.js APIs that you want in a serverless Worker are already on by default. The new URL-based module registry is a &lt;strong>separate&lt;/strong> compatibility flag named &lt;code>new_module_registry&lt;/code>. Official post: that flag does not have a default-on date. It will not turn on from &lt;code>compatibility_date&lt;/code> alone. You add it on purpose. [Source: https://blog.cloudflare.com/workers-module-registry-nodejs/]&lt;/p>
&lt;p>I already refused to treat a failed Cloudflare login as a status outage in &lt;a href="https://zemna.net/blog/requested-scopes-before-outage/">Read the Requested Scopes Before You File an Outage&lt;/a>. I already refused to treat an unreadable managed allow-list as a company lockdown in &lt;a href="https://zemna.net/blog/treat-unreadable-managed-settings-as-empty/">Treat Unreadable Managed Settings as Empty&lt;/a>. This post is the same desk rule for Workers flags. Read the list. Split the two switches. Name who owns that list. Do not let the agent flip &lt;code>no_nodejs_compat&lt;/code> because a URL API is missing.&lt;/p>
&lt;p>The question is not whether Node “works” in chat. The question is which flag the named owner pinned, and whether the coding agent is allowed to touch it.&lt;/p></description></item><item><title>Treat Unreadable Managed Settings as Empty</title><link>https://zemna.net/blog/treat-unreadable-managed-settings-as-empty/</link><pubDate>Thu, 10 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/treat-unreadable-managed-settings-as-empty/</guid><description>&lt;p>Standup hears “the org locked HTTP hooks.” Someone pasted a red agent log. The hook URL that worked yesterday is blocked. The junior opens the vendor status page. The coding agent offers to “relax the allow-list in the project file until chat works.”&lt;/p>
&lt;p>I stop the run there. A sudden block on HTTP hooks is a readable-file problem until proven otherwise. It is not “Claude Code locked the company.”&lt;/p>
&lt;p>On 9 September 2026 Claude Code published a patch that names three managed allow-list keys. Official notes say managed &lt;code>allowedHttpHookUrls&lt;/code>, &lt;code>httpHookAllowedEnvVars&lt;/code>, and &lt;code>allowedChannelPlugins&lt;/code> now admit nothing, not everything, when those settings are unreadable. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.267] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat an Organization policy why-line as a loaded fleet file in &lt;a href="https://zemna.net/blog/org-policy-why-line/">Read the Why-Line Before You Trust the Org Policy&lt;/a>. I already refused to treat a failed Cloudflare login as a status outage in &lt;a href="https://zemna.net/blog/requested-scopes-before-outage/">Read the Requested Scopes Before You File an Outage&lt;/a>. This post is the same desk rule for the managed allow-list. Read the file. Parse the three keys. Name who owns that file. Do not let the agent rewrite policy as a drive-by lint.&lt;/p>
&lt;p>The question is not whether the CLI printed a new block. The question is whether the managed file on this machine is readable, and who owns the pin when it is not.&lt;/p></description></item><item><title>Read the Requested Scopes Before You File an Outage</title><link>https://zemna.net/blog/requested-scopes-before-outage/</link><pubDate>Wed, 09 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/requested-scopes-before-outage/</guid><description>&lt;p>Standup hears “Cloudflare OAuth is down.” Someone pasted a red terminal. &lt;code>cf auth login&lt;/code> died. The junior opens the status page. The coding agent offers to “switch providers until login works.”&lt;/p>
&lt;p>I stop the run there. A failed login is a requested-scope list. It is not a status incident.&lt;/p>
&lt;p>On 7 September 2026 Cloudflare published a patch for &lt;code>@cloudflare/workers-auth&lt;/code>. The notes are blunt: &lt;code>cf auth login&lt;/code> asked for &lt;code>email_routing:read&lt;/code> and &lt;code>email_sending:read&lt;/code>. The Cloudflare OAuth server does not define those names. Login failed with an unknown OAuth scope error. The CLI now requests only the registered write scopes for those products. [Source: https://github.com/cloudflare/workers-sdk/releases/tag/%40cloudflare/workers-auth%400.6.9]&lt;/p>
&lt;p>I already refused to treat an Organization policy why-line as a loaded fleet file in &lt;a href="https://zemna.net/blog/org-policy-why-line/">Read the Why-Line Before You Trust the Org Policy&lt;/a>. I already refused to treat a green &lt;code>apt update&lt;/code> as a trusted key in &lt;a href="https://zemna.net/blog/green-apt-is-not-a-trusted-key/">A Green apt Update Is Not a Trusted Key After Saturday&lt;/a>. This post is the login version of the same desk rule. Read the scope the client asked for. Check whether the server registered it. Name who owns the CLI pin. Do not page an outage for a catalog mismatch.&lt;/p>
&lt;p>The question is not whether Cloudflare’s dashboard loaded this morning. The question is whether the binary on this laptop asked the OAuth server for a name that server has never heard.&lt;/p></description></item><item><title>Read the Why-Line Before You Trust the Org Policy</title><link>https://zemna.net/blog/org-policy-why-line/</link><pubDate>Tue, 08 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/org-policy-why-line/</guid><description>&lt;p>Standup hears “org policy is on.” Someone pasted a screenshot of &lt;code>/status&lt;/code>. There is a line that says Organization policy. The junior treats that line as a green light and keeps prompting.&lt;/p>
&lt;p>I stop the run there. A line that explains a miss is not a loaded policy. A proxy that drops the settings endpoint will still print a reason. The reason is useful. The reason is not enforcement.&lt;/p>
&lt;p>On 4 September 2026 Claude Code added an Organization policy line to &lt;code>/status&lt;/code> and &lt;code>claude doctor&lt;/code>. Official notes say the line reports &lt;strong>why&lt;/strong> the organization’s policy could not be loaded, for example a proxy not passing the endpoint through. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.261] [Source: https://code.claude.com/docs/en/changelog]&lt;/p>
&lt;p>I already refused to treat a deferred group name as a PHP helper in &lt;a href="https://zemna.net/blog/deferred-group-name-is-not-a-helper/">A Deferred Group Name Is Not a Helper&lt;/a>. I already refused to treat a missing Issues tab as a missing ticket in &lt;a href="https://zemna.net/blog/issues-tab-is-not-a-package-ticket/">An Issues Tab Is Not a Laravel Package Ticket&lt;/a>. This post is the same desk rule for fleet policy. Read the why-line. Name who owns the proxy and the allow-list. Do not resume a coding agent until that owner answers.&lt;/p>
&lt;p>The question is not whether the CLI printed a new label. The question is whether the machine you are about to trust actually loaded the policy your admin thinks is in force.&lt;/p></description></item><item><title>A Deferred Group Name Is Not a Helper</title><link>https://zemna.net/blog/deferred-group-name-is-not-a-helper/</link><pubDate>Mon, 07 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/deferred-group-name-is-not-a-helper/</guid><description>&lt;p>The dashboard is green in the browser. Users load. Roles load. The Vue &lt;code>Deferred&lt;/code> fallback flips to the user card. The feature test is red. Standup hears “Inertia dropped the auth group.”&lt;/p>
&lt;p>I stop the run there. A deferred group named &lt;code>auth&lt;/code> is a request batch. It is not the Laravel &lt;code>auth()&lt;/code> helper. A red &lt;code>loadDeferredProps('auth')&lt;/code> line this week is not a missing Vue prop.&lt;/p>
&lt;p>On 4 September 2026 the Laravel adapter for Inertia published a testing-helper fix: &lt;code>loadDeferredProps()&lt;/code> crashed when the group string matched a global PHP function. The Vue npm client did not move. [Source: https://github.com/inertiajs/inertia-laravel/releases/tag/v3.3.3] [Source: https://github.com/inertiajs/inertia-laravel/pull/908]&lt;/p>
&lt;p>I already refused to treat a missing Issues tab as a missing ticket in &lt;a href="https://zemna.net/blog/issues-tab-is-not-a-package-ticket/">An Issues Tab Is Not a Laravel Package Ticket&lt;/a>. I already refused to treat a green &lt;code>apt&lt;/code> line as a trusted key in &lt;a href="https://zemna.net/blog/green-apt-is-not-a-trusted-key/">A Green apt Update Is Not a Trusted Key After Saturday&lt;/a>. This post is the deferred-props version of the same desk rule. Name the group. Name whether it collides with a PHP helper. Name who owns that string before a coding agent copies &lt;code>auth&lt;/code> from the controller into the test.&lt;/p>
&lt;p>The question is not whether the Vue client bumped. The question is whether your next feature test treats a batch name as a callback.&lt;/p></description></item><item><title>An Issues Tab Is Not a Laravel Package Ticket</title><link>https://zemna.net/blog/issues-tab-is-not-a-package-ticket/</link><pubDate>Sun, 06 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/issues-tab-is-not-a-package-ticket/</guid><description>&lt;p>The junior opens &lt;code>laravel/socialite&lt;/code> and looks for the Issues tab. GitHub has nothing to click. Standup hears “we cannot file the OAuth bug.” The coding agent offers to post in Discord instead.&lt;/p>
&lt;p>I stop the run there. A missing Issues tab is not a missing ticket. On most Laravel packages this week, the ticket is a pull request that names the failure. The chat thread is gossip.&lt;/p>
&lt;p>Taylor Otwell wrote the rule in public on 3 September 2026: last week he disabled GitHub Issues on most Laravel open-source packages. If you hit a bug, describe it to a coding agent and open a PR. Even if the code is not great, iterate. The PR still documents the problem. Laravel News restated it on 4 September: this is for packages, not the main Laravel repo. [Source: https://x.com/taylorotwell/status/2095516796748996843] [Source: https://laravel-news.com/taylor-disabled-github-issues]&lt;/p>
&lt;p>I already refused to treat a green &lt;code>apt&lt;/code> line as a trusted key in &lt;a href="https://zemna.net/blog/green-apt-is-not-a-trusted-key/">A Green apt Update Is Not a Trusted Key After Saturday&lt;/a>. I already refused to treat one Copilot exclusion list as every surface in &lt;a href="https://zemna.net/blog/name-the-copilot-exclusion-surface/">Name the Copilot Surface Before You Trust the Exclusion List&lt;/a>. This post is the contribution-path version of the same desk rule. Name the repo. Name whether Issues are on. Name who owns the PR that documents the bug.&lt;/p>
&lt;p>The question is not whether Taylor is “right about open source.” The question is whether your next Socialite failure becomes a fork, a Discord ping, or a PR a maintainer can review.&lt;/p></description></item><item><title>A Green apt Update Is Not a Trusted Key After Saturday</title><link>https://zemna.net/blog/green-apt-is-not-a-trusted-key/</link><pubDate>Sat, 05 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/green-apt-is-not-a-trusted-key/</guid><description>&lt;p>The CI job is green. Last night &lt;code>apt-get update&lt;/code> returned zero. The runner still has &lt;code>gh&lt;/code>. Standup hears “Linux packages are fine.”&lt;/p>
&lt;p>I stop the run there. A green last update is a timestamp. It is not a trusted key.&lt;/p>
&lt;p>Today is Saturday, 5 September 2026. GitHub’s changelog is blunt: the current PGP key for the GitHub CLI Linux package repositories expires on this date. Beginning with the first release after that date, APT and RPM repository metadata and newly published RPM packages will be signed with just the replacement key. [Source: https://github.blog/changelog/2026-09-03-github-cli-linux-package-signing-key-expires-september-5/]&lt;/p>
&lt;p>If this box installed &lt;code>gh&lt;/code> from the official APT or RPM repos before 8 April 2026 and nobody reran the setup, the local keyring is still the old file. Last night’s green &lt;code>apt update&lt;/code> only proves the old key was still alive at that hour. It does not prove the replacement key is on disk.&lt;/p>
&lt;p>I already refused to treat a Copilot overview as a merge vote in &lt;a href="https://zemna.net/blog/leave-copilot-approve-off/">Leave Copilot Approve Off&lt;/a>. I already refused to treat one exclusion list as every Copilot surface in &lt;a href="https://zemna.net/blog/name-the-copilot-exclusion-surface/">Name the Copilot Surface Before You Trust the Exclusion List&lt;/a>. This post is the package-trust version of the same desk rule. Name who owns the keyring on the image. Name which install path you used. Do not treat a green &lt;code>apt&lt;/code> line as recovered trust.&lt;/p>
&lt;p>The question is not whether GitHub published a new key in April. The question is whether the Dockerfile that still copies last year’s &lt;code>githubcli-archive-keyring.gpg&lt;/code> will survive the first &lt;code>apt-get update&lt;/code> after the old key dies.&lt;/p></description></item><item><title>Name the Copilot Surface Before You Trust the Exclusion List</title><link>https://zemna.net/blog/name-the-copilot-exclusion-surface/</link><pubDate>Fri, 04 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/name-the-copilot-exclusion-surface/</guid><description>&lt;p>A junior opens &lt;code>.env&lt;/code>, sees no ghost text, and closes the laptop. The exclusion list is in repository settings. Inline suggestions stayed quiet. Standup hears &amp;ldquo;secrets are off Copilot.&amp;rdquo;&lt;/p>
&lt;p>I stop the run there. Quiet inline is one surface. It is not every surface.&lt;/p>
&lt;p>On 2 September 2026 GitHub made content exclusion generally available in the Copilot app and Copilot CLI for Copilot Business and Copilot Enterprise. Those two products now skip excluded files as context. The same week, GitHub&amp;rsquo;s own docs still say Edit mode and Agent mode in Copilot Chat do not support content exclusion. [Source: https://github.blog/changelog/2026-09-02-content-exclusions-generally-available-in-copilot-app-and-cli/] [Source: https://docs.github.com/en/copilot/concepts/context/content-exclusion]&lt;/p>
&lt;p>The list is real. The gap is also real. A policy row in settings is not a recovered secret.&lt;/p>
&lt;p>I already left Copilot&amp;rsquo;s ready-to-approve line as a comment, not a vote, in &lt;a href="https://zemna.net/blog/leave-copilot-approve-off/">Leave Copilot Approve Off&lt;/a>. I already kept Merge on a named human in &lt;a href="https://zemna.net/blog/coding-agent-merge-rights/">I Do Not Give a Coding Agent Merge Rights&lt;/a>. This post is the file-boundary version of the same desk rule. Name who owns the exclusion list. Name which Copilot surface you tested. Do not treat one green check as the whole product.&lt;/p>
&lt;p>The question is not whether GitHub added a settings page. The question is whether the path you care about still reaches Agent chat when a person is tired.&lt;/p></description></item><item><title>Leave Copilot Approve Off</title><link>https://zemna.net/blog/leave-copilot-approve-off/</link><pubDate>Thu, 03 Sep 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/leave-copilot-approve-off/</guid><description>&lt;p>The pull request is green. Copilot already reviewed it. The overview comment says the change is ready to approve. A junior on the desk reads that line as the missing vote.&lt;/p>
&lt;p>I stop the run there. Ready-to-approve is Copilot&amp;rsquo;s judgment in a comment. It is not a required approval. It is not Merge.&lt;/p>
&lt;p>GitHub shipped this as public preview on 1 September 2026. Every Copilot code review now includes an approval assessment in the overview comment. Admins can later authorize Copilot to submit a real approval that counts toward the repository&amp;rsquo;s required-approvals rule. That ability is &lt;strong>off by default&lt;/strong> at enterprise, organization, and repository level. An assessment alone does not count toward merge requirements. [Source: https://github.blog/changelog/2026-09-01-copilot-code-review-can-now-approve-pull-requests/]&lt;/p>
&lt;p>I already refused agent merge in &lt;a href="https://zemna.net/blog/coding-agent-merge-rights/">I Do Not Give a Coding Agent Merge Rights&lt;/a>. This post is the review-line that landed after that refusal. The agent still opens the branch. Copilot still comments. A named human still owns required approvals and the Merge click.&lt;/p>
&lt;p>The question is not whether Copilot writes a fluent review. The question is whether the team treats that fluency as a vote on the branch we deploy.&lt;/p></description></item><item><title>Five Things I Refused This Week</title><link>https://zemna.net/blog/five-refusals-this-week/</link><pubDate>Sun, 30 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/five-refusals-this-week/</guid><description>&lt;p>The agent opened the pull request on Friday. Saturday morning nobody could name the model. The merge button still had no owner. The retry after abort sat in a chat log.&lt;/p>
&lt;p>I did not ship a new tool this week. I refused five things. Rank them. Screenshot the list. Put the names on the ticket before Monday.&lt;/p>
&lt;p>The question is not whether the agent demos well. The question is whether the change survives review, merge, retry, and the next person who has to roll it back.&lt;/p></description></item><item><title>I Do Not Let a Coding Agent Pick Its Own Model</title><link>https://zemna.net/blog/coding-agent-model-owner/</link><pubDate>Sat, 29 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/coding-agent-model-owner/</guid><description>&lt;p>The session is already running. The agent opened a pull request, wrote a confident summary, and asked for review. Nobody on the ticket can say which model produced the diff.&lt;/p>
&lt;p>That is the moment I stop the run. The agent may write the branch. The agent may open the pull request. The agent does not choose the model for that work. A named human writes the model on the ticket before the first prompt.&lt;/p>
&lt;p>Yesterday I wrote the merge click. Merge stays with a person. Today&amp;rsquo;s refusal is one gate earlier: the model that authors the change, and the model that reviews it, are human picks. Auto is a vendor default. It is not a name.&lt;/p></description></item><item><title>I Do Not Give a Coding Agent Merge Rights</title><link>https://zemna.net/blog/coding-agent-merge-rights/</link><pubDate>Fri, 28 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/coding-agent-merge-rights/</guid><description>&lt;p>The pull request is green. The agent asked for review. Someone is in a standup and enables &lt;strong>agent merge&lt;/strong> so the branch can land while they talk.&lt;/p>
&lt;p>That is the moment I stop the run. The agent may write the branch. The agent may push commits. The agent may request review. The merge button stays with a named human.&lt;/p>
&lt;p>GitHub already treats Copilot pull requests as work that needs a human pass. The docs say to check the diff thoroughly before merging, and if the repository requires approvals, &lt;strong>your&lt;/strong> approval of a Copilot pull request does not count toward that number. Another reviewer has to approve. [Source: https://docs.github.com/en/copilot/how-tos/copilot-on-github/use-copilot-agents/review-copilot-output]&lt;/p>
&lt;p>I use that as policy, not as a product footnote. The question is not whether the agent can land the PR. The question is whether a person with a name still owns the click when the deploy path is Laravel, Vue, and a Monday morning database.&lt;/p></description></item><item><title>I Do Not Add a Second Chart Stack Mid-Sprint</title><link>https://zemna.net/blog/second-chart-stack-mid-sprint/</link><pubDate>Thu, 27 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/second-chart-stack-mid-sprint/</guid><description>&lt;p>The chart PR is green. The screenshot on the happy path looks fine. Then someone adds a second chart library &amp;ldquo;just for one widget.&amp;rdquo;&lt;/p>
&lt;p>That is not a visualization improvement. That is two tooltips, two dark-mode contracts, two SSR hydrate paths, and two rollback stories living in the same admin shell. I refuse that merge until two names exist on the ticket: the person who owns revert, and the fixture that fails when the chart lies.&lt;/p>
&lt;p>This week&amp;rsquo;s ApexCharts default-bundle shrink is evidence that the stack you already have still moves. It is not a reason to install Apache ECharts beside it before Friday.&lt;/p>
&lt;p>The question is not whether the extra widget demos well. The question is whether it survives maintenance, handoff, and the three dashboard screens your operators open every Monday.&lt;/p></description></item><item><title>Empty Tool Output Is Not Success Until the Harness Says Interrupted</title><link>https://zemna.net/blog/empty-tool-output-is-not-success/</link><pubDate>Wed, 26 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/empty-tool-output-is-not-success/</guid><description>&lt;p>The cron row is green. The JSONL says the MCP tool finished. The stdout field is empty.&lt;/p>
&lt;p>That is the most expensive sentence a headless coding agent can write. The next step is invented. The PR lands. The Laravel worker image still talks to last week&amp;rsquo;s schema. Nobody owns the rollback because the transcript never recorded a failure.&lt;/p>
&lt;p>Anthropic&amp;rsquo;s Claude Code notes for &lt;strong>25 August 2026&lt;/strong> (tag &lt;code>v2.1.246&lt;/code>, published &lt;code>2026-08-25T22:31:43Z&lt;/code>) name the lie in one line: MCP tool calls interrupted by an incoming message in headless and remote sessions were reported to the model as &lt;strong>&amp;ldquo;completed with no output&amp;rdquo;&lt;/strong> instead of an explicit interrupted error. [Source: https://github.com/anthropics/claude-code/releases/tag/v2.1.246]&lt;/p>
&lt;p>The question is not whether a new tag demos well. It is whether your harness still treats empty completion as a close.&lt;/p></description></item><item><title>Your Coding Agent Needs a Map, Not a Bigger Context Window — Part 2</title><link>https://zemna.net/blog/your-coding-agent-needs-a-map-not-a-bigger-context-window-part-2/</link><pubDate>Fri, 21 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/your-coding-agent-needs-a-map-not-a-bigger-context-window-part-2/</guid><description>&lt;p>The agent grepped the whole Laravel app, opened a sibling Vue folder, and &amp;ldquo;fixed&amp;rdquo; shipping in a helper nobody called from the failing test. The PR looked busy. The bug was still in &lt;code>app/Services/ShippingCost.php&lt;/code>.&lt;/p>
&lt;p>That is the failure I now refuse. A bigger context window does not make a coding agent safer. It makes the wrong search cheaper. The map file is the allow-list. I do not let the agent search the whole tree. Paths outside the map fail the job.&lt;/p>
&lt;p>The question is not whether this demos well. The question is whether it survives the next review, the next handoff, and the person who has to revert the branch.&lt;/p></description></item><item><title>Hermes Agent Multi-Agent Orchestration: How Cron Pipelines Delegate, Verify, and Recover</title><link>https://zemna.net/blog/hermes-agent-multi-agent-orchestration-how-cron-pipelines-delegate-verify-and-recover/</link><pubDate>Sun, 16 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/hermes-agent-multi-agent-orchestration-how-cron-pipelines-delegate-verify-and-recover/</guid><description>&lt;p>You schedule the cron. The agent runs. Exit code 0. Green dashboard. Nothing actually shipped.&lt;/p>
&lt;p>I&amp;rsquo;ve watched this pattern repeat across dozens of production pipelines. The process exits cleanly. The heartbeat ticks. The monitoring shows healthy. But the artifact — the file, the row, the URL, the test log — never materialized. This is the &lt;strong>zombie task&lt;/strong> problem, and it&amp;rsquo;s the central failure mode of autonomous AI agent pipelines.&lt;/p></description></item><item><title>Building a Background Agent Recovery CLI: The Three-Gate Check</title><link>https://zemna.net/blog/building-a-background-agent-recovery-cli-the-three-gate-check/</link><pubDate>Fri, 14 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/building-a-background-agent-recovery-cli-the-three-gate-check/</guid><description>&lt;p>The overnight coding agent wrote &amp;ldquo;Done.&amp;rdquo; The chat was green. The shipping cost file on disk was still yesterday&amp;rsquo;s version.&lt;/p>
&lt;p>I do not mark that run recovered. Recovered means three things I can show a junior without opening the vendor UI: a file that exists, a job log that names why the process stopped, and one restart from the last good commit that is allowed to fail once.&lt;/p>
&lt;p>The question is not whether the agent demos well. The question is whether the next person can restart the job from a known commit when I am offline.&lt;/p></description></item><item><title>Parallel AI Agents on a Shared Checkout — The Lease Contract You Need</title><link>https://zemna.net/blog/parallel-ai-agents-on-a-shared-checkout-the-lease-contract-you-need/</link><pubDate>Thu, 06 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/parallel-ai-agents-on-a-shared-checkout-the-lease-contract-you-need/</guid><description>&lt;p>Two agents. One checkout. No lease. The merge race starts the moment the second agent writes.&lt;/p>
&lt;p>Your team adopts Claude Code for the backend, Codex for the frontend, and a Hermes subagent for the migration script. They all target &lt;code>main&lt;/code>. The first agent creates a feature branch, stages changes, commits. The second agent, unaware, checks out the same branch, stages its own changes, commits. The third agent runs a rebase. The result: orphan commits, staging pollution, a force-push that rewrites history, and three hours of forensic git archaeology to recover what was lost. This is not a hypothetical. GeekNews has documented parallel agent failure patterns including branch hijacking, orphan commits, staging pollution, and double implementation [Source: https://news.hada.io/topic?id=26120].&lt;/p></description></item><item><title>A Green Cron Exit Is Not a Finished Job</title><link>https://zemna.net/blog/a-green-cron-exit-is-not-a-finished-job/</link><pubDate>Sun, 02 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/a-green-cron-exit-is-not-a-finished-job/</guid><description>&lt;p>Your nightly embedding pipeline exits &lt;code>0&lt;/code>. The logs show &lt;code>INFO Job completed&lt;/code>. The monitoring dashboard stays green. Three days later you discover the vector index hasn&amp;rsquo;t been updated since Tuesday — the agent skipped the upsert because the upstream API returned &lt;code>429&lt;/code>, caught the exception, logged a warning, and returned success. The cron scheduler sees exit &lt;code>0&lt;/code> and marks the run healthy. This is not a bug. This is the default behavior of every cron implementation since 1979 (Version 7 Unix).&lt;/p></description></item><item><title>A table beta bump is still a pilot decision</title><link>https://zemna.net/blog/table-beta-is-still-a-pilot-decision/</link><pubDate>Sat, 01 Aug 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/table-beta-is-still-a-pilot-decision/</guid><description>&lt;p>The release note says the table got smarter about global filters.&lt;/p>
&lt;p>&lt;strong>TanStack Table &lt;code>v9.0.0-beta.64&lt;/code>&lt;/strong>, published &lt;strong>2026-07-31&lt;/strong>, includes one core fix that sounds small enough to merge while someone else is talking: &lt;strong>table-core honors an explicit global-filter opt-in&lt;/strong> (&lt;a href="https://github.com/TanStack/table/pull/6439">#6439&lt;/a>). Packages on that tag include &lt;code>@tanstack/table-core@9.0.0-beta.64&lt;/code> and &lt;code>@tanstack/vue-table@9.0.0-beta.64&lt;/code>. [Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.64]&lt;/p>
&lt;p>That line is useful. It is not a production decision.&lt;/p>
&lt;p>On the same calendar day, npm &lt;code>dist-tags&lt;/code> for both &lt;code>@tanstack/table-core&lt;/code> and &lt;code>@tanstack/vue-table&lt;/code> still showed:&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>dist-tag&lt;/th>
 &lt;th>Version (verified 2026-08-01)&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>latest&lt;/code>&lt;/td>
 &lt;td>&lt;strong>8.21.3&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>beta&lt;/code>&lt;/td>
 &lt;td>&lt;strong>9.0.0-beta.65&lt;/strong> (already past &lt;code>.64&lt;/code>)&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>[Source: https://registry.npmjs.org/@tanstack/table-core] [Source: https://registry.npmjs.org/@tanstack/vue-table]&lt;/p>
&lt;p>Read that table twice. Stable is still &lt;strong>8.21.3&lt;/strong>. The beta channel did not freeze on the opt-in fix you just bookmarked. &lt;strong>beta.65&lt;/strong> landed minutes later with another global-filter fix (undefined first values). If your spike branch says “install beta” without an exact pin, you are not testing the release note you read. You are testing whichever beta the registry served that hour.&lt;/p>
&lt;p>The question is not whether beta.64 demos cleanly in a fresh sandbox. The question is whether a multi-surface admin table — filters, sort, pagination, selection, SSR hydrate, worker parity — still has a named owner, a fixture that would fail if opt-in is ignored, and a one-command rollback when the pilot goes sideways.&lt;/p></description></item><item><title>A UI kit lockfile bump is change control, not cosmetics</title><link>https://zemna.net/blog/ui-kit-bump-is-change-control/</link><pubDate>Fri, 31 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/ui-kit-bump-is-change-control/</guid><description>&lt;p>A quiet &lt;code>package.json&lt;/code> bump can still be a release.&lt;/p>
&lt;p>&lt;strong>shadcn-vue 2.8.1&lt;/strong> shipped on &lt;strong>2026-07-29&lt;/strong>. npm &lt;code>latest&lt;/code> points at &lt;strong>2.8.1&lt;/strong>. The GitHub release is not a major redesign and does not advertise a Reka primitive rewrite. It adds registry entries, tightens CLI icon mapping, ships shimmer and scroll-fade utilities, preserves a message-scroller scrollbar gutter, improves Tailwind icon-selector extraction, and applies style-aware classes to number-field inputs. That is exactly the kind of patch teams merge while talking about something else. [Source: https://github.com/unovue/shadcn-vue/releases/tag/v2.8.1] [Source: https://www.npmjs.com/package/shadcn-vue]&lt;/p>
&lt;p>The failure mode is not “the patch is evil.” The failure mode is calling cosmetics what is actually &lt;strong>generated surface area&lt;/strong>: registry pull paths, icon maps, chat UI chrome, form field styling, and CSS extraction. If your product has an admin shell, a support chat pane, or dense numeric forms, those surfaces break before the marketing page does.&lt;/p>
&lt;p>The question is not whether the bump demos cleanly in a fresh sandbox. The question is whether it survives maintenance, handoff, and the three screens your operators open every Monday.&lt;/p></description></item><item><title>PHP 8.5.9 is a runtime patch contract, not an app deploy</title><link>https://zemna.net/blog/php-8-5-9-runtime-patch-inventory/</link><pubDate>Thu, 30 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/php-8-5-9-runtime-patch-inventory/</guid><description>&lt;p>A green application deploy is a weak answer to a PHP security release.&lt;/p>
&lt;p>PHP &lt;strong>8.5.9&lt;/strong> is already visible as source truth: the &lt;code>php-8.5.9&lt;/code> NEWS entry is dated &lt;strong>30 Jul 2026&lt;/strong>, the distribution tarball at &lt;code>https://www.php.net/distributions/php-8.5.9.tar.gz&lt;/code> returns HTTP 200, and the PGSQL section records a fix for &lt;strong>GHSA-7qpv-r5mr-78m4&lt;/strong> — SQL injection via &lt;code>E'...'&lt;/code> backslash breakout, tracked as &lt;strong>CVE-2026-17543&lt;/strong>. At the same time, the public php.net releases JSON endpoint still presents &lt;strong>8.5.8&lt;/strong> (02 Jul 2026) as the current 8.x announcement row, and the main ChangeLog page has not yet absorbed 8.5.9. That lag is not a reason to ignore the patch. It is a reason to treat the release as a &lt;strong>runtime inventory problem&lt;/strong> instead of an “npm-style bump” you fold into the next app ship. [Source: https://raw.githubusercontent.com/php/php-src/php-8.5.9/NEWS] [Source: https://www.php.net/releases/index.php?json]&lt;/p>
&lt;p>If your product is a Laravel app on &lt;code>pgsql&lt;/code>, the vulnerable surface is not “some controller you forgot to review.” It is the PHP binary that still accepts traffic, runs queue workers, executes scheduled commands, and builds CI images. The question is not whether the changelog demos cleanly. The question is whether the fix survives handoff across every surface that still claims to be production.&lt;/p></description></item><item><title>ApexCharts 6.6 unit and waffle charts are a product decision, not a type swap</title><link>https://zemna.net/blog/apexcharts-6-6-unit-waffle-contract/</link><pubDate>Wed, 29 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/apexcharts-6-6-unit-waffle-contract/</guid><description>&lt;p>A new chart type looks like a one-line config change. In a SaaS dashboard, it is rarely that small.&lt;/p>
&lt;p>ApexCharts &lt;strong>6.6.0&lt;/strong>, published on 2026-07-27, introduces the first premium chart type in the library: &lt;code>unit&lt;/code>, with &lt;code>waffle&lt;/code> as a thin alias. The release is explicit that the feature renders in trial mode with an &lt;code>APEXCHARTS&lt;/code> watermark until a license key is set, that other chart types stay free and unwatermarked, and that existing configs render unchanged. Those three sentences already change the maintenance conversation. You are no longer choosing between &lt;code>pie&lt;/code> and &lt;code>donut&lt;/code>. You are choosing whether a paid, watermarked, opt-in visualization path belongs in a product surface that support, sales, and compliance will see.&lt;/p>
&lt;p>The question is not whether the demo looks clever. The question is whether the change survives license review, handoff, SSR, accessibility checks, and a clean rollback when the new type is the wrong fit.&lt;/p></description></item><item><title>The Schema Change an AI Coding Agent Cannot Safely Guess</title><link>https://zemna.net/blog/ai-agent-schema-migration-contract/</link><pubDate>Tue, 28 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/ai-agent-schema-migration-contract/</guid><description>&lt;p>An AI coding agent can open your repository, invent a migration class, rename a column in three models, and produce a green local test suite before lunch. That speed is useful. It is also the trap.&lt;/p>
&lt;p>The dangerous output is not a broken SQL statement. The dangerous output is a tidy schema change that looks complete because &lt;code>php artisan migrate&lt;/code> succeeded on a developer laptop. Production still has old queue workers, dual readers, partially backfilled rows, cached Eloquent attributes, and a rollback path that only exists as a chat transcript. The model guessed a destination schema. It did not own the compatibility contract that keeps live traffic safe while both shapes exist.&lt;/p>
&lt;p>The question is not whether the agent demos well. The question is whether the change survives maintenance, handoff, and local constraints. If nobody can answer “what still works while old and new code run together,” the migration is not ready to merge.&lt;/p></description></item><item><title>The Rollback Path Is the Real AI Coding Feature</title><link>https://zemna.net/blog/ai-coding-rollback-path/</link><pubDate>Sat, 25 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/ai-coding-rollback-path/</guid><description>&lt;p>The most dangerous AI-generated change is not the obviously bad one. It is the tidy diff that passes a happy-path test, reaches staging, and leaves nobody able to answer one operational question: &lt;strong>how do we get back?&lt;/strong>&lt;/p>
&lt;p>A model can produce a controller, a migration, a queue handler, and a deployment file in minutes. That does not make the combined change reversible. The question is not whether this demos well; it is whether it survives maintenance, handoff, and local constraints. If the answer depends on someone remembering a chat transcript or improvising SQL during an incident, the change was not ready to merge.&lt;/p>
&lt;p>This is a backend and infrastructure discipline, not an anti-AI argument. AI coding is useful when it reduces the cost of drafting a bounded implementation. It becomes risky when the speed of drafting hides the fact that a change crosses several recovery boundaries at once: application behavior, schema shape, data backfill, asynchronous work, configuration, and deployment.&lt;/p></description></item><item><title>Audit the resume boundary: verify a coding agent from repository evidence</title><link>https://zemna.net/blog/audit-agent-resume/</link><pubDate>Thu, 23 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/audit-agent-resume/</guid><description>&lt;p>A transcript tells you what an agent said it did. A repository tells you what it left behind.&lt;/p>
&lt;p>That difference matters when a coding-agent session ends at an inconvenient point: the terminal disconnects, a context window closes, a machine reboots, a CI command hangs, or someone simply returns the next day. The tempting move is to resume and ask, &amp;ldquo;Where were we?&amp;rdquo; That question invites a narrative. It does not establish a safe starting point.&lt;/p></description></item><item><title>Vue Query Cache Invalidation Is a Contract, Not a Refetch Button</title><link>https://zemna.net/blog/vue-query-cache-invalidation-contract/</link><pubDate>Wed, 22 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/vue-query-cache-invalidation-contract/</guid><description>&lt;p>A save can return &lt;code>200 OK&lt;/code>, the detail view can show the new value, and the list that brought the operator there can still show yesterday’s row. That is not a cosmetic bug. In an admin screen, the list is often the operational truth: who is assigned, which invoice is overdue, whether an account is active, or which queue needs attention.&lt;/p>
&lt;p>The usual response is to add a refetch button. That hides the decision that caused the stale screen: nobody owns the relationship between a mutation and the query keys it changes. The question is not whether this demos well; it is whether it survives maintenance, handoff, and local constraints.&lt;/p>
&lt;p>TanStack Query already gives us the primitives: query keys, invalidation, mutation callbacks, optimistic updates, cancellation, snapshots, and hydration. Its documentation describes invalidation as marking matching queries stale and refetching active observers; mutation callbacks are where teams connect a completed write to that work. &lt;a href="https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation">Source: TanStack Query invalidation guide&lt;/a> &lt;a href="https://tanstack.com/query/v5/docs/framework/react/guides/invalidations-from-mutations">Source: mutations guide&lt;/a>&lt;/p>
&lt;p>The hard part is not calling an API. It is writing a contract that says which cached facts a write invalidates, which screen owns the policy, what gets restored after failure, and how server-rendered state joins the browser cache. This is a practical companion to the maintenance boundaries in &lt;a href="https://zemna.net/laravel-vue-saas/">Laravel + Vue SaaS&lt;/a> and the verification habits collected in &lt;a href="https://zemna.net/developer-tools/">Developer Tools&lt;/a>.&lt;/p></description></item><item><title>ApexCharts 6.4 Is a Dashboard Contract Migration, Not a Package Bump</title><link>https://zemna.net/blog/apexcharts-6-4-dashboard-contract/</link><pubDate>Tue, 21 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/apexcharts-6-4-dashboard-contract/</guid><description>&lt;p>A charting dependency can change a dashboard without changing a single API endpoint. That is the uncomfortable part of an ApexCharts 6.4 upgrade.&lt;/p>
&lt;p>The release was published on 2026-07-20, and it changes the rules that turn a heatmap data point into pixels, interaction, and labels. It also introduces an opt-in Canvas renderer with its own import requirement and fallback behavior. Those are dashboard contracts: visible agreements between your data, your configuration, and the people who use the screen. &lt;a href="https://github.com/apexcharts/apexcharts.js/releases/tag/v6.4.0">The v6.4.0 release notes&lt;/a> are the source for those changes.&lt;/p></description></item><item><title>Git trailers turn a commit into a handoff contract</title><link>https://zemna.net/blog/git-trailers-handoff-contract/</link><pubDate>Mon, 20 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/git-trailers-handoff-contract/</guid><description>&lt;p>A commit already records code and a message. For a team that hands work between people, time zones, and automated checks, that is often not enough. Someone needs to know what changed, what verified it, where the review discussion lives, and whether there is a follow-up that should not vanish into a chat thread.&lt;/p></description></item><item><title>AI Agent Frameworks in 2026 — What the Comparison Charts Don't Tell You</title><link>https://zemna.net/blog/ai-agent-frameworks-in-2026-what-the-comparison-charts-dont-tell-you/</link><pubDate>Sun, 19 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/ai-agent-frameworks-in-2026-what-the-comparison-charts-dont-tell-you/</guid><description>&lt;p>The comparison chart is already lying to you the moment it asks which agent framework is “best.” It turns an operational decision into a shopping decision: features in rows, names in columns, and a tidy winner at the bottom.&lt;/p></description></item><item><title>Treat frontend file watching as a maintenance contract</title><link>https://zemna.net/blog/frontend-file-watch-reliability/</link><pubDate>Sat, 18 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/frontend-file-watch-reliability/</guid><description>&lt;p>A dev server that misses filesystem events is not a frontend nuisance. It is an unreliable feedback loop.&lt;/p>
&lt;p>The visible symptom looks small: a component changes on disk and the browser does not update. The operational cost is larger. A developer edits again, refreshes manually, restarts the server, doubts the CSS, then doubts the build. The system has stopped giving a trustworthy answer to a simple question: “did the change I made reach the page I am looking at?”&lt;/p></description></item><item><title>A Coding-Agent Policy Is Change Control, Not a Settings File</title><link>https://zemna.net/blog/coding-agent-policy-change-control/</link><pubDate>Fri, 17 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/coding-agent-policy-change-control/</guid><description>&lt;p>A repository instruction file is easy to underestimate. It looks like a convenient place to put a test command, a style preference, and a warning about migrations. That framing is wrong once a coding agent can open pull requests, change schemas, edit infrastructure, or run commands on behalf of a team.&lt;/p></description></item><item><title>Laravel Image Processing Is an Adoption Contract, Not an Upload Feature</title><link>https://zemna.net/blog/laravel-image-processing-adoption-contract/</link><pubDate>Thu, 16 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/laravel-image-processing-adoption-contract/</guid><description>&lt;p>A file input and &lt;code>Storage::putFile()&lt;/code> can make image work look finished. The browser accepts a JPEG, the application stores it, and a page renders something back. That is a demo.&lt;/p>
&lt;p>The production question is different: what does the rest of the application get to rely on after an image arrives?&lt;/p></description></item><item><title>The Request Boundary Is Your Compatibility Contract</title><link>https://zemna.net/blog/request-boundary-compatibility-contract/</link><pubDate>Wed, 15 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/request-boundary-compatibility-contract/</guid><description>&lt;p>A request is not a bag of values that happens to arrive at a controller. It is a compatibility promise between at least four pieces of software: the browser that sends intent, the Laravel code that interprets it, any queued work that outlives the HTTP request, and the client code that consumes the next response.&lt;/p></description></item><item><title>A Small Diff Is Not a Small Change: Review AI Patches by Code Entity</title><link>https://zemna.net/blog/semantic-code-entity-review/</link><pubDate>Tue, 14 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/semantic-code-entity-review/</guid><description>&lt;p>A coding agent changes seven lines in a validator. The pull request looks safe. The reviewer sees a small diff, a green test run, and a tidy commit message.&lt;/p>
&lt;p>But the validator feeds a controller, the controller builds a queue payload, and a fixture still describes the old rejection shape. The diff is seven lines. The behavior boundary is not.&lt;/p>
&lt;p>That is the review mistake I see most often with AI-assisted changes: treating line count as a proxy for change size. A diff is an excellent record of text edits. It is not automatically a map of the functions, classes, callers, tests, and rollback points that now have a different contract. The question is not whether this demos well; it is whether it survives maintenance, handoff, and local constraints.&lt;/p></description></item><item><title>Strict TypeScript Is a Migration Contract, Not a Compiler Nuisance</title><link>https://zemna.net/blog/typescript-migration-contract/</link><pubDate>Mon, 13 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/typescript-migration-contract/</guid><description>&lt;p>A UI utility upgrade rejects &lt;code>undefined&lt;/code>. The pull request looks harmless: replace a few object literals, widen a type, move on. Then the same error appears in a prop adapter, an API mapper, a saved-filter serializer, and a test fixture. The compiler is not being difficult. It found an ownership decision that the codebase never wrote down.&lt;/p>
&lt;p>That is why strict TypeScript work deserves a migration plan. &lt;code>exactOptionalPropertyTypes&lt;/code> draws a real line between a property that is absent and a property that is present with &lt;code>undefined&lt;/code>. The question is not whether an editor can silence the error. The question is which layer owns the meaning of absence, default, and invalid input. The question is not whether this demos well; it is whether it survives maintenance, handoff, and local constraints.&lt;/p></description></item><item><title>Vue 3.6 Vapor Mode: Build a Pilot Harness Before You Change a Rendering Path</title><link>https://zemna.net/blog/vue-3-6-vapor-mode-no-virtual-dom-no-rewrite/</link><pubDate>Sat, 11 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/vue-3-6-vapor-mode-no-virtual-dom-no-rewrite/</guid><description>&lt;p>Vue 3.6 Vapor Mode is worth watching for a practical reason: it gives a mature Vue application a compiler-oriented rendering path to evaluate without committing the whole product to a rewrite. That is a useful option for teams with a real performance complaint, an established test suite, and enough operational discipline to compare a change against a baseline.&lt;/p></description></item><item><title>Grok 4.5 Is a Coding-Agent Moment Only If It Survives the Harness</title><link>https://zemna.net/blog/grok-4-5-developer-verification-harness/</link><pubDate>Fri, 10 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/grok-4-5-developer-verification-harness/</guid><description>Grok 4.5 is available through the xAI API for coding, agentic tasks, and knowledge work. Before moving coding-agent work to a new frontier model, verify behavior against your own repos, tools, budgets, and rollback needs.</description></item><item><title>Wrangler Now Treats AI-Agent Static Deploys Differently: Verify the Target, Not Just Success</title><link>https://zemna.net/blog/wrangler-now-treats-ai-agent-static-deploys-differently-verify-the-target-not-just-success/</link><pubDate>Thu, 09 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/wrangler-now-treats-ai-agent-static-deploys-differently-verify-the-target-not-just-success/</guid><description>&lt;p>A deploy command can exit zero and still leave you looking in the wrong product.&lt;/p>
&lt;p>That is the interesting part of Wrangler 4.108.0. Cloudflare did not ship a loud “AI deployment platform” announcement. It shipped a small release-note line with a large operational consequence: when &lt;code>wrangler pages deploy&lt;/code> or &lt;code>wrangler pages project create&lt;/code> is run by an AI coding agent against a brand-new, purely static project, Wrangler can delegate that deploy to Workers static assets instead of Cloudflare Pages. The question is not whether this demos well; it is whether your deployment contract survives maintenance, handoff, and local constraints.&lt;/p></description></item><item><title>TanStack Table v9 Row-Model Workers: What Breaks When Your Grid Crosses postMessage?</title><link>https://zemna.net/blog/tanstack-table-v9-row-model-workers/</link><pubDate>Wed, 08 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/tanstack-table-v9-row-model-workers/</guid><description>&lt;p>TanStack Table v9 beta did something that looks small in a changelog and large in a production grid: &lt;code>v9.0.0-beta.33&lt;/code> added &lt;strong>web workers for row models&lt;/strong>. Less than two days later, &lt;code>v9.0.0-beta.34&lt;/code>, &lt;code>beta.35&lt;/code>, and &lt;code>beta.36&lt;/code> followed with sorting optimizations, sticky pinned-column documentation, cell-order performance work, touch-cancel cleanup, and extra tests. [Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.33] [Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.34] [Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.35] [Source: https://github.com/TanStack/table/releases/tag/v9.0.0-beta.36]&lt;/p>
&lt;p>That sequence is the signal. Workerized row models are not a speed button. They are a boundary change. The moment a table&amp;rsquo;s row pipeline crosses &lt;code>postMessage&lt;/code>, your migration moves from &amp;ldquo;upgrade a package&amp;rdquo; to &amp;ldquo;prove serialization, determinism, adapter behavior, SSR fallback, and rollback.&amp;rdquo; The question is not whether the demo scrolls smoothly; it is whether the grid survives maintenance, handoff, and local constraints after the first bug report.&lt;/p></description></item><item><title>A Repeatable Review Harness for AI-Generated Code After the First Merge Gate</title><link>https://zemna.net/blog/a-repeatable-review-harness-for-ai-generated-code-after-the-first-merge-gate/</link><pubDate>Tue, 07 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/a-repeatable-review-harness-for-ai-generated-code-after-the-first-merge-gate/</guid><description>&lt;p>The first merge gate is where teams feel safe too early.&lt;/p>
&lt;p>An AI agent opens a pull request. The code compiles. The unit tests pass. A human reviewer scans the diff, asks for two naming changes, and merges. Everyone moves on because the gate did its job.&lt;/p>
&lt;p>Then the slower problems start. The new route bypasses an old authorization check. A background job retries forever because the agent copied the wrong timeout pattern. The test passes because it asserts a mocked response, not the real boundary. The rollback plan is missing because the change looked small. Three weeks later, another agent edits the same area and treats that weak pattern as local convention.&lt;/p>
&lt;p>That is the part I care about. The question is not whether AI-generated code can pass review. The question is whether it survives maintenance, handoff, production rollback, and the next agent that touches it. Working as a software architect in Indonesia makes this practical rather than theoretical: vendor callbacks, deployment windows, and handoff notes decide whether a change survives the week.&lt;/p>
&lt;p>This post is about the harness I want after the first merge gate: a repeatable, boring, evidence-heavy review loop for AI-generated code. Not another proof checklist. The proof checklist belongs before merge. This one runs after the first gate and keeps running while the code becomes part of the system.&lt;/p></description></item><item><title>Developer Tools and Model Choices</title><link>https://zemna.net/developer-tools/</link><pubDate>Mon, 06 Jul 2026 21:52:00 +0700</pubDate><guid>https://zemna.net/developer-tools/</guid><description>&lt;p>Developer tools are not neutral. They shape review habits, cost structure, portability, and how quickly a small team can recover when a workflow breaks.&lt;/p>
&lt;p>This hub collects the posts where the question is not &amp;ldquo;is the tool impressive?&amp;rdquo; but &amp;ldquo;what does this tool make easier or harder to operate?&amp;rdquo;&lt;/p></description></item><item><title>Laravel and Vue SaaS Notes</title><link>https://zemna.net/laravel-vue-saas/</link><pubDate>Mon, 06 Jul 2026 21:51:00 +0700</pubDate><guid>https://zemna.net/laravel-vue-saas/</guid><description>&lt;p>This is the engineering lens I use when writing about Laravel, Vue, cron jobs, and small SaaS systems.&lt;/p>
&lt;p>The public posts are not framework tutorials in isolation. They are notes about the seams that usually break in real products: scheduled jobs, generated files, state ownership, UI correctness, upgrade paths, and rollback discipline.&lt;/p></description></item><item><title>AI Agent Operations</title><link>https://zemna.net/ai-agent-operations/</link><pubDate>Mon, 06 Jul 2026 21:50:00 +0700</pubDate><guid>https://zemna.net/ai-agent-operations/</guid><description>&lt;p>AI agents are useful when they become part of operations, not when they only produce impressive transcripts.&lt;/p>
&lt;p>This hub collects the posts on agent boundaries, cron verification, repository maps, model portability, and artifact proof. Read these as a working system: each article answers one failure mode in the same operating loop.&lt;/p></description></item><item><title>Uses</title><link>https://zemna.net/uses/</link><pubDate>Mon, 06 Jul 2026 21:06:00 +0700</pubDate><guid>https://zemna.net/uses/</guid><description>&lt;p>This page documents the working setup behind zemna.net. It is not a sponsored tool list. It is the stack I use, test, or write about.&lt;/p>
&lt;h2 id="writing-and-publishing">Writing and publishing&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Area&lt;/th>
 &lt;th>Current choice&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Site generator&lt;/td>
 &lt;td>Hugo extended&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hosting&lt;/td>
 &lt;td>Cloudflare Pages&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Domain&lt;/td>
 &lt;td>zemna.net&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Source control&lt;/td>
 &lt;td>GitHub&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Writing format&lt;/td>
 &lt;td>Markdown with Hugo frontmatter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Public feed&lt;/td>
 &lt;td>RSS and JSON feed&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Design system&lt;/td>
 &lt;td>Token-driven custom Hugo theme&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>The site is intentionally static. Static pages keep the operational surface small: no database, no login, no server runtime, and fewer moving parts to debug.&lt;/p></description></item><item><title>Start Here</title><link>https://zemna.net/start-here/</link><pubDate>Mon, 06 Jul 2026 21:05:00 +0700</pubDate><guid>https://zemna.net/start-here/</guid><description>&lt;p>zemna.net is not a news feed. It is a working notebook for software systems that need to survive real maintenance.&lt;/p>
&lt;p>If you are new here, read these paths in order.&lt;/p>
&lt;h2 id="1-agent-boundaries">1. Agent boundaries&lt;/h2>
&lt;p>Start with the contract before the agent touches a repository.&lt;/p></description></item><item><title>Zero-Cost Observability for Agent Crons</title><link>https://zemna.net/blog/zero-cost-observability-agent-crons/</link><pubDate>Mon, 06 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/zero-cost-observability-agent-crons/</guid><description>&lt;p>A cron job that says &lt;code>success&lt;/code> can still leave no customer email sent, no blog post published, no report attached, and no safe way back. AI-assisted development makes that failure mode sharper: the agent often completes the instruction it was given, while the system around it has no proof that the work is usable.&lt;/p>
&lt;p>The fix is not a larger dashboard on day one. Before adding another paid automation layer, prove three things on the smallest possible surface: one artifact, one alert, and one rollback path. The question is not whether this demos well; it is whether it survives maintenance, handoff, and local constraints.&lt;/p>
&lt;p>This post is for solo maintainers and small teams running AI agents, cron jobs, Laravel/Vue SaaS flows, content pipelines, and internal scripts that already have enough moving parts. The goal is zero-cost observability first: local files, structured logs, a shell verifier, GitHub Actions, and free developer or usage-based free tiers only when they earn their place.&lt;/p></description></item><item><title>Closed Loop Beats More Automation</title><link>https://zemna.net/blog/closed-loop-beats-more-automation/</link><pubDate>Sun, 05 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/closed-loop-beats-more-automation/</guid><description>&lt;p>A broken automation system is easy to recognize when it crashes. The dangerous one exits cleanly, posts a green check, and teaches you nothing. That is the failure mode I keep seeing in agent workflows, content pipelines, scheduled jobs, and developer tooling: the work runs, the logs look polite, and the system forgets to learn from the thing that just happened.&lt;/p></description></item><item><title>The Agent Edit Contract I Use Before a Coding Agent Touches a Repo</title><link>https://zemna.net/blog/the-agent-edit-contract-i-use-before-a-coding-agent-touches-a-repo/</link><pubDate>Sat, 04 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/the-agent-edit-contract-i-use-before-a-coding-agent-touches-a-repo/</guid><description>&lt;p>A coding agent should not get write access just because the prompt sounds clear.&lt;/p>
&lt;p>The prompt is the weakest part of the workflow. It is conversational, lossy, and too easy to expand while nobody is watching. The contract has to sit outside the chat: a short file that says what the agent may inspect, what it may change, what command proves the change, what artifact must exist, and how to roll back if the run gets clever in the wrong direction.&lt;/p></description></item><item><title>Your Coding Agent Needs a Map, Not a Bigger Context Window</title><link>https://zemna.net/blog/your-coding-agent-needs-a-map-not-a-bigger-context-window/</link><pubDate>Fri, 03 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/your-coding-agent-needs-a-map-not-a-bigger-context-window/</guid><description>&lt;p>A coding agent with a very large context window can still make the wrong change in the wrong file.&lt;/p>
&lt;p>That sounds harsh until you watch the failure happen. The agent reads half the repository, quotes three irrelevant modules back at you, edits the visible function, and misses the caller that actually owns the invariant. The problem was never that the window was too small. The problem was that the agent had no map.&lt;/p></description></item><item><title>Before You Adopt a Beta Library, Prove the Exit Path</title><link>https://zemna.net/blog/before-you-adopt-a-beta-library-prove-the-exit-path/</link><pubDate>Thu, 02 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/before-you-adopt-a-beta-library-prove-the-exit-path/</guid><description>&lt;p>A beta dependency feels safe right up to the moment it becomes load-bearing.&lt;/p>
&lt;p>The release note looks small: one adapter added, one selection bug fixed, one worker event improved, one alpha runtime tagged. You pin it in a branch, the demo works, and the pull request starts to look boring. That is the dangerous part. Boring demos hide the work that matters: how to reverse the change, how to prove the migration surface, how to stop a beta from leaking into production by accident, and how to keep the team honest after the first successful run.&lt;/p></description></item><item><title>Code That Renders Is Not Code You Can Trust</title><link>https://zemna.net/blog/code-that-renders-is-not-code-you-can-trust/</link><pubDate>Wed, 01 Jul 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/code-that-renders-is-not-code-you-can-trust/</guid><description>&lt;p>A developer asks an AI agent to build a settings modal. Thirty seconds later, there is a modal on screen. It opens when you click a button. It closes when you click the backdrop. It looks clean. The developer merges the pull request.&lt;/p></description></item><item><title>Your Cron Job Is Not Healthy Until the Artifact Proves It</title><link>https://zemna.net/blog/your-cron-job-is-not-healthy-until-the-artifact-proves-it/</link><pubDate>Tue, 30 Jun 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/your-cron-job-is-not-healthy-until-the-artifact-proves-it/</guid><description>&lt;p>Your cron job exited zero. Fine. Where is the artifact?&lt;/p>
&lt;p>That is the question I want on every scheduled job review now. Not &amp;ldquo;did the process run?&amp;rdquo; Not &amp;ldquo;did the dashboard stay green?&amp;rdquo; Not &amp;ldquo;did the log avoid a stack trace?&amp;rdquo; A scheduled job is healthy only when the expected artifact exists, is fresh, and passes a domain check. Anything weaker is process theater.&lt;/p></description></item><item><title>Open Weights Just Ate the API Margin — What a Pragmatic Dev Actually Does on Monday</title><link>https://zemna.net/blog/open-weights-api-margin/</link><pubDate>Mon, 29 Jun 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/open-weights-api-margin/</guid><description>&lt;p>&lt;figure class="zn-prose__figure">
 &lt;picture>
 &lt;source srcset="https://zemna.net/covers/open-weights-api-margin.webp" type="image/webp">
 &lt;img src="https://zemna.net/covers/open-weights-api-margin.png" alt="Open weights vs API margin" loading="lazy" decoding="async">
 &lt;/picture>&lt;/figure>
&lt;/p>
&lt;p>If you&amp;rsquo;ve been shipping LLM-powered features for the last two years, you&amp;rsquo;ve felt it: the API bill that started as a rounding error and is now a line item your CFO asks about in weekly meetings. The model providers built beautiful APIs, priced them per token, and we all built businesses on top of usage-based pricing that only goes in one direction — up.&lt;/p></description></item><item><title>The 5 Cron Patterns That Lie to You — Exit 0 Doesn't Mean Success</title><link>https://zemna.net/blog/the-5-cron-patterns-that-lie-to-you-exit-0-doesnt-mean-success/</link><pubDate>Sun, 28 Jun 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/the-5-cron-patterns-that-lie-to-you-exit-0-doesnt-mean-success/</guid><description>&lt;p>You set up a cron job — let&amp;rsquo;s say a database backup. It runs every night at 2 AM. The exit code is always 0. The dashboard is green. Six months go by. Then one day you need to restore, and the backup file is 0 bytes. The job never failed. It just never actually worked.&lt;/p></description></item><item><title>The JavaScript I Deleted With CSS: A 2026 Survival Guide</title><link>https://zemna.net/blog/the-javascript-i-deleted-with-css-a-2026-survival-guide/</link><pubDate>Sat, 27 Jun 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/the-javascript-i-deleted-with-css-a-2026-survival-guide/</guid><description>&lt;p>I deleted 150 lines of JavaScript from a tooltip component last week. The replacement was four lines of CSS. No virtual DOM reconciliation, no &lt;code>requestAnimationFrame&lt;/code> loop, no &lt;code>ResizeObserver&lt;/code> to handle container changes, no &lt;code>scroll&lt;/code> listener for collision detection. The browser does all of it now, natively, on the compositor thread, faster than any framework&amp;rsquo;s JS ever did. The diff went into the PR with a one-line comment: &amp;ldquo;CSS does this.&amp;rdquo;&lt;/p></description></item><item><title>Why AI Cron Jobs Lie to You: The Exit 0 With Empty Output Pattern</title><link>https://zemna.net/blog/why-ai-cron-jobs-lie-to-you-the-exit-0-with-empty-output-pattern/</link><pubDate>Fri, 26 Jun 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/why-ai-cron-jobs-lie-to-you-the-exit-0-with-empty-output-pattern/</guid><description>&lt;p>My health check cron ran 180 times over six hours. Exit code 0 every time. Green
dashboard. And you never once asked. Neither did I — until I checked the output and
found six hours of perfect silence.&lt;/p></description></item><item><title>I Swapped My LLM Backend — The API Call Worked on the First Try</title><link>https://zemna.net/blog/i-swapped-my-llm-backend-the-api-call-worked-on-the-first-try/</link><pubDate>Thu, 25 Jun 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/i-swapped-my-llm-backend-the-api-call-worked-on-the-first-try/</guid><description>&lt;h1 id="i-swapped-my-llm-backend--the-api-call-worked-on-the-first-try">I Swapped My LLM Backend — The API Call Worked on the First Try&lt;/h1>
&lt;p>The migration took eleven minutes. The fallout took three weeks. I switched our production agent from GPT-4o to a cheaper model on a Tuesday afternoon, confirmed the health check passed, and pushed it live. The API returned 200. The response body was valid JSON. Everything looked fine. It was not fine. Over the next twenty-one days, I watched our error rate climb 340%, our RAG pipeline silently return garbage, and our &amp;ldquo;deterministic&amp;rdquo; eval suite give everything a pass because it was never actually measuring what I thought it was. This post is about what breaks when you swap an LLM mid-project — not the API layer, which is the easy part, but everything built on top of it.&lt;/p></description></item><item><title>Your AI Agent Pipeline Has No Zombie Detection — Here's How to Add It</title><link>https://zemna.net/blog/your-ai-agent-pipeline-has-no-zombie-detection-heres-how-to-add-it/</link><pubDate>Wed, 24 Jun 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/your-ai-agent-pipeline-has-no-zombie-detection-heres-how-to-add-it/</guid><description>&lt;p>I ran an autonomous task system for three months before I realized it had a blind spot the size of a barn door. Processes were &amp;ldquo;healthy.&amp;rdquo; CPU was fine. Memory was fine. And yet, nothing was getting done.&lt;/p></description></item><item><title>The AI Coding Agent Arms Race — Why Model Portability Matters More Than Benchmarks</title><link>https://zemna.net/blog/ai-agent-frameworks-in-2026-what-the-comparison-charts-dont-tell-you/</link><pubDate>Tue, 23 Jun 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/ai-agent-frameworks-in-2026-what-the-comparison-charts-dont-tell-you/</guid><description>&lt;p>A coding-agent migration often starts with a model comparison, then fails somewhere much less glamorous: a test command was stored in a chat preset, a deployment warning existed only in one tool’s dashboard, or a local override quietly changed what the agent was allowed to touch.&lt;/p>
&lt;p>That is the wrong place to discover the real dependency. The durable asset is not a model setting. It is the repository-owned release policy: which paths are protected, which command verifies a change, who approves an exception, what evidence must survive review, and how the team returns to safety when a tool behaves badly.&lt;/p>
&lt;p>The question is not whether a coding agent demos well; it is whether its work survives maintenance, handoff, and local constraints.&lt;/p></description></item><item><title>Autonomous AI Agents on Cron: A Zombie-Task Postmortem</title><link>https://zemna.net/blog/autonomous-ai-agents-on-cron-a-zombie-task-postmortem/</link><pubDate>Sat, 20 Jun 2026 08:00:00 +0700</pubDate><guid>https://zemna.net/blog/autonomous-ai-agents-on-cron-a-zombie-task-postmortem/</guid><description>&lt;p>Two weeks ago I woke up to a green dashboard. Every cron job had fired, every task reported &lt;code>status: completed&lt;/code>, every heartbeat was healthy. The content pipeline I run — research, draft, fact-check, design, publish — had apparently done its job overnight while I slept.&lt;/p></description></item><item><title>Long-Running AI Agents — From Demos to Production</title><link>https://zemna.net/blog/long-running-ai-agents-from-demos-to-production/</link><pubDate>Fri, 19 Jun 2026 07:00:00 +0700</pubDate><guid>https://zemna.net/blog/long-running-ai-agents-from-demos-to-production/</guid><description>&lt;p>The session was still green. The agent had typed &lt;code>cd backend&lt;/code>, kept editing files, and wrote &amp;ldquo;done&amp;rdquo; in the log. Nobody on the ticket could say whether the sandbox moved with the working directory.&lt;/p>
&lt;p>That is the failure I now refuse. A long-running agent is not production because it stayed up overnight. It is production when the boundary still holds after a directory change, after a nested helper spends tokens, and after a tool result is rewritten before the model sees it.&lt;/p>
&lt;p>The question is not whether this demos well. The question is whether it survives maintenance, handoff, and the next person who has to roll the branch back.&lt;/p></description></item><item><title>OpenCode Dethroned Cursor — and Claude Fable 5 Got Suspended. What June 2026's AI Coding Shakeup Means for You</title><link>https://zemna.net/blog/opencode-fable5-ai-coding-shakeup/</link><pubDate>Thu, 18 Jun 2026 08:00:00 +0700</pubDate><guid>https://zemna.net/blog/opencode-fable5-ai-coding-shakeup/</guid><description>&lt;p>June 2026 is the month the AI coding agent landscape cracked open. Two stories broke within days of each other, and both have real implications for how you build software.&lt;/p>
&lt;p>&lt;strong>Story one:&lt;/strong> OpenCode hit 160,000 GitHub stars and 7.5 million monthly active users, displacing Cursor from the #1 spot in LogRocket&amp;rsquo;s AI dev tool power rankings. It&amp;rsquo;s open source, it&amp;rsquo;s model-agnostic, and it runs in your terminal.&lt;/p></description></item><item><title>GLM 5.2 Just Became the Best Open-Weights Model — Here's What That Means</title><link>https://zemna.net/blog/glm-5-2-open-weights/</link><pubDate>Wed, 17 Jun 2026 18:28:00 +0700</pubDate><guid>https://zemna.net/blog/glm-5-2-open-weights/</guid><description>&lt;p>Zhipu AI (Z.AI) dropped GLM 5.2 on June 13, 2026, and it didn&amp;rsquo;t just raise the bar for open-weights models — it grabbed the bar and threw it across the room. With a 744B Mixture-of-Experts architecture, a usable 1-million-token context window, and an MIT license, this is the strongest argument yet that you don&amp;rsquo;t need a closed API to run frontier-level AI.&lt;/p></description></item><item><title>How I Dissected 54 Design Systems and Transplanted Linear.app DNA Into My Hugo Blog in One Day</title><link>https://zemna.net/blog/linear-design-system-hugo/</link><pubDate>Tue, 16 Jun 2026 18:00:00 +0700</pubDate><guid>https://zemna.net/blog/linear-design-system-hugo/</guid><description>&lt;p>This morning I opened my blog and thought: this looks like a 2015 Tistory template. So I told my Hermes agent, in exactly these words: &amp;ldquo;This looks like garbage. Go through all 54 design systems and rebuild it with the best one.&amp;rdquo;&lt;/p></description></item><item><title>Colophon</title><link>https://zemna.net/colophon/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://zemna.net/colophon/</guid><description>&lt;h1 id="colophon">Colophon&lt;/h1>
&lt;p>This page documents the &lt;strong>how&lt;/strong> of zemnanet — what the site is built with, what tools run it, and why those choices were made.&lt;/p>
&lt;h2 id="stack">Stack&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Layer&lt;/th>
 &lt;th>Tool&lt;/th>
 &lt;th>Why&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Site generator&lt;/strong>&lt;/td>
 &lt;td>&lt;a href="https://gohugo.io/">Hugo&lt;/a> v0.140 extended&lt;/td>
 &lt;td>Go single binary, sub-second builds, no Node toolchain&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Hosting&lt;/strong>&lt;/td>
 &lt;td>&lt;a href="https://pages.cloudflare.com/">Cloudflare Pages&lt;/a>&lt;/td>
 &lt;td>Free, global CDN, zemna.net already on Cloudflare&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Domain&lt;/strong>&lt;/td>
 &lt;td>&lt;a href="https://zemna.net">zemna.net&lt;/a> on Cloudflare Registrar&lt;/td>
 &lt;td>Same provider, no extra DNS hops&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Content&lt;/strong>&lt;/td>
 &lt;td>Markdown + Hugo frontmatter&lt;/td>
 &lt;td>Plain text, version-controlled, AI-friendly&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Versioning&lt;/strong>&lt;/td>
 &lt;td>Git on &lt;a href="https://github.com/zemnanet">GitHub&lt;/a>&lt;/td>
 &lt;td>Public history, no vendor lock-in&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Deployment&lt;/strong>&lt;/td>
 &lt;td>git push to &lt;code>main&lt;/code> → Cloudflare Pages build&lt;/td>
 &lt;td>Zero-config CI; the only &amp;ldquo;ops&amp;rdquo; is git&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Search&lt;/strong>&lt;/td>
 &lt;td>Pagefind (planned)&lt;/td>
 &lt;td>Static index, runs at build time, no JS framework&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="typography">Typography&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Inter&lt;/strong> for UI, navigation, and English headlines — open source, broad weight range, readable at small sizes.&lt;/li>
&lt;li>&lt;strong>Noto Serif KR&lt;/strong> for Korean editorial content — open source, designed for the Korean script.&lt;/li>
&lt;li>&lt;strong>JetBrains Mono&lt;/strong> for code — ligatures for &lt;code>=&amp;gt;&lt;/code> and &lt;code>!=&lt;/code>, tabular numerals.&lt;/li>
&lt;/ul>
&lt;h2 id="colors">Colors&lt;/h2>
&lt;p>Three brand colors, derived from a synthesis of &lt;strong>Korean hanji&lt;/strong> (off-white) and &lt;strong>Indonesian batik&lt;/strong> (navy + vermilion). Full palette and semantic mappings live in the &lt;a href="https://zemna.net/design-system/">design system&lt;/a>.&lt;/p></description></item><item><title>Design System</title><link>https://zemna.net/design-system/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://zemna.net/design-system/</guid><description>&lt;h1 id="zemnanet-design-system">zemnanet Design System&lt;/h1>
&lt;p>A single source of truth for the &lt;strong>visual&lt;/strong> and &lt;strong>typographic&lt;/strong> language used across:&lt;/p>
&lt;ul>
&lt;li>zemna.net (this website)&lt;/li>
&lt;li>Blog posts and series&lt;/li>
&lt;li>Social media graphics (IG carousels, X headers, podcast covers)&lt;/li>
&lt;li>Any presentation, README, or tool that says &amp;ldquo;zemnanet&amp;rdquo; on it&lt;/li>
&lt;/ul>
&lt;p>The system is &lt;strong>token-first&lt;/strong> — every visual decision is a named token. No hard-coded values in templates. No drift between surfaces.&lt;/p></description></item><item><title>The quiet consolidation in Korean dev tooling</title><link>https://zemna.net/blog/the-quiet-consolidation-in-korean-dev-tooling/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://zemna.net/blog/the-quiet-consolidation-in-korean-dev-tooling/</guid><description>&lt;p>The Korean developer-tools market has historically been fragmented. Dozens of small teams shipping one thing well, each with its own API, its own auth model, its own quirks. That era is ending.&lt;/p>
&lt;p>In the last twelve months, three acquisitions have reshaped the landscape:&lt;/p></description></item><item><title>Stop reaching for a state library</title><link>https://zemna.net/blog/stop-reaching-for-a-state-library/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://zemna.net/blog/stop-reaching-for-a-state-library/</guid><description>&lt;p>Every JavaScript codebase I&amp;rsquo;ve touched in the last two years has had a state-management library somewhere in &lt;code>package.json&lt;/code>. Most of them didn&amp;rsquo;t need one.&lt;/p>
&lt;p>Here&amp;rsquo;s the test: &lt;strong>does your state need to outlive a single function call?&lt;/strong> If the answer is &amp;ldquo;no&amp;rdquo; for 80% of your state, you don&amp;rsquo;t need a library. You need a function.&lt;/p></description></item><item><title>The Indonesian developer scene is having its Linux moment</title><link>https://zemna.net/blog/the-indonesian-developer-scene-is-having-its-linux-moment/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><guid>https://zemna.net/blog/the-indonesian-developer-scene-is-having-its-linux-moment/</guid><description>&lt;p>I&amp;rsquo;ve been in Jakarta long enough to have watched the Indonesian tech scene go through three hype cycles. The current one is different — and more durable — because it&amp;rsquo;s not being driven by outside attention.&lt;/p></description></item><item><title>A pragmatic review of Hermes Agent after six months</title><link>https://zemna.net/blog/a-pragmatic-review-of-hermes-agent-after-six-months/</link><pubDate>Sat, 13 Jun 2026 00:00:00 +0000</pubDate><guid>https://zemna.net/blog/a-pragmatic-review-of-hermes-agent-after-six-months/</guid><description>&lt;p>I have been running Hermes as my primary AI agent for about six months. This is a pragmatic review — not sponsored, not a teardown. Just notes from daily use.&lt;/p>
&lt;h2 id="what-it-is-and-what-it-isnt">What it is, and what it isn&amp;rsquo;t&lt;/h2>
&lt;p>Hermes is a &lt;strong>personal AI agent&lt;/strong>. It runs locally, talks to a CLI, a Telegram bot, a Slack workspace, a TUI, and a desktop app — all from the same core. It has skills, memory, scheduled jobs, and the ability to spawn sub-agents.&lt;/p></description></item><item><title>Why I'm no longer worried about AI replacing senior developers</title><link>https://zemna.net/blog/why-im-no-longer-worried-about-ai-replacing-senior-developers/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><guid>https://zemna.net/blog/why-im-no-longer-worried-about-ai-replacing-senior-developers/</guid><description>&lt;p>There&amp;rsquo;s a genre of LinkedIn post that goes: &amp;ldquo;AI will replace senior developers because seniors are slower, more expensive, and refuse to learn new tools.&amp;rdquo;&lt;/p>
&lt;p>I have been watching this play out in actual engineering teams for two years. It is not what&amp;rsquo;s happening.&lt;/p></description></item><item><title>About</title><link>https://zemna.net/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://zemna.net/about/</guid><description>&lt;h2 id="hi-im-shinjae-kang">Hi, I&amp;rsquo;m &lt;strong>Shinjae Kang&lt;/strong>&lt;/h2>
&lt;p>I&amp;rsquo;m a &lt;strong>programmer and software architect&lt;/strong> based in &lt;strong>Cileungsi, Bogor, Indonesia&lt;/strong>. Since &lt;strong>May 2026&lt;/strong> I have been &lt;strong>General Manager, AI Business&lt;/strong> at &lt;strong>PT Fast Beauty Indonesia&lt;/strong>. Public writing and personal projects stay on &lt;a href="https://zemna.net">zemna.net&lt;/a> and GitHub.&lt;/p></description></item><item><title>Portfolio</title><link>https://zemna.net/portfolio/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://zemna.net/portfolio/</guid><description>&lt;h2 id="now">Now&lt;/h2>
&lt;p>&lt;strong>General Manager, AI Business, PT Fast Beauty Indonesia&lt;/strong> (May 2026 – present). Public branding remains programmer and software architect. This page lists &lt;strong>systems I built&lt;/strong>, not employers.&lt;/p>
&lt;h2 id="laravel-saas-projects">Laravel SaaS projects&lt;/h2>
&lt;p>Modoo appears here as a set of software projects I built, not as an employer label. Repositories live under &lt;a href="https://github.com/modoo-id">&lt;code>modoo-id&lt;/code>&lt;/a>. Application repos are private; docs surfaces are public.&lt;/p></description></item></channel></rss>