<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Takashi Matsuyama</title>
    <description>The latest articles on DEV Community by Takashi Matsuyama (@takashimatsuyama).</description>
    <link>https://dev.to/takashimatsuyama</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3996973%2F51c4515b-a3df-433f-9276-13e86fe74ba0.jpg</url>
      <title>DEV Community: Takashi Matsuyama</title>
      <link>https://dev.to/takashimatsuyama</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/takashimatsuyama"/>
    <language>en</language>
    <item>
      <title>Add a Verifiable, Replayable Trail to Your Claude Code Sessions in 5 Minutes</title>
      <dc:creator>Takashi Matsuyama</dc:creator>
      <pubDate>Tue, 14 Jul 2026 02:15:39 +0000</pubDate>
      <link>https://dev.to/takashimatsuyama/add-a-verifiable-replayable-trail-to-your-claude-code-sessions-in-5-minutes-3043</link>
      <guid>https://dev.to/takashimatsuyama/add-a-verifiable-replayable-trail-to-your-claude-code-sessions-in-5-minutes-3043</guid>
      <description>&lt;p&gt;Claude Code is already keeping a diary in your repos. It just keeps it as a pile of vendor logs you will never read back — not readable, not verifiable, and nowhere near your code.&lt;/p&gt;

&lt;p&gt;Five minutes from now, that changes. You won't change anything about your workflow. And your past sessions are included — retroactively.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll have in five minutes
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.basou/&lt;/code&gt; directory next to your code, holding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;handoff.md&lt;/code&gt;, for humans — where things stand, what happened, where to pick up&lt;/li&gt;
&lt;li&gt;per-session &lt;code&gt;events.jsonl&lt;/code&gt; files under &lt;code&gt;.basou/sessions/&lt;/code&gt;, the source of truth — append-only, hash-chained event logs&lt;/li&gt;
&lt;li&gt;and &lt;code&gt;basou verify&lt;/code&gt;, a machine answer to the question "has this record been tampered with?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full picture of basou is in the &lt;a href="https://blog.tak3.jp/en/blog/introducing-basou/" rel="noopener noreferrer"&gt;introduction post&lt;/a&gt;. In one line: it's a local-first harness — a full set of tack — for AI coding agents: a saddle (a declarative workspace), reins (orientation that carries intent), and, underneath both, a replayable record of what the agent actually did. Today is hands-on with that bottom layer only: the &lt;strong&gt;trail&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and Step 1: install and init (30 seconds)
&lt;/h2&gt;

&lt;p&gt;Three prerequisites: Node.js 20.10.0 or later, Claude Code, and a real repository you've been using Claude Code in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @basou/cli
&lt;span class="nb"&gt;cd &lt;/span&gt;path/to/your-repo
basou init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This article is written against basou &lt;strong&gt;v0.34.0&lt;/strong&gt; (as of 2026-07-14).&lt;/p&gt;

&lt;p&gt;One thing to know up front: what ends up in git. By default, basou keeps the noisy parts out of git — the raw event log, internal logs, and the live orientation view — and leaves the durable summaries trackable: the manifest, &lt;code&gt;handoff.md&lt;/code&gt;, &lt;code&gt;decisions.md&lt;/code&gt;, and each session's &lt;code&gt;session.yaml&lt;/code&gt;, so you can review them in a PR. Those summaries carry real work content — session labels, decision titles, the handoff text itself — so if you'd rather commit nothing at all, run &lt;code&gt;basou init --local-only&lt;/code&gt; and the whole &lt;code&gt;.basou/&lt;/code&gt; directory stays gitignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: import what's already there — &lt;code&gt;basou import&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the moment the title promised.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;basou import claude-code &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Imported sessions: 20 path(s) sanitized
Imported 4 session(s) (101 events)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command, and four Claude Code sessions I had run in this repository — 101 events' worth — became a trail, retroactively. &lt;code&gt;--all&lt;/code&gt; just means "everything you can find for this repo"; there is no path to point at and no config to write — basou auto-detects the native logs on disk. If you'd rather look before you leap, add &lt;code&gt;--dry-run&lt;/code&gt; first: it writes nothing and returns a preview — &lt;code&gt;Dry run: would import …&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The finer points and the usual gotchas are in the &lt;a href="https://basou.dev/quickstart/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;. The one thing to take away here: &lt;strong&gt;you changed nothing about your workflow&lt;/strong&gt;, and your history became a trail anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: generate the handoff, read it back — and what "replayable" means
&lt;/h2&gt;

&lt;p&gt;What the import wrote is the &lt;strong&gt;record&lt;/strong&gt;: append-only, hash-chained JSONL under &lt;code&gt;.basou/sessions/&lt;/code&gt;. The human view of it is something you ask for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;basou handoff generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Handoff&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Generated at 2026-07-13T16:31:18.106Z from ses_01KXE534X7..ses_01KXE534XZ&lt;/span&gt;

&lt;span class="gu"&gt;## Current state&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Last session: claude-code 2026-06-14: 23 commands, 3 files (imported) [ses_01KXE534X7]

&lt;span class="gu"&gt;## Latest decision&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; ████████ の AGENTS.md 文書そのもの（散文本文）の言語をどう改定しますか？ … -&amp;gt; 本文は日本語のまま維持 [decision_01KXE534X0]

(3 decisions total — see decisions.md)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The Japanese line under &lt;em&gt;Latest decision&lt;/em&gt; is my data — a decision title — and basou keeps user data verbatim; this is a real workspace of mine, not a staged demo. The view chrome itself defaults to English; declare &lt;code&gt;language: ja&lt;/code&gt; on the anchor repo in the manifest and the headings render in Japanese.)&lt;/p&gt;

&lt;p&gt;This is an excerpt of &lt;code&gt;.basou/handoff.md&lt;/code&gt; — where things stand, what happened last, where to pick up. Point a teammate at it. Point tomorrow's you at it. Point the next agent session at it.&lt;/p&gt;

&lt;p&gt;Now, the part that earns the word &lt;em&gt;replayable&lt;/em&gt; in the title. The Markdown you just read is not the record — it's a &lt;strong&gt;view&lt;/strong&gt;, derived from the record. Which means it's disposable. Let's prove it. I could &lt;code&gt;rm&lt;/code&gt; it, but I'll move it aside so we can diff afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; .basou/handoff.md /tmp/before.md
basou handoff generate
diff /tmp/before.md .basou/handoff.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;4c4
&lt;/span&gt;&lt;span class="gd"&gt;&amp;lt; &amp;gt; Generated at 2026-07-13T16:31:18.106Z from ses_01KXE534X7..ses_01KXE534XZ
&lt;/span&gt;&lt;span class="p"&gt;---
&lt;/span&gt;&lt;span class="gi"&gt;&amp;gt; &amp;gt; Generated at 2026-07-13T16:32:02.596Z from ses_01KXE534X7..ses_01KXE534XZ
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line moved: the generation timestamp. &lt;strong&gt;Everything else came back byte-identical&lt;/strong&gt; — re-derived from &lt;code&gt;events.jsonl&lt;/code&gt;, deterministically, offline, with no LLM involved. Same command, same view, resurrected from the source of truth.&lt;/p&gt;

&lt;p&gt;That's what replayable means here. There is no step-through player — there is a &lt;strong&gt;source log you can re-consume at any time&lt;/strong&gt;, and every human-readable view can be rebuilt from it. The handoff, the decision log, the orientation — each is a cheap projection of one verifiable log. Hand-edit the views if you like; when you want the log's version back, regenerate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: verify — and what "verifiable" means
&lt;/h2&gt;

&lt;p&gt;Peek into a session's &lt;code&gt;events.jsonl&lt;/code&gt; and you'll see each event holding the hash of the one before it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"schema_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"0.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"evt_01KXE534X7…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ses_01KXE534X7…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"occurred_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-06-14T06:10:20.317Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"claude-code-import"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"session_started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"prev_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"12d50f19…2f70014"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"schema_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"0.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"evt_01KXE534X7…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ses_01KXE534X7…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"occurred_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-06-14T06:10:25.021Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"claude-code-import"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"command_executed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"prev_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"793df9dd…330ac00"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is why a machine can answer the question: "has this record been tampered with since it was written?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;basou verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ses_01KXE534X7…  verified (34 events)
ses_01KXE534XJ…  verified (19 events)
ses_01KXE534XV…  verified (45 events)
ses_01KXE534XZ…  verified (3 events)
Sessions: 4 total — 4 verified, 0 unchained, 0 empty, 0 incomplete, 0 in_progress, 0 tampered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape you want is that summary line ending in &lt;code&gt;0 tampered&lt;/code&gt;. If even one session had been altered, it would show up as &lt;code&gt;TAMPERED (...)&lt;/code&gt; and the exit code would be 1 — which means you can put this in a script. And note that the chain has been intact since the moment the import wrote it. There is no blessing step afterwards.&lt;/p&gt;

&lt;p&gt;The trail format itself is vendor-neutral — an adapter reads each tool's native logs, and from there everything lands in the same shape under a common schema. And as you just saw, verification is a local operation against the &lt;code&gt;.basou/&lt;/code&gt; on your disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sessions ahead
&lt;/h2&gt;

&lt;p&gt;Everything so far was about the past. The future is even easier.&lt;/p&gt;

&lt;p&gt;Start the morning with &lt;code&gt;basou orient&lt;/code&gt; — "where was I?" answered from the record. Day to day, &lt;code&gt;basou refresh&lt;/code&gt; is the one-shot that ingests new sessions and refreshes every view. When a design decision lands, &lt;code&gt;basou decision capture&lt;/code&gt;; before you step away, &lt;code&gt;basou note "pick up here next"&lt;/code&gt;. And if you want a session recorded live from the start, &lt;code&gt;basou run claude-code&lt;/code&gt; wraps the whole process.&lt;/p&gt;

&lt;p&gt;If you use Codex: &lt;code&gt;basou import codex --all&lt;/code&gt; — same shape, same kind of chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;Not a dashboard SaaS. Nothing leaves your machine. No LLM calls at runtime. The trail itself lives entirely inside &lt;code&gt;.basou/&lt;/code&gt;, and walking away is as easy as deleting that one directory. If you want to browse locally, there's &lt;code&gt;basou view&lt;/code&gt; (bound to 127.0.0.1 only) — that too is optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;That was the five minutes: install and &lt;code&gt;init&lt;/code&gt;, one &lt;code&gt;import&lt;/code&gt;, &lt;code&gt;handoff generate&lt;/code&gt;, &lt;code&gt;verify&lt;/code&gt;. From here the trail keeps growing next to your code, views can be deleted and resurrected from the source of truth, and tampering gets caught by a machine.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://basou.dev/quickstart/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; and the CLI reference have the rest. If something snags on 0.x, &lt;a href="https://github.com/basou-dev/basou" rel="noopener noreferrer"&gt;issues&lt;/a&gt; are welcome.&lt;/p&gt;

</description>
      <category>basou</category>
      <category>claudecode</category>
      <category>vibecoding</category>
      <category>agents</category>
    </item>
    <item>
      <title>Ending the CLAUDE.md / AGENTS.md / copilot-instructions.md Sprawl — Declare One Source, Verify the Wiring</title>
      <dc:creator>Takashi Matsuyama</dc:creator>
      <pubDate>Sat, 11 Jul 2026 14:25:57 +0000</pubDate>
      <link>https://dev.to/takashimatsuyama/ending-the-claudemd-agentsmd-copilot-instructionsmd-sprawl-declare-one-source-verify-the-1gbb</link>
      <guid>https://dev.to/takashimatsuyama/ending-the-claudemd-agentsmd-copilot-instructionsmd-sprawl-declare-one-source-verify-the-1gbb</guid>
      <description>&lt;p&gt;Open a repository these days and you'll find several nearly identical agent-instruction files sitting side by side — &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;. This post is about ending that sprawl not by hand-syncing it forever, but with declaration and verification. Last time I introduced &lt;a href="https://blog.tak3.jp/en/blog/introducing-basou/" rel="noopener noreferrer"&gt;the harness itself, basou&lt;/a&gt;. This one is about a smaller, duller problem that sits just upstream of it — and that far more people are quietly stepping on.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Yet another instruction file"
&lt;/h2&gt;

&lt;p&gt;Over the past year or two, every AI coding tool has brought its own "agent-instruction file." The project's rules, coding conventions, the things not to do. A file meant to be read by the agent, separate from the human-facing README.&lt;/p&gt;

&lt;p&gt;The trouble is that each tool uses a different filename. Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, Codex reads &lt;code&gt;AGENTS.md&lt;/code&gt;, GitHub Copilot reads &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;. Use more than one of these tools on the same team and you end up with &lt;strong&gt;three copies of the same content&lt;/strong&gt; in the repo.&lt;/p&gt;

&lt;p&gt;And they rot in silence. You fix one and leave the other a version behind. You think you've told the agent "here are the rules," while a different tool is reading the previous generation's rules. Worse, you can't tell by eye which copy is current and which has gone stale.&lt;/p&gt;

&lt;p&gt;The scope of this post is a single repository, for an individual or a small team. Not company-wide policy distribution — just the everyday problem of "instruction files keep multiplying in my repo," dealt with in a realistic number of moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you can't just merge them into one
&lt;/h2&gt;

&lt;p&gt;The naive thought is "just standardize on one AGENTS.md." And in fact, AGENTS.md is becoming the de facto common format. Designed as a "README for agents," it's an open spec that, as of mid-2026, is stewarded by the &lt;a href="https://agents.md/" rel="noopener noreferrer"&gt;Agentic AI Foundation&lt;/a&gt; (under the Linux Foundation), read by more than twenty tools, and adopted in over 60,000 open-source repositories. Codex CLI, Cursor, Windsurf, Aider, Zed — most of the field has converged on AGENTS.md, and even GitHub Copilot's coding agent now reads it. Google's newer entrants (Jules and friends) do too.&lt;/p&gt;

&lt;p&gt;The one that didn't converge is Claude Code. What Claude Code reads is &lt;code&gt;CLAUDE.md&lt;/code&gt;, not &lt;code&gt;AGENTS.md&lt;/code&gt;. It doesn't even read AGENTS.md as a fallback (the claim that "it reads AGENTS.md if CLAUDE.md is absent" is wrong). Start Claude Code in a repo that has only AGENTS.md and you'll get no error — it just runs, cheerfully, with zero project instructions loaded. That's the quietest place for a "unified" team to break. The request to "please support AGENTS.md" has gathered thousands of reactions on &lt;a href="https://github.com/anthropics/claude-code/issues/6235" rel="noopener noreferrer"&gt;anthropics/claude-code issue #6235&lt;/a&gt;, but as of July 2026 the official docs show no sign of it on the roadmap.&lt;/p&gt;

&lt;p&gt;Laid out, the matrix looks like this (as of July 2026).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Instruction file it reads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt; (does not read &lt;code&gt;AGENTS.md&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; (the coding agent also reads &lt;code&gt;AGENTS.md&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt; (&lt;code&gt;.cursor/rules&lt;/code&gt; is a separate, current mechanism; the old &lt;code&gt;.cursorrules&lt;/code&gt; is deprecated)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As long as you use Claude Code, you can't delete &lt;code&gt;CLAUDE.md&lt;/code&gt;. Standardize on AGENTS.md and you're still left maintaining a second file just for Claude Code. That one asymmetry is what keeps "just merge it into one file" from ever quite working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Naive solutions, and where they run out
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Copy-paste into all of them
&lt;/h3&gt;

&lt;p&gt;The most naive move is to copy the same body into each file. It works the first time. It falls apart from the second time onward: every one-line rule change now obliges you to edit N files, and a human will always forget one of them. And, as above, you can't see which copy has rotted. The cost of syncing, and the inability to detect rot — those two are the intrinsic limits of copy-paste.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consolidate into one file and reference it via import
&lt;/h3&gt;

&lt;p&gt;Claude Code has an &lt;code&gt;@path&lt;/code&gt; import syntax. Write &lt;code&gt;@AGENTS.md&lt;/code&gt; as the first line of &lt;code&gt;CLAUDE.md&lt;/code&gt; and, at startup, the contents of AGENTS.md are expanded inline and read as-is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- CLAUDE.md --&amp;gt;&lt;/span&gt;
@AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a clean instinct. The real body lives in a single place — AGENTS.md — and CLAUDE.md is just a one-line stub. Import also lets you &lt;strong&gt;add tool-specific instructions&lt;/strong&gt; on top of the shared body — if there's a rule you only want Claude Code to follow, put it from line two of the stub onward. It has limits, though. The import syntax isn't common across tools (the same trick doesn't work in Copilot's &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;). And since both files can still carry a body, the moment the "keep the stub to one line" discipline slips, the drift is back.&lt;/p&gt;

&lt;p&gt;For what it's worth, running Claude Code's &lt;code&gt;/init&lt;/code&gt; in a repo that has an AGENTS.md will read the existing AGENTS.md and incorporate the relevant parts into the generated CLAUDE.md — but that's a &lt;strong&gt;copy&lt;/strong&gt;, not a reference. If consolidation is the goal, writing the one import line by hand is the cleaner move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundle into one real file with a symlink
&lt;/h3&gt;

&lt;p&gt;The more direct option is to make "there is only one real file" true at the filesystem level. Make &lt;code&gt;CLAUDE.md&lt;/code&gt; a symlink to &lt;code&gt;AGENTS.md&lt;/code&gt;, and when Claude Code opens &lt;code&gt;CLAUDE.md&lt;/code&gt;, the OS transparently returns the contents of AGENTS.md. The tool never has to know about the symlink. The worry that "it won't follow the symlink" is, at least for &lt;strong&gt;reads&lt;/strong&gt;, unfounded: the tool does the opening, but the OS does the following. In fact, Claude Code's own official documentation lists &lt;code&gt;ln -s AGENTS.md CLAUDE.md&lt;/code&gt; as an option.&lt;/p&gt;

&lt;p&gt;For a single repository, this is the sturdiest choice. Here's the actual procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  In practice — bundling to one canonical with symlinks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Topology
&lt;/h3&gt;

&lt;p&gt;Make &lt;code&gt;AGENTS.md&lt;/code&gt; the canonical (the hub) and let the other files be spokes pointing at it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;myrepo/AGENTS.md                          # canonical (the real file — edit only this)
myrepo/CLAUDE.md                          -&amp;gt; AGENTS.md
myrepo/.github/copilot-instructions.md    -&amp;gt; ../AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason to make AGENTS.md the canonical is that it's the format the most tools read out of the box. If Claude Code ever supports AGENTS.md, you just drop one spoke.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# If the body currently lives in CLAUDE.md, move the real file to AGENTS.md first&lt;/span&gt;
git &lt;span class="nb"&gt;mv &lt;/span&gt;CLAUDE.md AGENTS.md

&lt;span class="c"&gt;# Make CLAUDE.md a symlink to AGENTS.md&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; AGENTS.md CLAUDE.md

&lt;span class="c"&gt;# For Copilot (mind the relative path as seen from inside .github/)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .github
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ../AGENTS.md .github/copilot-instructions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Making the paths relative is the trick. Clone the repo anywhere and the links resolve entirely within the repo, so they don't break.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Git handles it
&lt;/h3&gt;

&lt;p&gt;Git records a symlink plainly, as a "blob of mode &lt;code&gt;120000&lt;/code&gt; whose content is the link target path." So symlinks commit as-is and are restored on clone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git ls-files &lt;span class="nt"&gt;-s&lt;/span&gt; AGENTS.md CLAUDE.md .github/copilot-instructions.md
120000 …  .github/copilot-instructions.md  &lt;span class="c"&gt;# symlink&lt;/span&gt;
100644 …  AGENTS.md                        &lt;span class="c"&gt;# real file&lt;/span&gt;
120000 …  CLAUDE.md                        &lt;span class="c"&gt;# symlink&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caveat is Windows. Checking symlinks out correctly requires &lt;code&gt;core.symlinks=true&lt;/code&gt; plus developer mode or the right privileges. Without that, a symlink is expanded as a plain text file whose content is the link target path. A team with Windows in the mix should just confirm this point up front. (As a general note — I run macOS myself.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Where this approach runs out
&lt;/h3&gt;

&lt;p&gt;Symlinks are enough within a single repo, but three problems surface as the scope widens.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Public repos and private contents.&lt;/strong&gt; If the instruction file contains non-public planning information, putting the real file in a public repo's history exposes that content directly. A symlink keeps the &lt;em&gt;contents&lt;/em&gt; out, but the link target path — the directory layout on the private side — still lands in the history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wanting one canonical across several repos.&lt;/strong&gt; The more repos you have, the more hand-linking breaks down as a practice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wiring rots in silence.&lt;/strong&gt; A link goes missing, breaks, or points somewhere else — and you can't track that by eye. You've reintroduced instruction-file rot, this time at the symlink layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In short, symlinks solve "bundle to one real file," but not "declare" and "verify." You're still linking by hand and checking by hand, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further — declare the canonical, verify the wiring
&lt;/h2&gt;

&lt;p&gt;Everything so far stayed inside one repository. From here we step just past the scope I drew in the first section — to when repos multiply and public and private start to mix. The idea is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Declare one canonical, generate the wiring to each tool, and verify that wiring.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That three-beat rhythm needs no dedicated tool. Write the canonical's location once (declare), lay the links with a few lines of script (generate), and drop a few lines in CI that diff the expected output of &lt;code&gt;git ls-files -s&lt;/code&gt; (verify) — and the shape holds. What separates it from linking by hand is that "the wiring as it should be" exists as code rather than prose, and a machine tells you when it breaks.&lt;/p&gt;

&lt;p&gt;What I actually use is this same idea realized in &lt;a href="https://basou.dev" rel="noopener noreferrer"&gt;basou&lt;/a&gt;. basou is a harness for steering AI coding agents, and one of its features treats the wiring of agent-instruction files declaratively. What follows describes v0.32.0.&lt;/p&gt;

&lt;h3&gt;
  
  
  Declare
&lt;/h3&gt;

&lt;p&gt;You declare each repository and its attributes (visibility — public or private, language, and so on) in a manifest. The canonical body lives in exactly one place, on a private "anchor" — the repo that aggregates the canonicals (&lt;code&gt;agents/&amp;lt;repo&amp;gt;/AGENTS.md&lt;/code&gt;). Each repo's instruction file is treated as wiring pointing there.&lt;/p&gt;

&lt;p&gt;The topology is the same hub-and-spoke as before, except the hub points at the anchor's canonical.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;repo&amp;gt;/AGENTS.md                          -&amp;gt; &amp;lt;anchor&amp;gt;/agents/&amp;lt;repo&amp;gt;/AGENTS.md   # hub -&amp;gt; canonical
&amp;lt;repo&amp;gt;/CLAUDE.md                          -&amp;gt; AGENTS.md                          # spoke -&amp;gt; hub
&amp;lt;repo&amp;gt;/.github/copilot-instructions.md    -&amp;gt; ../AGENTS.md                       # spoke -&amp;gt; hub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The canonical is the single file on the anchor side. Each repo's AGENTS.md becomes a &lt;strong&gt;gitignored symlink&lt;/strong&gt; pointing to it. So the canonical (which may contain non-public planning information) is edited exactly once, every tool reads it through the symlink, and it never enters a public repo's history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate
&lt;/h3&gt;

&lt;p&gt;Don't lay the wiring by hand — generate it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;basou project derive    &lt;span class="c"&gt;# generate the full wiring from the declaration (dry-run by default; --apply writes)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;derive&lt;/code&gt; builds, in dependency order, each repo's symlinks, the canonical's boilerplate block, the &lt;code&gt;.gitignore&lt;/code&gt; that keeps a public repo clean, and the workspace view that bundles several repos (there are also &lt;code&gt;symlinks&lt;/code&gt; / &lt;code&gt;preset&lt;/code&gt; / &lt;code&gt;gitignore&lt;/code&gt; / &lt;code&gt;workspace&lt;/code&gt; to run individually). All of it is &lt;strong&gt;non-destructive&lt;/strong&gt;. It creates only what's missing (&lt;code&gt;preset&lt;/code&gt; updates only its own generated block and never touches your hand-written body), it won't overwrite an existing file or repoint a symlink that points elsewhere, and it reports conflicts as conflicts and leaves them to a human. There's also a &lt;code&gt;retrofit&lt;/code&gt; for pulling an existing hand-written AGENTS.md into this topology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify
&lt;/h3&gt;

&lt;p&gt;And then, verify the wiring. This is the piece that was missing from the hand-run symlink approach.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;basou project check     &lt;span class="c"&gt;# surface drift between the declaration and the actual wiring (read-only)&lt;/span&gt;
✅ Every present repo&lt;span class="s1"&gt;'s and the view'&lt;/span&gt;s instruction files &lt;span class="o"&gt;(&lt;/span&gt;AGENTS.md + spokes&lt;span class="o"&gt;)&lt;/span&gt; are wired as declared.

&lt;span class="nv"&gt;$ &lt;/span&gt;basou project wiring    &lt;span class="c"&gt;# inspect the wiring and privacy risk (read-only)&lt;/span&gt;
✅ No instruction file is tracked by git &lt;span class="k"&gt;in &lt;/span&gt;a public-facing repo &lt;span class="o"&gt;(&lt;/span&gt;no privacy risk&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check&lt;/code&gt; reports drift like "the canonical is missing," "a spoke is absent," or "there's a conflict." &lt;code&gt;wiring&lt;/code&gt; goes further and surfaces &lt;strong&gt;privacy risk&lt;/strong&gt; — a state where a public repo tracks an instruction file in git and could expose the contents of a private canonical in public history. What the eye couldn't track comes out as a verdict from a single command.&lt;/p&gt;

&lt;p&gt;The output above isn't a staged sample. It's the real thing (excerpted), run just now against the repository of the blog you're reading. And this blog's symlinks were laid &lt;strong&gt;by hand&lt;/strong&gt;, following the procedure in the "In practice" section — not through basou's wiring feature. Add the manifest declaration after the fact and run &lt;code&gt;check&lt;/code&gt;, and it finds nothing to fix — the hand-laid wiring passes the reconciliation against the declaration as-is. The "In practice" and "Going further" sections aren't an either/or; they converge on the same shape. Start by hand, and nothing is wasted.&lt;/p&gt;

&lt;h3&gt;
  
  
  The exception — a self-contained private repo
&lt;/h3&gt;

&lt;p&gt;You don't always converge on the anchor with hub-and-spoke. The very blog repo I've been using as the example is private, and it &lt;strong&gt;keeps &lt;code&gt;AGENTS.md&lt;/code&gt; as a real file and commits it, symlinks and all&lt;/strong&gt; — the &lt;code&gt;git ls-files&lt;/code&gt; output shown in the "In practice" section is, in fact, from this very repository. Its manifest declaration is, in reality, just these three lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;private&lt;/span&gt;
    &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ja&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Being private, it has no leakage risk, so rather than aggregating the canonical onto an outside anchor, it's more natural to keep the instruction docs' editing and diffs entirely inside the repo — the single-repo shape, where the repo is its own anchor. In basou you can declare a repo that "commits its instruction files by design" as &lt;code&gt;instructions: self&lt;/code&gt;, and a declared repo isn't flagged as a privacy risk even though it tracks instruction files.&lt;/p&gt;

&lt;p&gt;Where &lt;code&gt;self&lt;/code&gt; really earns its keep is on &lt;strong&gt;public&lt;/strong&gt; repos — an OSS project that deliberately commits AGENTS.md for contributors to read. A private repo doesn't need it: it passes on the strength of its visibility alone (that's what the &lt;code&gt;wiring&lt;/code&gt; output above shows). So a public repo hides its canonical and wires to it, while a repo that's fine being self-contained commits the canonical outright — being able to declare even that distinction is the difference from hand-running.&lt;/p&gt;

&lt;h3&gt;
  
  
  An honest positioning
&lt;/h3&gt;

&lt;p&gt;Let me put the exit up front. If you're on a single repository and don't need per-tool differences, &lt;strong&gt;stopping at the symlink from the "In practice" section is fine&lt;/strong&gt;. This blog itself is still in exactly that shape. Conversely, once you want to add agent-specific instructions, that's the domain of the import from the earlier section — full symlink unification (and basou's wiring too) has no answer for it.&lt;/p&gt;

&lt;p&gt;And, to be clear: basou is my own harness, still at a single-author dogfooding stage. What I want to recommend here isn't a product but the &lt;strong&gt;shift in framing&lt;/strong&gt; — from "hand-sync N files" to "declare one canonical, and have the wiring generated and verified." Laying symlinks by hand already gets you half of it today, and verification can start from a few lines in CI. basou is just one example of putting that three-beat rhythm onto a single manifest. What basou itself is, I'll leave to &lt;a href="https://blog.tak3.jp/en/blog/introducing-basou/" rel="noopener noreferrer"&gt;the previous post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  In summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Each AI coding tool uses a different instruction filename (&lt;code&gt;CLAUDE.md&lt;/code&gt; / &lt;code&gt;AGENTS.md&lt;/code&gt; / &lt;code&gt;copilot-instructions.md&lt;/code&gt;). AGENTS.md is converging toward a de facto standard, but because &lt;strong&gt;Claude Code reads only the &lt;code&gt;CLAUDE.md&lt;/code&gt; family, never &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt;, you can't collapse everything into one file.&lt;/li&gt;
&lt;li&gt;The first hands-on move is the &lt;strong&gt;symlink&lt;/strong&gt;. Make &lt;code&gt;AGENTS.md&lt;/code&gt; the canonical and turn &lt;code&gt;CLAUDE.md&lt;/code&gt; and friends into symlinks to it: the real file becomes one, and tools read it transparently. Git carries it plainly as a &lt;code&gt;120000&lt;/code&gt; blob (only Windows needs care).&lt;/li&gt;
&lt;li&gt;Symlinks solve "bundle to one real file," but not the mix of public and private, multiple repos, or wiring rot. From there, move onto the three-beat rhythm of &lt;strong&gt;declare (one canonical), generate (the wiring), and verify (drift and privacy risk)&lt;/strong&gt; — and you can start from a few lines of CI, no dedicated tool required.&lt;/li&gt;
&lt;li&gt;Take the management of instruction files themselves from hand-syncing to human declaration plus tool generation and verification. The wiring of instruction files is one more rein a human should hold. The scarce resource, again, is control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://blog.tak3.jp/ja/blog/declarative-agent-instructions/" rel="noopener noreferrer"&gt;Japanese version of this post&lt;/a&gt; — its "paired" article — is already live.&lt;/p&gt;

</description>
      <category>agentsmd</category>
      <category>claudemd</category>
      <category>claudecode</category>
      <category>basou</category>
    </item>
    <item>
      <title>When AI Agents Write the Code, What's Missing Are the Reins — Introducing basou</title>
      <dc:creator>Takashi Matsuyama</dc:creator>
      <pubDate>Thu, 09 Jul 2026 02:00:39 +0000</pubDate>
      <link>https://dev.to/takashimatsuyama/when-ai-agents-write-the-code-whats-missing-are-the-reins-introducing-basou-1m62</link>
      <guid>https://dev.to/takashimatsuyama/when-ai-agents-write-the-code-whats-missing-are-the-reins-introducing-basou-1m62</guid>
      <description>&lt;p&gt;I closed the previous post with a promise: that the development style behind this blog, and the OSS I've been shipping — a harness for steering AI coding agents — deserved their own write-up. This is that write-up.&lt;/p&gt;

&lt;p&gt;The project is &lt;a href="https://basou.dev" rel="noopener noreferrer"&gt;basou&lt;/a&gt;. In one line: it's &lt;strong&gt;a harness — a full set of tack — for steering AI coding agents&lt;/strong&gt;. A "saddle" you declare once per repository to say how the project should be driven; "reins" that carry intent from one session to the next; and, underneath both, a replayable record of what the agent actually did. All of it as plain files, in a &lt;code&gt;.basou/&lt;/code&gt; directory right next to your code. Local-first: nothing leaves your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code only captures the &lt;em&gt;what&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Once agents write most of the code, the scarce resource isn't typing — it's &lt;strong&gt;control&lt;/strong&gt;. The more I leaned on agents, the more the pain narrowed to three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One: the "why" evaporates between sessions.&lt;/strong&gt; The answer to "what does this code do" lives in the code; even a read-only assistant can reconstruct it. What can't be reconstructed is the living context around a decision — what you intended, which path you rejected, how far you got. That has to be captured in the moment, and what you capture is worthless unless it survives to the next session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two: the cost of re-explaining.&lt;/strong&gt; Because it wasn't captured, every session starts with a human re-narrating the context by mouth. Run three projects in parallel and you pay that tax three times over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three: you can't audit it.&lt;/strong&gt; What an agent did is buried inside vendor-specific log formats, or nowhere at all. "That change last week — why did we do it that way?" has no answer you can verify after the fact.&lt;/p&gt;

&lt;p&gt;basou is my answer to those three. Not a dashboard you glance at, not an audit log you file away — tack you keep in your hands the whole time you're working. The name comes from &lt;em&gt;basou&lt;/em&gt; (馬装), the Japanese word for tacking up a horse: fitting the saddle and the reins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two foundations — the saddle and the reins
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🐎 The saddle: a declarative workspace
&lt;/h3&gt;

&lt;p&gt;You &lt;strong&gt;declare each repository's nature once&lt;/strong&gt; — public or private, what language it's in, where the canonical copy of the agent-instruction file (&lt;code&gt;AGENTS.md&lt;/code&gt;) lives — in a manifest. Then &lt;code&gt;basou project&lt;/code&gt; derives the rest: which roots to record, the wiring of &lt;code&gt;AGENTS.md&lt;/code&gt; / &lt;code&gt;CLAUDE.md&lt;/code&gt; / Copilot instruction files, the &lt;code&gt;.gitignore&lt;/code&gt; that keeps a private canonical file out of a public repo's history, and a workspace view that bundles multiple repositories. Humans edit the declaration; basou maintains the plumbing.&lt;/p&gt;

&lt;p&gt;Every generating command is &lt;strong&gt;dry-run by default&lt;/strong&gt; (&lt;code&gt;--apply&lt;/code&gt; is what actually writes), and additive and non-destructive. How to consolidate the ever-multiplying pile of instruction files is a whole post on its own, so I'll save it for another time.&lt;/p&gt;

&lt;h3&gt;
  
  
  🪢 The reins: orientation that carries intent
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;basou orient&lt;/code&gt; answers "where is the work right now, and what's next" from the &lt;strong&gt;record&lt;/strong&gt;, not from memory. Recent decisions, open tracks, the recorded next step, and a note on whether the record itself has gone stale.&lt;/p&gt;

&lt;p&gt;The decision record has a trick to it. A decision made mid-conversation with an agent — the reasoning, the alternatives, and why you rejected them — normally dies with the chat log. &lt;code&gt;basou decision capture&lt;/code&gt; makes the &lt;strong&gt;agent itself&lt;/strong&gt;, while it still holds the conversation in context, emit the decision as structured data, and basou writes it down deterministically. basou never runs an LLM of its own. So the record is yours, verifiable, and works offline.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;basou note&lt;/code&gt; leaves a one-line "pick up here next." &lt;code&gt;basou handoff generate&lt;/code&gt; regenerates a handoff document for the next session — or the next teammate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Underneath: a replayable provenance trail
&lt;/h2&gt;

&lt;p&gt;The two foundations sit on a simple, inspectable base.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The source of truth is JSONL.&lt;/strong&gt; Observed events — commands run, files changed, decisions, approvals — are appended to &lt;code&gt;.basou/sessions/&amp;lt;session_id&amp;gt;/events.jsonl&lt;/code&gt; and &lt;strong&gt;linked in a hash chain&lt;/strong&gt;. Tampering is detectable with &lt;code&gt;basou verify&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Humans read Markdown.&lt;/strong&gt; &lt;code&gt;.basou/handoff.md&lt;/code&gt; and &lt;code&gt;.basou/decisions.md&lt;/code&gt; are regenerated from the event log (you can hand-edit them too). Review them in an editor, or right in a PR diff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion doesn't depend on any vendor's internal format.&lt;/strong&gt; &lt;code&gt;basou run claude-code&lt;/code&gt; wraps a process from the outside and records it; &lt;code&gt;basou import claude-code&lt;/code&gt; / &lt;code&gt;basou import codex&lt;/code&gt; derive sessions after the fact from the logs each tool leaves behind. &lt;code&gt;basou refresh&lt;/code&gt; does ingestion and Markdown regeneration in one pass, and &lt;code&gt;basou view&lt;/code&gt; shows the whole thing in a local web UI bound to 127.0.0.1 only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep exactly one source of truth and regenerate the human-facing copies from it — that stance is the "one source, many uses" discipline from twenty years of designing business systems, carried straight into the agent era.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real thing — this blog's own records
&lt;/h2&gt;

&lt;p&gt;I use basou every day, across all of my projects. The repository behind the blog you're reading is one of them. What follows isn't a staged sample — it's a real excerpt from that repository's own &lt;code&gt;.basou/&lt;/code&gt; (some paths and identifiers are masked; the Japanese working notes are translated here for readability).&lt;/p&gt;

&lt;p&gt;First, the current position &lt;code&gt;basou orient&lt;/code&gt; returned when I resumed work. Behind it, &lt;code&gt;handoff.md&lt;/code&gt; holds the same content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Handoff&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Generated at 2026-07-08T13:20:50Z from ses_01KVZAFJT0..ses_01KX0Y6TPT&lt;/span&gt;

&lt;span class="gu"&gt;## Current state&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Last session: claude-code 2026-07-07: 110 commands, 11 files (imported) [ses_01KX0Y6TPR]
&lt;span class="p"&gt;-&lt;/span&gt; Last task: (no tasks recorded yet)

&lt;span class="gu"&gt;## Recently changed files&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; src/components/MermaidRenderer.astro
&lt;span class="p"&gt;-&lt;/span&gt; src/layouts/BaseLayout.astro
&lt;span class="p"&gt;-&lt;/span&gt; src/pages/ja/index.astro
&lt;span class="p"&gt;-&lt;/span&gt; …

&lt;span class="gu"&gt;## Recent decisions&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Use the existing static image for the shared header avatar [decision_01KWYHFVDE]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, one decision made during that work, recorded like this — with the reasoning and the road not taken.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Render Mermaid diagrams as SVG on the client&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; date: 2026-07-07
&lt;span class="p"&gt;-&lt;/span&gt; rationale: Build-time rendering (rehype-mermaid and friends) depends on headless
  Chromium, which risks breaking Cloudflare's build pipeline. Instead, load mermaid.js
  in the browser and swap the Shiki-highlighted pre[data-language=mermaid] for SVG.
&lt;span class="p"&gt;-&lt;/span&gt; alternatives: Pre-generate the SVG at build time (SSG) with a headless browser
&lt;span class="p"&gt;-&lt;/span&gt; rejected_reason: Adding a headless-Chromium dependency to the build makes builds
  fragile; that operational risk outweighs the "no JS required" benefit.
&lt;span class="p"&gt;-&lt;/span&gt; linked_files: src/components/MermaidRenderer.astro, src/styles/global.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And beneath all of it, the event log and its verification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"schema_version":"0.1.0","session_id":"ses_01KW0J3F…","type":"session_started","prev_hash":"1e4d0428…e34dc37"}
{"schema_version":"0.1.0","session_id":"ses_01KW0J3F…","type":"session_status_changed","from":"initialized","to":"running","prev_hash":"e3108afa…efb2bc9"}
{"schema_version":"0.1.0","session_id":"ses_01KW0J3F…","type":"session_ended","exit_code":0,"prev_hash":"9d11ac73…b4b29f0e"}

$ basou verify
ses_01KW0J3F…  verified (5 events)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Markdown is easy to read, but it's only a copy. The source of truth is the hash-chained JSONL — which is why a machine can answer "has this record been tampered with?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Design promises
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local-first / zero-network.&lt;/strong&gt; It reads the agent logs already on your machine and writes only to the &lt;code&gt;.basou/&lt;/code&gt; directory next to your code. Nothing leaves the machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic.&lt;/strong&gt; No runtime LLM. It just writes down the structure the agent handed it, so the same input produces the same record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversible.&lt;/strong&gt; Everything stays inside &lt;code&gt;.basou/&lt;/code&gt;, and generating commands are dry-run by default and non-destructive. Adopting it — or backing out — is low-risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CLI surface is frozen even at 0.x.&lt;/strong&gt; The on-disk format is versioned and ships with a JSON Schema. The format and semver guarantees land at 1.0, which is what I'm building toward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache-2.0.&lt;/strong&gt; A single author dogfooding it daily, aiming for a 1.0 that outside maintainers and teams can rely on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start in five minutes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Node.js &amp;gt;= 20.10&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @basou/cli

&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your/project
basou init

&lt;span class="c"&gt;# Try a minimal record by hand&lt;/span&gt;
basou task new &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"Refactor login form"&lt;/span&gt;
basou note &lt;span class="s2"&gt;"Starting from a look at auth.ts"&lt;/span&gt;
basou handoff generate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; .basou/handoff.md

&lt;span class="c"&gt;# Or ingest in bulk from the agent logs already on disk&lt;/span&gt;
basou refresh
basou view   &lt;span class="c"&gt;# local UI, 127.0.0.1 only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A walkthrough with sample output and the usual gotchas is at &lt;a href="https://basou.dev/quickstart/" rel="noopener noreferrer"&gt;basou.dev/quickstart/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One more thing: if basou is the side that records what an agent &lt;em&gt;did&lt;/em&gt;, there's a companion project, &lt;a href="https://kozou.org" rel="noopener noreferrer"&gt;Kozou&lt;/a&gt;, for the side that tells an agent what a database &lt;em&gt;means&lt;/em&gt;. I'll introduce that one separately too.&lt;/p&gt;

&lt;h2&gt;
  
  
  In summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In the age of coding agents, the scarce resource is control — staying in the saddle, driving every project the same way, and not losing the "why" between sessions.&lt;/li&gt;
&lt;li&gt;basou is the harness for that: a saddle (a declarative workspace) and reins (orientation that carries intent), with a hash-chained, replayable trail underneath.&lt;/li&gt;
&lt;li&gt;All of it as plain files next to your code. Local-first, zero-network, no runtime LLM. Adoption is low-risk, and you can always back out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository is &lt;a href="https://github.com/basou-dev/basou" rel="noopener noreferrer"&gt;github.com/basou-dev/basou&lt;/a&gt;; the docs are at &lt;a href="https://basou.dev" rel="noopener noreferrer"&gt;basou.dev&lt;/a&gt;. If something trips you up on 0.x, I'd be grateful for an issue.&lt;/p&gt;

&lt;p&gt;The Japanese version of this post — its "paired" article — is already live.&lt;/p&gt;

</description>
      <category>basou</category>
      <category>aiagents</category>
      <category>harnessengineering</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Building a Bilingual Blog on Cloudflare Workers Static Assets — and Why I Skipped Pages</title>
      <dc:creator>Takashi Matsuyama</dc:creator>
      <pubDate>Fri, 03 Jul 2026 08:00:19 +0000</pubDate>
      <link>https://dev.to/takashimatsuyama/building-a-bilingual-blog-on-cloudflare-workers-static-assets-and-why-i-skipped-pages-419o</link>
      <guid>https://dev.to/takashimatsuyama/building-a-bilingual-blog-on-cloudflare-workers-static-assets-and-why-i-skipped-pages-419o</guid>
      <description>&lt;p&gt;🔗 This article is republished from &lt;a href="https://blog.tak3.jp/en/blog/building-this-blog/" rel="noopener noreferrer"&gt;https://blog.tak3.jp/en/blog/building-this-blog/&lt;/a&gt; — the original source.&lt;/p&gt;

&lt;p&gt;This blog (blog.tak3.jp) is a static Astro site served from Cloudflare Workers static assets, edited through Sveltia CMS, and published in two languages, Japanese and English. When a post goes live, it is automatically syndicated to DEV.to and, by design, to Qiita (a major Japanese developer platform) — either as a summary with a pointer back or in full, chosen per post. If you're reading this on DEV.to, this article reached you through the exact pipeline it describes. (Qiita is the honest exception right now: its API is blocking this account, so that side runs manually for the moment — more on that below.)&lt;/p&gt;

&lt;p&gt;One more meta detail before we start: a Japanese edition of this post exists, and it is &lt;em&gt;not&lt;/em&gt; a translation of this one. It's a pair — same subject, independently written article. That distinction is a deliberate design decision, and it's half of what this post is about. The other half is why I didn't use Cloudflare Pages.&lt;/p&gt;

&lt;p&gt;This is a design write-up, not a tutorial. I'll focus on the decisions: skipping Pages, pairing languages with &lt;code&gt;translationKey&lt;/code&gt;, draft safety in a setup where publishing has side effects, and running a Git-based CMS with no auth infrastructure at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements and stack
&lt;/h2&gt;

&lt;p&gt;What I wanted wasn't "a blog" but a publishing base with my own site as the single source of origin. Five requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Publish in both Japanese and English&lt;/li&gt;
&lt;li&gt;Keep all content in Git (history, no lock-in, editable from both an editor and a browser)&lt;/li&gt;
&lt;li&gt;Push operating costs as close to zero as possible (serverless, free tiers)&lt;/li&gt;
&lt;li&gt;Auto-syndicate published posts to external platforms (DEV.to / Qiita)&lt;/li&gt;
&lt;li&gt;Be implementable by AI coding agents, with a spec document as the single source of truth&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The resulting stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSG&lt;/td&gt;
&lt;td&gt;Astro + TypeScript (Content Layer API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content&lt;/td&gt;
&lt;td&gt;Markdown / MDX + YAML frontmatter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMS&lt;/td&gt;
&lt;td&gt;Sveltia CMS (Git-based, admin at &lt;code&gt;/admin/&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Cloudflare Workers static assets + a thin main Worker for language routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syndication&lt;/td&gt;
&lt;td&gt;A separate Worker (Cron + D1 + KV)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSS / sitemap&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@astrojs/rss&lt;/code&gt; / &lt;code&gt;@astrojs/sitemap&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The whole system in one picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph edit[Writing &amp;amp; editing]
    A[Editor / AI agents] --&amp;gt; G
    C[Sveltia CMS /admin/] --&amp;gt; G
  end
  G[(GitHub repo)] --&amp;gt;|push to main| B[Build: astro build → dist/]
  B --&amp;gt;|wrangler deploy| W[Cloudflare Workers&amp;lt;br&amp;gt;static assets + main Worker]
  W --&amp;gt; V[Readers&amp;lt;br&amp;gt;/ja/ and /en/]
  W --&amp;gt;|/feed.json| S[Syndication Worker&amp;lt;br&amp;gt;Cron + D1 + KV]
  S --&amp;gt; Q[Qiita]
  S --&amp;gt; D[DEV.to]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the CMS I chose Sveltia over Decap (formerly Netlify CMS): it keeps Decap-compatible config while being a newer, lighter, actively developed implementation. I treat a Git-based CMS not as a WordPress replacement but as a UI for editing structured content that lives in Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Cloudflare Pages deprecated?
&lt;/h2&gt;

&lt;p&gt;Facts first, because this question generates a lot of noise on Reddit and Hacker News. &lt;strong&gt;As of July 2026, there is no official end-of-life announcement for Pages.&lt;/strong&gt; Existing projects are supported, and you can still create new ones.&lt;/p&gt;

&lt;p&gt;The practical reality, though, is "not recommended for new projects." Cloudflare's docs ship an official Pages → Workers migration guide and state plainly that development focus is on Workers going forward, recommending Workers for anything new. Feature-wise, Workers now covers nearly everything Pages does, and adds things Pages never had — Durable Objects, Cron Triggers, richer observability. The remaining gaps in the official compatibility matrix are small (branch-build configurability, for one).&lt;/p&gt;

&lt;p&gt;One clarification, because it fuels the confusion: the thing that &lt;em&gt;is&lt;/em&gt; officially deprecated is Workers &lt;strong&gt;Sites&lt;/strong&gt; — an older, entirely different feature that predates Pages — with a documented migration path to Workers static assets. A good share of the "Pages is deprecated" claims are actually about that.&lt;/p&gt;

&lt;p&gt;So no, I didn't skip Pages "because it's deprecated." I skipped it for one defensive reason and two positive ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The defensive reason:&lt;/strong&gt; adopting Pages for a brand-new project means signing up for a future migration on day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Positive reason #1: routing as code.&lt;/strong&gt; This site has &lt;code&gt;/ja/&lt;/code&gt; and &lt;code&gt;/en/&lt;/code&gt; side by side, and no real page at the root &lt;code&gt;/&lt;/code&gt;. Static hosting can't read a visitor's language — but a thin main Worker sitting in front of the static assets can handle exactly one path, &lt;code&gt;/&lt;/code&gt;, and answer with a 302.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wrangler.jsonc (excerpt)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"takashimatsuyama-blog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"worker/index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"directory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASSETS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"run_worker_first"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// only the root runs the Worker; everything else is served as-is&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Language negotiation is a three-step cascade: ① an explicit &lt;code&gt;?hl=ja|en&lt;/code&gt; query from the language switcher (recorded in a cookie), ② the cookie from a previous explicit choice (which outranks browser settings), ③ on first visit, &lt;code&gt;Accept-Language&lt;/code&gt; — top preference &lt;code&gt;en*&lt;/code&gt; goes to &lt;code&gt;/en/&lt;/code&gt;, everything else defaults to &lt;code&gt;/ja/&lt;/code&gt;, because this is a Japanese-primary site. The 302 carries &lt;code&gt;Vary: Accept-Language&lt;/code&gt; and &lt;code&gt;Cache-Control: private, no-store&lt;/code&gt; so the language decision never ends up in a shared cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker/index.ts (excerpt, simplified)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isRootDocument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// language pages, assets, 404s: straight to static assets&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isRootDocument&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 1. explicit switch → 2. cookie → 3. Accept-Language (default: ja)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hl&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ja&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;hl&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirectToLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LANG_COOKIE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ja&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirectToLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirectToLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;preferredLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept-Language&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Positive reason #2: one deploy story.&lt;/strong&gt; This repo also contains the syndication Worker (Cron / D1 / KV). Instead of maintaining a Pages project and a Workers project side by side, everything is &lt;code&gt;wrangler deploy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trade-off, stated honestly: the build-and-preview experience Pages gave you for free becomes your problem. Here, a push to &lt;code&gt;main&lt;/code&gt; triggers &lt;code&gt;wrangler deploy&lt;/code&gt;, and previews are the URLs issued by &lt;code&gt;wrangler versions upload&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bilingual design: pairs, not translations
&lt;/h2&gt;

&lt;p&gt;URLs use path prefixes, &lt;code&gt;/ja/...&lt;/code&gt; and &lt;code&gt;/en/...&lt;/code&gt;, and the hreflang &lt;code&gt;x-default&lt;/code&gt; points to &lt;code&gt;/ja/&lt;/code&gt;. Most i18n write-ups assume an English-primary site; this one is Japanese-primary, and I wanted zero ambiguity about what a visitor with an unknown language preference sees.&lt;/p&gt;

&lt;p&gt;The decision that actually needed thought was how to hold the content collections:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;A: one collection per language&lt;/th&gt;
&lt;th&gt;B: single collection + &lt;code&gt;lang&lt;/code&gt; filter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;call &lt;code&gt;getCollection('blog_ja')&lt;/code&gt; etc. per language&lt;/td&gt;
&lt;td&gt;one uniform query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMS mapping&lt;/td&gt;
&lt;td&gt;1:1 with CMS collections&lt;/td&gt;
&lt;td&gt;language is pickable (and mis-pickable) in the UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;a forgotten &lt;code&gt;lang&lt;/code&gt; filter fails silently&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I went with A (&lt;code&gt;blog_ja&lt;/code&gt; / &lt;code&gt;blog_en&lt;/code&gt; / &lt;code&gt;notes_ja&lt;/code&gt; / &lt;code&gt;notes_en&lt;/code&gt;). The deciding factor: neither a human editor nor an AI agent should ever have to think about which language they're in. Each Sveltia collection maps 1:1 to an Astro collection, &lt;code&gt;lang&lt;/code&gt; is a hidden fixed field in the CMS, and the entire class of "silently leaking the wrong language" bugs disappears.&lt;/p&gt;

&lt;p&gt;The two languages are linked through a &lt;code&gt;translationKey&lt;/code&gt; in the frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// content.config.ts (excerpt, simplified)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ja&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;translationKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;([]),&lt;/span&gt;
  &lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;syndication&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;({}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blog_en&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineCollection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/content/blog_en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blogSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part: &lt;strong&gt;the slug is independent of the translationKey.&lt;/strong&gt; The two editions of a post may have different slugs, different titles, different structure. What you build is not a translation but a pair — two articles on the same subject, each written for its audience. This very post demonstrates it: the Japanese edition opens differently, orders its arguments differently, and spends more words on the platforms Japanese readers syndicate to.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;translationKey&lt;/code&gt; is typed by hand, a typo fails silently — the language-switch link just doesn't appear. That's fixed with a two-tier build check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A translationKey that exists in only one language → &lt;strong&gt;warning&lt;/strong&gt; ("not translated yet" is a normal state, so it must be allowed)&lt;/li&gt;
&lt;li&gt;A duplicate translationKey within one language → &lt;strong&gt;error&lt;/strong&gt; (that's always a typo)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You cannot mechanically distinguish "no pair yet" from "typo'd key." So route the possibly-legitimate case to a warning and the impossible case to an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Draft safety across every output path
&lt;/h2&gt;

&lt;p&gt;Posts with &lt;code&gt;draft: true&lt;/code&gt; are excluded from all of this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output path&lt;/th&gt;
&lt;th&gt;Drafts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Home / lists / detail pages / tags&lt;/td&gt;
&lt;td&gt;excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSS / JSON Feed&lt;/td&gt;
&lt;td&gt;excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sitemap&lt;/td&gt;
&lt;td&gt;excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language-switch links, related posts&lt;/td&gt;
&lt;td&gt;excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External syndication (Worker)&lt;/td&gt;
&lt;td&gt;excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On a plain static blog, a mis-published draft is fixed with a redeploy. Here, publishing has side effects: it triggers syndication to DEV.to and Qiita. Deleting the post from your own site doesn't recall what already left. So the CMS defaults new posts to &lt;code&gt;draft: true&lt;/code&gt;, and the exclusion is enforced not just in HTML but in feeds, the sitemap, and the syndication Worker. Automated syndication and draft discipline only work as a set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sveltia CMS with a token — no OAuth proxy
&lt;/h2&gt;

&lt;p&gt;The standard way to run a Git-based CMS against GitHub is to deploy an OAuth proxy for authentication. This site doesn't have one. There's a single editor (me) and a single target repo, and Sveltia offers "Sign in with Token": paste a GitHub fine-grained PAT and you're in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# public/admin/config.yml (excerpt)&lt;/span&gt;
&lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github&lt;/span&gt;
  &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-account/your-blog-repo&lt;/span&gt; &lt;span class="c1"&gt;# point at your repo&lt;/span&gt;
  &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;span class="na"&gt;publish_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="na"&gt;media_folder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public/images/uploads&lt;/span&gt;
&lt;span class="na"&gt;public_folder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/images/uploads&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token is scoped down hard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fine-grained PAT restricted to this one repository&lt;/li&gt;
&lt;li&gt;Contents: Read and write (plus the mandatory Metadata: Read) — nothing else&lt;/li&gt;
&lt;li&gt;Expiring. With &lt;code&gt;publish_mode: simple&lt;/code&gt; (direct commits to main), no PR permissions are needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest risk assessment: the token lives in the browser's localStorage and goes straight to the GitHub API. You keep zero secrets on your own infrastructure, in exchange for operational rules — don't use shared machines, rotate on expiry. For a single-operator personal blog, removing an entire moving part (the OAuth proxy) is worth that trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The syndication pipeline, briefly
&lt;/h2&gt;

&lt;p&gt;The reveal for the opening line. The site emits &lt;code&gt;/feed.json&lt;/code&gt; (JSON Feed), where each item carries a &lt;code&gt;_meta&lt;/code&gt; block for syndication: the GitHub sourcePath / gitRef / sha, the canonical URL, and the post's &lt;code&gt;syndication&lt;/code&gt; settings. A separate Worker polls it on a Cron schedule, detects new posts, and publishes to whichever platforms are set to &lt;code&gt;enabled&lt;/code&gt; — with an allowlist: only DEV.to and Qiita are ever posted to automatically; everything else stays manual. Each post chooses whether its syndicated copy is a summary with a pointer back or the full text (via the &lt;code&gt;syndicationBodyMode&lt;/code&gt; frontmatter field); either way, the canonical URL stays on this blog. This post is syndicated in full.&lt;/p&gt;

&lt;p&gt;One honest caveat: at publish time, Qiita's API returns 403 Forbidden for this account — not a scope or rate-limit problem, but an account-level block on creating items through the API (the same account posted fine a few days earlier). So Qiita is temporarily manual while DEV.to keeps running automatically. A post about automating syndication, tripped up by a real syndication limit — which felt worth stating plainly.&lt;/p&gt;

&lt;p&gt;Duplicate protection is single-flight via a unique constraint in D1, and every adapter ran in dry-run mode (no real HTTP, no storage writes) before going live. Retries, rate limits, and partial-success handling deserve their own post, so that's where they'll go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;compatibility_date&lt;/code&gt; cannot be in the future.&lt;/strong&gt; The moment the main Worker was added, deploys started failing runtime validation — because the wrangler config carried a compatibility date Cloudflare hadn't released yet. An assets-only config had accepted it silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A single collection for static pages collides.&lt;/strong&gt; The glob loader uses the frontmatter &lt;code&gt;slug&lt;/code&gt; as the entry id, so putting &lt;code&gt;/ja/about&lt;/code&gt; and &lt;code&gt;/en/about&lt;/code&gt; in one &lt;code&gt;pages&lt;/code&gt; collection collides on the same slug. Pages got split per language too (&lt;code&gt;pages_ja&lt;/code&gt; / &lt;code&gt;pages_en&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pin the CMS CDN version.&lt;/strong&gt; Loading Sveltia from a &lt;code&gt;latest&lt;/code&gt; CDN URL means the admin UI can change behavior whenever upstream ships. The script tag in &lt;code&gt;/admin/index.html&lt;/code&gt; is version-pinned; upgrades happen on my schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The value of Workers static assets is "static hosting where routing is code." You don't need "Pages is deprecated" as a reason — a thin Worker in front and a single deploy story are positive ones&lt;/li&gt;
&lt;li&gt;Design bilingual content as pairs, not translations. Separating &lt;code&gt;slug&lt;/code&gt; from &lt;code&gt;translationKey&lt;/code&gt;, plus a warning/error split in validation, is the whole implementation&lt;/li&gt;
&lt;li&gt;If publishing triggers syndication, draft discipline has to cover every output path, not just HTML. The automation and the discipline only exist together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This repo was implemented primarily by AI coding agents, working from a spec document as the single source of truth. That workflow — and the open-source harness for steering coding agents that I'm building — is a story for another post.&lt;/p&gt;

&lt;p&gt;The Japanese pair of this article is already live on the &lt;code&gt;/ja/&lt;/code&gt; side of this blog.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>sveltiacms</category>
      <category>cloudflareworkers</category>
      <category>i18n</category>
    </item>
    <item>
      <title>About this blog</title>
      <dc:creator>Takashi Matsuyama</dc:creator>
      <pubDate>Wed, 24 Jun 2026 23:00:34 +0000</pubDate>
      <link>https://dev.to/takashimatsuyama/about-this-blog-28ij</link>
      <guid>https://dev.to/takashimatsuyama/about-this-blog-28ij</guid>
      <description>&lt;p&gt;Starting a personal media that records and shares all my work.&lt;/p&gt;

&lt;p&gt;Read the full article 👉 &lt;a href="https://blog.tak3.jp/en/blog/hello-blog/" rel="noopener noreferrer"&gt;https://blog.tak3.jp/en/blog/hello-blog/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>cloudflare</category>
    </item>
  </channel>
</rss>
