<?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 Abe</title>
    <description>The latest articles on DEV Community by Takashi Abe (@sho7650).</description>
    <link>https://dev.to/sho7650</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%2F3713571%2F465bf58b-8167-4210-a239-7c75f861a94c.png</url>
      <title>DEV Community: Takashi Abe</title>
      <link>https://dev.to/sho7650</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sho7650"/>
    <language>en</language>
    <item>
      <title>PunyGuard: A Chrome Extension That Blocks Every Internationalized Domain</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Fri, 16 Oct 2026 09:02:56 +0000</pubDate>
      <link>https://dev.to/sho7650/punyguard-a-chrome-extension-that-blocks-every-internationalized-domain-4alk</link>
      <guid>https://dev.to/sho7650/punyguard-a-chrome-extension-that-blocks-every-internationalized-domain-4alk</guid>
      <description>&lt;p&gt;&lt;code&gt;аррӏө.com&lt;/code&gt; and &lt;code&gt;apple.com&lt;/code&gt;. Put them side by side: can you tell which one is real?&lt;/p&gt;

&lt;p&gt;All five letters in the first one are Cyrillic. Even so, Chrome 154's address bar displayed it exactly as-is. It looks identical to &lt;code&gt;apple.com&lt;/code&gt; (&lt;a href="https://www.gizmodo.jp/article/2610-chrome-phishing-domain-bypass/" rel="noopener noreferrer"&gt;GIZMODO article&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;After reading that article, I built a small Chrome extension called &lt;a href="https://github.com/sho7650/punyguard" rel="noopener noreferrer"&gt;PunyGuard&lt;/a&gt;. It blocks access to internationalized domain names (IDNs), all of them, no exceptions. It doesn't judge whether a domain looks similar to anything. Legitimate Japanese domains get blocked too. It may look like a heavy-handed tradeoff, but that tradeoff is the core of the design.&lt;/p&gt;

&lt;p&gt;Out of nowhere, a confession: I don't like Japanese domain names. Rather than getting sent to some Japanese-named site where I can't even tell who's behind it, I'd honestly prefer not to be able to reach it at all. That conviction is what the whole design rests on.&lt;/p&gt;

&lt;p&gt;So instead of making the machine do fuzzy judgment calls, I gave it a rule it can't get wrong. The machine handles the routine work; I decide what to protect and what to give up. It's a small extension, but the way I think about working with computers and AI shows up directly in both its design decisions and how I built it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sho7650/punyguard" rel="noopener noreferrer"&gt;https://github.com/sho7650/punyguard&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ If you use &lt;code&gt;unbound&lt;/code&gt; (I do too), riorist's post &lt;a href="https://rio.st/2026/10/16/unbound%e3%81%a7%e3%83%9b%e3%83%a2%e3%82%b0%e3%83%a9%e3%83%95%e6%94%bb%e6%92%83%e3%82%92%e9%98%b2%e6%ad%a2%e3%81%99%e3%82%8b/?rio_share=20261016012951" rel="noopener noreferrer"&gt;Preventing homograph attacks with Unbound (in Japanese)&lt;/a&gt; is worth a look too. Since it works regardless of which browser you use, it's arguably the more secure option.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Miss one character, and the look-alike detection falls apart
&lt;/h2&gt;

&lt;p&gt;Chrome has a mechanism that displays domains containing confusable characters in Punycode. Punycode is a scheme for rewriting domain names that contain non-ASCII characters (ASCII being half-width letters, digits, and symbols), like Japanese or Cyrillic, into an ASCII string starting with &lt;code&gt;xn--&lt;/code&gt;. &lt;code&gt;аррӏө.com&lt;/code&gt; becomes &lt;code&gt;xn--80a6aa68c8d.com&lt;/code&gt;. If a suspicious domain is shown as that jumble of symbols, at least it won't look like &lt;code&gt;apple.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So what counts as "confusable"? The basis for that call is a list of look-alike characters. This time, &lt;code&gt;ө&lt;/code&gt; (U+04E9) wasn't on the list. Because of that single missing character, five Cyrillic letters sat in the address bar looking exactly like the real thing.&lt;/p&gt;

&lt;p&gt;I don't think this is really anyone's mistake so much as a limit of the approach itself. The world has an enormous number of characters, and how they look changes from font to font. Exhaustively listing every "similar-looking character" is a job that never ends. However carefully you grow the list, the next &lt;code&gt;ө&lt;/code&gt; will be hiding somewhere.&lt;/p&gt;

&lt;p&gt;Human eyes are the same. The assumption that you can tell them apart if you look carefully collapses instantly in the face of a one-character difference. So I decided to stop judging by appearance altogether.&lt;/p&gt;

&lt;h2&gt;
  
  
  A design that stops judging
&lt;/h2&gt;

&lt;p&gt;All &lt;a href="https://github.com/sho7650/punyguard" rel="noopener noreferrer"&gt;PunyGuard&lt;/a&gt; looks at is whether the hostname has a label starting with &lt;code&gt;xn--&lt;/code&gt;. That's it. A label is each piece of &lt;code&gt;www.example.com&lt;/code&gt; split by &lt;code&gt;.&lt;/code&gt;. If even one label starts with &lt;code&gt;xn--&lt;/code&gt;, the host is treated as an IDN host and blocked.&lt;/p&gt;

&lt;p&gt;However natural an internationalized domain looks on screen in the browser, on the wire it always carries the &lt;code&gt;xn--&lt;/code&gt; prefix. A gatekeeper who judges by faces is vulnerable to look-alikes. A gatekeeper who checks name tags doesn't hesitate, no matter how similar the faces are. I built PunyGuard to be the second kind of gatekeeper.&lt;/p&gt;

&lt;p&gt;Before settling on this policy, I considered other paths and left a decision record (ADR). One option was to draw the line by script: let kanji and kana through, block Cyrillic and so on. Its upside is that Japanese domains would get through. But even within the allowed scripts there are confusable characters, so it just moves the problem somewhere else. Another option was to judge by visual similarity, which is the same idea Chrome uses, and exactly the approach that let this one slip through. And as I said, I dislike Japanese domains.&lt;/p&gt;

&lt;p&gt;What I agonized over most was an allowlist and a "proceed just this once" button. They'd be convenient. But as long as the warning screen has a button to proceed, there's room for someone to sweet-talk you into pressing it. Phishing is, after all, also the art of getting people to press buttons. I decided to leave them out of v1, and recorded them in the ADR as candidates to add if they ever become truly necessary.&lt;/p&gt;

&lt;p&gt;The price I pay instead is clear. Legitimate Japanese domains like &lt;code&gt;日本語.jp&lt;/code&gt; get blocked too. Since I dislike Japanese domains myself, I judged that these false positives don't hurt me. For people who use Japanese domains, it's an inconvenient extension, and I said so near the top of the README. This is a tool for me, first and foremost. It's enough if the people who share my philosophy get it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Just one blocking rule
&lt;/h2&gt;

&lt;p&gt;The core of the implementation is Chrome's declarativeNetRequest (DNR from here on). An extension registers rules with Chrome along the lines of "treat requests matching this condition like this," and from then on Chrome applies them itself. The extension doesn't need to peek at every page the user visits.&lt;/p&gt;

&lt;p&gt;PunyGuard registers just one rule, and its condition is this regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^https?://(?:[^/?#]*[.@])?xn--
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading from the start of the URL, it matches if any label in the hostname starts with &lt;code&gt;xn--&lt;/code&gt;. It also blocks when the top-level domain is an IDN (e.g. &lt;code&gt;example.xn--q9jyb4c&lt;/code&gt;). It doesn't match hosts that merely have &lt;code&gt;xn--&lt;/code&gt; in the middle of a label (&lt;code&gt;axn--b.com&lt;/code&gt;), or &lt;code&gt;xn--&lt;/code&gt; appearing in the path or query. It applies to two things, top-level page navigations and pages embedded in another page (iframes), and doesn't affect loading of resources like images or scripts.&lt;/p&gt;

&lt;p&gt;DNR converts the hostname to Punycode before matching. So even if you type &lt;code&gt;аррӏө.com&lt;/code&gt; into the address bar in Unicode, the same rule catches it. Clicking a link, typing in the address bar, a server-side redirect, a link opened from another app: whatever the path, the result is the same.&lt;/p&gt;

&lt;p&gt;The requirement I first wrote down was "if a URL contains anything other than English ASCII, refuse the click." While working out the plan, I realized I couldn't build it as written, and revised it in two directions.&lt;/p&gt;

&lt;p&gt;First, I narrowed the check to the hostname (FQDN) only. If paths were included, pages like &lt;code&gt;ja.wikipedia.org/wiki/東京&lt;/code&gt; would get blocked too, which would make it unusable. Besides, by the time a request goes out, Japanese in the path has already been converted into an alphanumeric sequence like &lt;code&gt;%E6%9D%B1&lt;/code&gt;, so you can't even ask "does it contain non-ASCII?"&lt;/p&gt;

&lt;p&gt;Second, I widened it to block paths other than clicks. Behind the wording of the requirement was the actual goal: "don't let me get to a fake site." Rather than building it to the letter, I chose to build it to that goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ask for one more permission?
&lt;/h2&gt;

&lt;p&gt;After blocking, what do you show the user? This is where I made one big decision.&lt;/p&gt;

&lt;p&gt;DNR has &lt;code&gt;block&lt;/code&gt;, which simply stops the request, and &lt;code&gt;redirect&lt;/code&gt;, which sends it somewhere else. With &lt;code&gt;block&lt;/code&gt;, the permissions you request stay minimal. But what the user sees is Chrome's standard error screen, &lt;code&gt;ERR_BLOCKED_BY_CLIENT&lt;/code&gt;. That gives no way to tell whether the network is acting up or an extension blocked it. Faced with an unexplained wall, some people would get confused and just remove the extension.&lt;/p&gt;

&lt;p&gt;So I went with &lt;code&gt;redirect&lt;/code&gt;, sending the user to a warning page bundled with the extension. The catch is that &lt;code&gt;redirect&lt;/code&gt; requires access to all sites (&lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt;). Minimal permissions versus telling the user why it was blocked: I weighed the two and chose the latter.&lt;/p&gt;

&lt;p&gt;Access to all sites is on the larger end of extension permissions. That's exactly why I keep everything else doing as little as possible. There isn't a single program that touches the content of the page you're viewing (a content script). It makes no external requests and stores nothing. What it requests, what it doesn't do, and why are all written plainly in the README. An extension that can't explain why it needs such broad permissions deserves not to be trusted.&lt;/p&gt;

&lt;p&gt;The redirect URL includes the extension's ID, and the ID varies by environment. So instead of hard-coding the rule in a file, the extension builds and registers it when it starts up. It re-registers every time: on install and update, and on browser startup. Dynamically registered rules are supposed to persist across restarts, but I decided not to rely on that "supposed to."&lt;/p&gt;

&lt;h2&gt;
  
  
  The warning page trusts nothing
&lt;/h2&gt;

&lt;p&gt;The warning page shows the original URL, the hostname in both Unicode and Punycode, and a list of the non-ASCII characters. For &lt;code&gt;аррӏө.com&lt;/code&gt;, you can see that &lt;code&gt;ө&lt;/code&gt; is Cyrillic U+04E9. It's a screen that reveals, character by character, an identity you can't see by looking. The display language follows the browser: Japanese for Chrome in Japanese, English otherwise.&lt;/p&gt;

&lt;p&gt;The original URL is passed in the fragment (the part after &lt;code&gt;#&lt;/code&gt;) of the redirect URL. The thing I was careful about here was not trusting any input to this page at all. The warning page can be opened from anywhere on the web with any fragment attached. You have to build it assuming a malicious string will show up.&lt;/p&gt;

&lt;p&gt;All output to the screen goes through &lt;code&gt;textContent&lt;/code&gt;, so there's no path for a string to be interpreted as HTML. If a URL comes in that can't be parsed, it shows only generic text, with no host information. The original URL isn't turned into a link, and there's no button to copy it either. I decided from the start not to build anything you could press by accident.&lt;/p&gt;

&lt;p&gt;On a smaller note, I tripped over the string &lt;code&gt;:~:&lt;/code&gt;. Chrome strips the &lt;code&gt;#:~:text=&lt;/code&gt; syntax, used to jump to a specific passage on a page, from the URL the page can read (&lt;code&gt;location.hash&lt;/code&gt;). If the original URL happens to contain &lt;code&gt;:~:&lt;/code&gt;, everything after it disappears. This was caught in a review by Codex, and I fixed it to read the URL from the navigation history, before the stripping happens.&lt;/p&gt;

&lt;p&gt;There are two buttons. The primary one, "Close this tab," is always shown. It's the action that best fits the goal of getting away from a suspicious link. The secondary one, "Go back to the previous page," is shown only when there's history. When the warning page appears inside an iframe, neither is shown. Closing the parent page's tab from inside an iframe goes beyond what the user intended.&lt;/p&gt;

&lt;p&gt;I thought about the buttons a lot. I wanted to get away with a single button, "go back if you can, close if you can't," but browsers have no way to accurately check whether there's cross-site history behind you. I also considered going back, waiting a bit, and closing the tab if the page was still there. But deciding based on a wait time risks closing the tab while it's still on its way back to a heavy page. Rather than break the tab with an inaccurate check, it's better to accept that there will be cases where it misses, and say so honestly. That's why I split it into two buttons.&lt;/p&gt;

&lt;p&gt;I also planned for cases where scripts don't run. If the redirect lands in an iframe whose embedder has disabled scripts with the &lt;code&gt;sandbox&lt;/code&gt; setting, the warning page's JavaScript won't run. So the HTML ships with a fixed warning message, in both Japanese and English, from the start. Even without the details, the one line "PunyGuard blocked navigation to this page" gets through.&lt;/p&gt;

&lt;p&gt;One more thing: if the URL contains a password, as in &lt;code&gt;https://user:password@...&lt;/code&gt;, it's removed both from the display and from the warning page's own address bar. Exposing a secret on screen as a result of blocking would defeat the purpose of a protective tool. That said, the other Shossan living in my head does feel like "you didn't have to go that far..."&lt;/p&gt;

&lt;h2&gt;
  
  
  The scariest defense is the one that fails silently
&lt;/h2&gt;

&lt;p&gt;The thing I most want to avoid in a security tool is a state where it's broken and nobody notices. When it looks like it's working but actually isn't protecting anything. In developing PunyGuard, I spent a lot of time figuring out how to stamp out these "silent failures."&lt;/p&gt;

&lt;p&gt;The first pitfall was regex dialects. DNR regexes aren't interpreted by JavaScript's regex engine but by a separate one called RE2. RE2 doesn't support some constructs, like lookahead, and Chrome also rejects regexes that are too large. Meanwhile, ordinary unit tests run on JavaScript regexes. Even if you use something RE2 can't handle, the tests pass. Ship it like that, and rule registration fails, and nothing gets blocked. And nothing at all happens on screen.&lt;/p&gt;

&lt;p&gt;I considered testing with a package that lets you call RE2 from Node instead. But that package translates JavaScript-style syntax for RE2 before running it, so it even lets through syntax that Chrome would reject. No matter how much you check in a similar environment, you won't get the same answer as production.&lt;/p&gt;

&lt;p&gt;What I ended up choosing was to have Chrome itself try it. In end-to-end tests (E2E tests) that actually drive a browser, I load the extension into a real Chromium. Then I pass the exact rule I'm going to register to Chrome's &lt;code&gt;isRegexSupported&lt;/code&gt; and have it answer whether it can be used. In the end, only production knows the answer. So run it in production.&lt;/p&gt;

&lt;p&gt;Even so, the chance of registration failing on a user's machine isn't zero. When that happens, a &lt;code&gt;!&lt;/code&gt; badge shows up on the toolbar icon.&lt;/p&gt;

&lt;p&gt;There was one more easy-to-miss hole. On Chrome's extensions page, users can restrict site access to "On click" or to specific sites. When they do, rule registration still succeeds, but only the redirect stops working. No error shows up anywhere. So I added a check for access to all sites, and if it's missing, the same &lt;code&gt;!&lt;/code&gt; appears. You can see the cause in the tooltip that appears when you hover over the icon. To keep a stale check result from overwriting the badge when access is toggled quickly, the checks run one at a time, in order.&lt;/p&gt;

&lt;p&gt;In a normal window, if there's no &lt;code&gt;!&lt;/code&gt;, blocking is working. I wanted to make it unmistakable that the settings and state are correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Say what it can't do
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sho7650/punyguard" rel="noopener noreferrer"&gt;PunyGuard&lt;/a&gt; has a few weaknesses I left in knowingly.&lt;/p&gt;

&lt;p&gt;One is a known false positive. If &lt;code&gt;xn--&lt;/code&gt; appears in the username part of a URL, as in &lt;code&gt;https://xn--a@example.com/&lt;/code&gt;, it gets blocked even though the host is plain ASCII. Fixing it properly would take a pretty complex regex within RE2's constraints. I've never seen anyone open a URL like this, and the only harm is the blocker being wrong, so for now it stays as specified behavior, and there's a test confirming it.&lt;/p&gt;

&lt;p&gt;Another is the back button. If the warning page opens as the first page of a new tab, and you navigate elsewhere and then come back with the browser's back button, "Go back to the previous page" is shown, but pressing it does nothing. That's the price of "accepting cases where it misses" from earlier; even in this state, "Close this tab" works.&lt;/p&gt;

&lt;p&gt;What I struggled with most was incognito windows. Chrome doesn't run extensions in incognito windows unless the user turns on "Allow in Incognito." If it isn't allowed, IDN hosts aren't blocked, and the badge doesn't warn you either. Showing &lt;code&gt;!&lt;/code&gt; forever to someone who has deliberately chosen not to allow it would be unkind.&lt;/p&gt;

&lt;p&gt;Even when it is allowed, the bundled warning page can't be shown in incognito tabs. Navigation to IDN hosts is blocked, but the screen shows Chrome's &lt;code&gt;ERR_BLOCKED_BY_CLIENT&lt;/code&gt;, and the address bar keeps the original URL, password and all. Switching the extension's operating mode might have made it possible to show the warning page. But that would mean reworking the foundation of the design, and all it would buy is the "show the reason" part, not the "block" part. Since blocking itself works, this time I only documented it in the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hand repetitive work to machines
&lt;/h2&gt;

&lt;p&gt;Once I actually built it, I spent more time on the machinery around &lt;a href="https://github.com/sho7650/punyguard" rel="noopener noreferrer"&gt;PunyGuard&lt;/a&gt; than on PunyGuard itself.&lt;/p&gt;

&lt;p&gt;The development environment is generated from &lt;a href="https://github.com/sho7650/copier-ts7" rel="noopener noreferrer"&gt;copier-ts7&lt;/a&gt;, a template for TypeScript 7 that I made. The Node.js and pnpm versions are pinned with a tool called Nix, so the same versions run both on my machine and in CI, which runs automated tests on GitHub. A single &lt;code&gt;pnpm run check&lt;/code&gt; runs, in order, type checking, linting, format checking, and tests that require at least 80% coverage.&lt;/p&gt;

&lt;p&gt;Releases are handled by an automation tool called &lt;a href="https://github.com/googleapis/release-please" rel="noopener noreferrer"&gt;release-please&lt;/a&gt;. As long as commit messages follow a set format, every time a change lands on main, it automatically creates a PR with the next version and the changelog. Merging that PR creates a tag and a GitHub Release, and bumps the version in &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;manifest.json&lt;/code&gt;. A workflow then builds the tagged commit and attaches &lt;code&gt;punyguard-vX.Y.Z.zip&lt;/code&gt; to the Release. Handy. I always forget to bump version numbers, so most of the apps I publish manage their versions this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the human doesn't hand over when building with AI
&lt;/h2&gt;

&lt;p&gt;As has become my usual lately, I built &lt;a href="https://github.com/sho7650/punyguard" rel="noopener noreferrer"&gt;PunyGuard&lt;/a&gt; together with Claude Code. Write a plan, write tests first, then implement, set up E2E tests, build the release pipeline. Most of the code writing and running was done by the AI. And for every PR, I had Codex review it. I've set up skills so all of this happens entirely within Claude Code.&lt;/p&gt;

&lt;p&gt;This combination is very effective, and not just for this app. The &lt;code&gt;:~:&lt;/code&gt; URL truncation, tests and the shipped build loading different files, the warning page not opening in incognito windows: all of these were first found in Codex reviews. Separating the writer from the reader makes it less likely that both fall into the same assumptions and the same unnecessary biases. The logic is the same as asking someone else to review your code on a human team.&lt;/p&gt;

&lt;p&gt;On the other hand, there's one thing I didn't hand to the AI: making the decisions.&lt;/p&gt;

&lt;p&gt;I'm the one who decided to block legitimate Japanese domains too. The AI's reaction was along the lines of "surely Japanese is fine...", but as I keep saying, I dislike Japanese domains. Narrowing the check to hostnames, and deciding to handle the incognito problem in the README only, were also all my calls. The AI lays out the options and the cost of each, carefully. But which cost to accept can only be decided by a human who knows the circumstances of the people using it. I can say the pain of false positives is small precisely because I'm someone who dislikes Japanese domains.&lt;/p&gt;

&lt;p&gt;That's why I always write down what I decide. Decisions, their reasoning, and the options I considered and rejected go in ADRs. Definitions of terms go in a glossary (CONTEXT.md). The plan, progress, and deferred issues with their reasons go in the plan document (tasks/todo.md). When working with AI, conversation memory gets summarized mid-way or cut off across days. If I had to explain everything from scratch each time, the context wouldn't be enough, and neither would my patience. If it's written down, the AI can interpret it in the next session and start from the same premises. The same goes for me reading it back six months from now.&lt;/p&gt;

&lt;p&gt;I also kept the final check as a human job. Links opened from external apps, switching Chrome's display language, the badge when site access is restricted, browser restarts, incognito windows. These are hard to verify with automated tests, so I checked each one myself in a real Chrome. You must not trust AI-written code on the strength of AI-written tests alone. Only after checking with my own eyes can I look anyone in the eye and say "it works."&lt;/p&gt;

&lt;p&gt;AI lends you more hands and an extra pair of eyes. Deciding what to build, what to give up, and whether it really works is, for now, the human's role. Draw that line clearly, and you can hand work to AI with peace of mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  I didn't choose the path of better detection
&lt;/h2&gt;

&lt;p&gt;There was surely a path of making the detection smarter. Grow the list, refine the similarity calculation, keep adding exceptions. PunyGuard didn't take that path. I made one single decision, not to judge, and handed the job of upholding it to a machine that never gets tired.&lt;/p&gt;

&lt;p&gt;Choose simplicity that never hesitates over competing on cleverness. That's the distance I want to keep when working with computers and AI.&lt;/p&gt;

&lt;p&gt;I decided not to put &lt;a href="https://github.com/sho7650/punyguard" rel="noopener noreferrer"&gt;PunyGuard&lt;/a&gt; on the Chrome Web Store. It's distributed only on GitHub. Grab &lt;code&gt;punyguard-vX.Y.Z.zip&lt;/code&gt; from Releases, unzip it, turn on Developer mode at &lt;code&gt;chrome://extensions&lt;/code&gt;, and load it, and it'll work. Please leave site access set to "On all sites." If you don't use Japanese domains and you're worried about phishing with look-alike domains, I'd be glad if you gave it a try.&lt;/p&gt;

&lt;p&gt;Why not the Web Store? It's something I made for myself, and I just didn't want to start fielding "please make it do this!!" requests from other people. That said, if you share Shossan's philosophy, feel free to use it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sho7650/punyguard" rel="noopener noreferrer"&gt;https://github.com/sho7650/punyguard&lt;/a&gt;&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>security</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>My Obsidian Vault Made 21 Permanent Notes in 3.5 Years, So I Redesigned It</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Sun, 11 Oct 2026 03:50:09 +0000</pubDate>
      <link>https://dev.to/sho7650/my-obsidian-vault-made-21-permanent-notes-in-35-years-so-i-redesigned-it-2229</link>
      <guid>https://dev.to/sho7650/my-obsidian-vault-made-21-permanent-notes-in-35-years-so-i-redesigned-it-2229</guid>
      <description>&lt;h2&gt;
  
  
  What I use Obsidian for, and what went wrong
&lt;/h2&gt;

&lt;p&gt;I adopted Obsidian to build knowledge for myself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Take in lots of personal knowledge through all kinds of methods, and turn it, one by one, into Permanent Notes: finished pieces of my own wisdom&lt;/li&gt;
&lt;li&gt;Build up those Permanent Notes and hand them down as a body of explicit knowledge for those who come after me&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Above all, I believed Obsidian's job was to keep producing solid Permanent Notes out of the collective knowledge I'd gathered. Or so I thought.&lt;/p&gt;

&lt;p&gt;But the Permanent Notes just weren't growing. At all. It was a disaster. Obsidian had been reduced to a plain old diary.&lt;/p&gt;

&lt;p&gt;I'd also assumed, on my own, that the reason Permanent Notes weren't growing was that "the notes aren't connected to each other." But when I dug into the vault with Claude Code's help, the cause turned out to be somewhere else. What was missing was a step where I write down my own claims, and the time to actually do it every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  21 notes in three and a half years, zero in the last 90 days
&lt;/h2&gt;

&lt;p&gt;What I want from Obsidian is to keep producing solid Permanent Notes, one at a time, from the knowledge I collect. Each note is meant to be about the size of an index card, something my future self or people coming after me can reuse.&lt;/p&gt;

&lt;p&gt;When I actually counted, here's what I found:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Actual&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Number of Permanent Notes&lt;/td&gt;
&lt;td&gt;21 (over roughly three and a half years since February 2023)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last Permanent Note update&lt;/td&gt;
&lt;td&gt;August 2025. Zero in the last 90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inflow over the last 90 days&lt;/td&gt;
&lt;td&gt;79 Clippings, 142 AI conversations and summaries, 1 Fleeting Note, 6 Inbox&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What comes in is other people's writing and AI's writing. There are almost none of my own insights in Obsidian. My Daily Note has a section called "Thoughts in my mind," but I've kept it empty the whole time. Meanwhile, the KPI section where I log sleep hours and running distance gets filled in every day. The sections measured in numbers stuck; the free-writing section didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding links didn't grow my Permanent Notes
&lt;/h2&gt;

&lt;p&gt;In March 2026, I built a Claude skill, &lt;code&gt;/zettelkasten&lt;/code&gt;, that automatically adds tags and links to Fleeting Notes, and ran it on 99 of 281 notes. Each note also got a list of related notes appended at the end. That was seven months ago. Not a single note has been promoted since.&lt;/p&gt;

&lt;p&gt;The reason was obvious the moment I opened the Fleeting Notes. Most of them are just a title. Terrible. One note I wrote in February 2024, "Transcribing from memos is what really matters, so set up a time to transcribe," had an empty body too. I'd written down the answer myself two years earlier, then left it there without ever making time to transcribe. Truly terrible.&lt;/p&gt;

&lt;p&gt;The notes that did have a body had problems too. "Architecture is just one hypothesis" still contained an AI reply, "Thank you for your understanding," verbatim. Worse and worse. Before I could even connect anything, the things I'd be connecting weren't in my own words.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bar of "unshakable intelligence" was what kept me from writing
&lt;/h2&gt;

&lt;p&gt;I had selfishly defined a &lt;code&gt;Permanent Note&lt;/code&gt; as "definitive, unshakable intelligence." With that bar, the day I could call a note finished was never going to come.&lt;/p&gt;

&lt;p&gt;My existing &lt;code&gt;適応度関数.md&lt;/code&gt; (fitness function) note was a textbook example. The title says "fitness" (適応度), yet the body says "goodness of fit" (適合度) nine times, for some mysterious reason. The title and the content don't even fit each other. It cited no sources, and the second half just restated the first. I hadn't touched it since May 2024. The note that was supposed to be unshakable had, in reality, become a note nobody revisits, or even looks at.&lt;/p&gt;

&lt;p&gt;So I lowered the bar to "correct as of the verification date, and revisable later," and set six criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The title is a single-sentence claim&lt;/li&gt;
&lt;li&gt;The body is in my own words and covers the claim, the reasoning, a concrete example, and the scope (around 200–400 Japanese characters)&lt;/li&gt;
&lt;li&gt;There's at least one piece of supporting evidence&lt;/li&gt;
&lt;li&gt;The verification result and the verification date are recorded&lt;/li&gt;
&lt;li&gt;The note makes sense when read on its own&lt;/li&gt;
&lt;li&gt;Its visibility (&lt;code&gt;visibility&lt;/code&gt;) is set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added the last one to check whether the content trips any confidentiality concerns: words meant only for me should be explicitly marked as never to be seen by anyone else. If you're going to let arbitrary readers see your notes, you need to decide, note by note, whether each one is okay to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  I write the claims; AI handles verification
&lt;/h2&gt;

&lt;p&gt;I chose who writes the body from three options. Letting AI write it and just approving would be the fastest and produce the most notes. But that's the same as building one more "storehouse of AI-written text," just like what I already had. I chose the option where I write the claims and reasoning myself. Obviously. &lt;code&gt;Claude Code&lt;/code&gt; can't be expected to understand what's obvious to me, but I think I should be grateful that it catches the biases humans like me let slip by.&lt;/p&gt;

&lt;p&gt;Since these are my claims and my own knowledge in the first place, I narrowed the AI's role down to the minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check factual statements against primary sources&lt;/li&gt;
&lt;li&gt;Present the strongest counterargument to the claim&lt;/li&gt;
&lt;li&gt;Suggest related notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don't let the AI rewrite the body. It only points out what to fix; I'm the one who rewrites. With this setup, about 100 notes a year is the ceiling, but only knowledge I can explain myself gets properly kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the writing entirely inside Obsidian
&lt;/h2&gt;

&lt;p&gt;The first system that came out of this was one Claude Code proposed to suit itself: a flow driven by conversation with Claude Code. It asks me questions, I answer, and I get verification results on the spot. It's comfortable enough when you use it, but you can't run it from inside Obsidian. That didn't match my premise: right after reviewing my Daily Note, spend 15 minutes writing, right there in Obsidian.&lt;/p&gt;

&lt;p&gt;Breaking the process into steps, the only part of this flow that actually needs AI is verification. The questions are the same every time, so I realized I could just put them in a template. So I moved the writing into Obsidian and turned verification into a batch job that runs in the background.&lt;/p&gt;

&lt;p&gt;My daily flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look at the "Today's one" section I newly added to my Daily Note. It lists notes awaiting review of their verification results, drafts in progress, and Permanent Note candidates.&lt;/li&gt;
&lt;li&gt;If anything is awaiting review, read that note's "Verification" section. If something needs fixing, fix the body, and for each counterargument, check either "reflected in scope" or "not reflected."&lt;/li&gt;
&lt;li&gt;If nothing is awaiting review, pick one candidate and write a new note from the Permanent Note template.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the template, right after the field for the claim, I put three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why do I think so?&lt;/li&gt;
&lt;li&gt;What experience of mine led me to this, or matched it?&lt;/li&gt;
&lt;li&gt;In what cases does it not apply?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For verification, I run the following command in the vault folder. It inspects all drafts at once and writes the results into each note's "Verification" section.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"/zettelkasten verify --all"&lt;/span&gt; &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Glob,Grep,Edit,Write,WebSearch,WebFetch"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verification takes time, so I stop working for the moment, and the back-and-forth in the moment goes away. In exchange, writing can continue even when the AI isn't running, and I can write from my iPhone or iPad. I set it up so it doesn't depend on where I am or what device I'm on.&lt;/p&gt;

&lt;p&gt;For Permanent Note candidates, besides Fleeting Notes and the Inbox, I also included the directory where I save my AI conversations. I'm starting to feel this is the downside of splitting things into too many directories. If you're just starting out with Obsidian, I recommend keeping directories to a minimum. A single inbox is the easiest place to operate for stashing information and dropping insights.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first note was carved out of a published article
&lt;/h2&gt;

&lt;p&gt;Alongside writing new Permanent Notes from candidates, I decided to also carve them out of articles I've already published. Those articles are already in my own words. They're backed by research and already in publishable shape, so they become finished notes much faster than writing from a candidate from scratch.&lt;/p&gt;

&lt;p&gt;I made the very first one from &lt;a href="https://zenn.dev/sho7650/articles/332f305b24fa68" rel="noopener noreferrer"&gt;my Zenn article on Copier&lt;/a&gt;. Verification confirmed two points. First, a repository created from a GitHub template can't merge updates. Second, Copier can pull updates into a project after it has been generated.&lt;/p&gt;

&lt;p&gt;Two counterarguments came back. One was that the update mechanism itself takes effort to maintain. I reflected that by adding "when there are few downstream projects" to the scope. The other pointed out that the update mechanism hasn't yet been tested with a major change. I finalized that one as-is, and I'll revise the note after trying updates with TypeScript 7.1 or 7.2. Being able to assume revision is thanks to lowering the bar. I'm grateful for that. There are times I just can't help wanting perfection, and at times like that, it becomes a shackle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I dropped, and what I haven't confirmed yet
&lt;/h2&gt;

&lt;p&gt;There are two things I decided not to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Processing the remaining 182 Fleeting Notes. Two-year-old title-only memos can't be decoded even by the person who wrote them. I'll start from new insights and only pick up old memos when they become relevant.&lt;/li&gt;
&lt;li&gt;Consolidating directories. I do think I split things too much, but stages can be managed with a property called &lt;code&gt;status&lt;/code&gt;, so I leaned on &lt;a href="https://community.obsidian.md/plugins/dataview" rel="noopener noreferrer"&gt;Dataview&lt;/a&gt; to run things without moving any folders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are also things I haven't confirmed yet. The template and the list are brand new, and I haven't run the whole thing end to end inside Obsidian. Whether everything from entering a claim, to recording the seed, to moving it into a folder works as expected will take a few days of testing. I've also only tried the batch verification with zero drafts. I plan to set up scheduled runs myself once the system has been running for a while.&lt;/p&gt;

&lt;p&gt;I'll judge whether this works by whether I have at least 8 verified Permanent Notes four weeks from now. By then, I should be able to share the verification results too.&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>productivity</category>
      <category>ai</category>
      <category>claude</category>
    </item>
    <item>
      <title>Minimal Practical Ontology Sample with MCP and Claude. A mechanism to pass Business Definitions, not just data, to AI.</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:13:22 +0000</pubDate>
      <link>https://dev.to/sho7650/minimal-practical-ontology-sample-with-mcp-and-claude-a-mechanism-to-pass-business-definitions-227d</link>
      <guid>https://dev.to/sho7650/minimal-practical-ontology-sample-with-mcp-and-claude-a-mechanism-to-pass-business-definitions-227d</guid>
      <description>&lt;p&gt;The word &lt;em&gt;ontology&lt;/em&gt; has suddenly become familiar in recent years. It appears in data platform product descriptions and in discussions of AI agent design. However, I suspect few people have actually worked with one hands-on. In fact, I was one of them.&lt;/p&gt;

&lt;p&gt;What you cannot understand through words alone, you must understand by running it. So I built a minimal &lt;em&gt;ontology-like thing&lt;/em&gt; using only the bare essentials, and prepared a program that lets Claude act as the orchestrator. The program itself is published on GitHub. In this article, I will describe what that sample is, what it can and cannot do, and what insights emerged from it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What &lt;em&gt;Ontology&lt;/em&gt; Means Here
&lt;/h2&gt;

&lt;p&gt;Let me first define the scope of the term. This is not philosophical ontology, nor OWL from the Semantic Web. What I am dealing with here is the more practical meaning used in the context of business systems.&lt;/p&gt;

&lt;p&gt;In this article, &lt;em&gt;ontology&lt;/em&gt; refers to something that declares the following three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Object types:&lt;/strong&gt;&lt;br&gt;
The kinds of &lt;em&gt;things&lt;/em&gt; that appear in business—such as customers and orders—and their attributes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link types:&lt;/strong&gt;&lt;br&gt;
Relationships between things—for example, a customer &lt;em&gt;places&lt;/em&gt; an order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action types:&lt;/strong&gt;&lt;br&gt;
Operations permitted on things—for example, &lt;em&gt;canceling&lt;/em&gt; an order. This includes the conditions under which the action can be executed and what changes as a result.&lt;/p&gt;

&lt;p&gt;The third one is the key. If you only have attributes and relationships, that is no different from an ER diagram or a schema. When you include &lt;em&gt;what is allowed to be done&lt;/em&gt; in the declaration, the definition of data transforms into the definition of business. While a database says &lt;em&gt;the order table has a status column&lt;/em&gt;, an ontology says &lt;em&gt;only unshipped orders can be canceled&lt;/em&gt;. This difference is the subject of this article.&lt;/p&gt;

&lt;p&gt;Let me touch on how this differs from similar concepts. Database input rules and validation ensure that a single piece of data has the correct form—for example, &lt;em&gt;status can only be open, shipped, or cancelled&lt;/em&gt;. Business logic holds judgments like &lt;em&gt;if already shipped, never allow cancellation again&lt;/em&gt; inside application code.&lt;/p&gt;

&lt;p&gt;Both are very important for actual business execution, but the former only looks at the shape of data, while the latter buries judgments deep in code where they become a black box. An ontology can combine both into a single declaration—&lt;em&gt;only unshipped orders can be canceled&lt;/em&gt;—and place it outside the code, in a form readable by both humans and AI. The significant difference is that what is being protected is not the database or the application, but the definition of the business itself. This difference in where things are placed is likely the key point that distinguishes the three.&lt;/p&gt;


&lt;h2&gt;
  
  
  Overview of the Sample Program
&lt;/h2&gt;

&lt;p&gt;The configuration consists of only four elements.&lt;/p&gt;

&lt;p&gt;First is the &lt;strong&gt;ontology definition&lt;/strong&gt;. Object types, link types, and action types are written in a single YAML file. This is the main component; everything else is supporting cast.&lt;/p&gt;

&lt;p&gt;Second is the &lt;strong&gt;instance store&lt;/strong&gt;. I use PostgreSQL, but there are only two tables. An &lt;code&gt;objects&lt;/code&gt; table that holds all types of objects as &lt;em&gt;type, ID, attributes (JSONB)&lt;/em&gt;, and a &lt;code&gt;links&lt;/code&gt; table that holds relationships as &lt;em&gt;type, from, to&lt;/em&gt;. Because I do not create a table for each type, adding a type to the YAML does not change the DDL.&lt;/p&gt;

&lt;p&gt;Third is the &lt;strong&gt;ontology MCP server&lt;/strong&gt;. It reads the YAML, wraps the database, and exposes only five generic tools to Claude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;describe_ontology&lt;/code&gt;: Returns a list of types, links, and actions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search_objects&lt;/code&gt;: Searches by attributes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_object&lt;/code&gt;: Retrieves a single item&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;traverse&lt;/code&gt;: Follows links&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;execute_action&lt;/code&gt;: Validates preconditions, applies effects if passed, and records an audit log&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not add tools for each type. If I designed it with separate searches for customers and orders, the tools would grow to fifty once types reached ten. By keeping it to five generic tools, the number stays at five no matter how many types are added.&lt;/p&gt;

&lt;p&gt;Fourth is the &lt;strong&gt;orchestrator&lt;/strong&gt;. For this project, I leave this entirely to Claude. Simply by registering the MCP server, Claude can call &lt;code&gt;describe_ontology&lt;/code&gt; first, understand what is possible, and then operate accordingly.&lt;/p&gt;

&lt;p&gt;With Docker Compose, the database and MCP server start up together.&lt;/p&gt;


&lt;h2&gt;
  
  
  What This Program Can Do
&lt;/h2&gt;

&lt;p&gt;First, &lt;strong&gt;exploration&lt;/strong&gt;. If you ask &lt;em&gt;What types and actions does this ontology have?&lt;/em&gt;, Claude will explain the definition directly. For questions like &lt;em&gt;List the Gold customers&lt;/em&gt;, &lt;em&gt;Show me all orders from Yamada Trading&lt;/em&gt;, or &lt;em&gt;Whose order is O1003?&lt;/em&gt;, it combines searching and link traversal to answer.&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="err"&gt;❯&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;me&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Yamada&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Trading&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;search&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Yamada&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Trading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ontology&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;MCP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;server.&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="err"&gt;ontology&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="err"&gt;describe_ontology&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(MCP)&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;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object_types"&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;"Customer"&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;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A trading customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"name"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"tier"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"values"&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="s2"&gt;"bronze"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"silver"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"gold"&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;span class="nl"&gt;"email"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Order"&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;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"An order from a customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"amount"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"status"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"values"&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="s2"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"shipped"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"cancelled"&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;span class="nl"&gt;"note"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;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;"link_types"&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;"places"&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;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A customer places an order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cardinality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"one_to_many"&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;span class="nl"&gt;"action_types"&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;"cancel_order"&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;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cancel an unshipped order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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;"reason"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;span class="nl"&gt;"preconditions"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"property"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&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;span class="nl"&gt;"effects"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cancelled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$params.reason"&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;span class="nl"&gt;"ship_order"&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;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mark an order as shipped"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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;"preconditions"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"property"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&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;span class="nl"&gt;"effects"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipped"&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;span class="nl"&gt;"upgrade_tier"&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;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upgrade a customer tier by one level"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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;"preconditions"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"property"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ne"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gold"&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;span class="nl"&gt;"effects"&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;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$next_enum"&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;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="err"&gt;⏺&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ontology&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="err"&gt;search_objects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(MCP)(type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;filter:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Yamada Trading"&lt;/span&gt;&lt;span class="p"&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C001"&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;"Yamada Trading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yamada@example.com"&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;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="err"&gt;ontology&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="err"&gt;traverse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(MCP)(type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C001"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;link:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"places"&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"O1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipped"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"O1002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipped"&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;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Yamada&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Trading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(Customer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;C&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tier&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;gold)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;has&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders.&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="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&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="err"&gt;Amount&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="err"&gt;Status&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="err"&gt;Note&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="err"&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="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1001&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="mi"&gt;120&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&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="err"&gt;shipped&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="err"&gt;none&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="err"&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="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1002&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="mi"&gt;45&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&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="err"&gt;shipped&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="err"&gt;none&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="err"&gt;└──────────┴─────────┴────────────┴──────┘&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;165&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;There&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unshipped&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(open)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cancelled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, &lt;strong&gt;action execution&lt;/strong&gt;. If you submit &lt;em&gt;Cancel all unshipped orders from Gold customers due to stock shortage&lt;/em&gt;, Claude searches for customers, follows links to gather orders, and executes &lt;code&gt;cancel_order&lt;/code&gt; one by one. A human only needs to give this instruction in a single line of natural language.&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="err"&gt;❯&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cancel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unshipped&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Gold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;customers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;due&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;stock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shortage&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;search&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Gold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;customers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cancel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;their&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unshipped&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(open)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders.&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="err"&gt;ontology&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="err"&gt;search_objects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(MCP)(type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;filter:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"gold"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;limit:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C001"&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;"Yamada Trading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yamada@example.com"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C003"&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;"Suzuki Manufacturing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"suzuki@example.com"&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;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="err"&gt;ontology&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="err"&gt;traverse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(MCP)(type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C003"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;link:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"places"&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"O1004"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"O1005"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cancelled"&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;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="err"&gt;ontology&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="err"&gt;execute_action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(MCP)(action:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cancel_order"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;target_id:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"O1004"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;params:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Stock shortage"&lt;/span&gt;&lt;span class="p"&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"O1004"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cancelled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stock shortage"&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;span class="err"&gt;There&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unshipped&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Gold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;customers,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cancellation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;complete.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;affected&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Gold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;customers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders&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="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Customer&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="err"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&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="err"&gt;Amount&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="err"&gt;Before&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="err"&gt;After&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="err"&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="err"&gt;Yamada&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Trading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(C&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="err"&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="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1001&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="mi"&gt;120&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&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="err"&gt;shipped&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="err"&gt;No&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;change&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="err"&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="err"&gt;Yamada&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Trading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(C&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="err"&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="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1002&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="mi"&gt;45&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&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="err"&gt;shipped&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="err"&gt;No&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;change&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="err"&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="err"&gt;Suzuki&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Manufacturing&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="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1004&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="mi"&gt;80&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&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="err"&gt;open&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="err"&gt;cancelled&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="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(C&lt;/span&gt;&lt;span class="mi"&gt;003&lt;/span&gt;&lt;span class="err"&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="err"&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="err"&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="err"&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="err"&gt;Suzuki&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Manufacturing&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="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1005&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="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&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="err"&gt;cancelled&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="err"&gt;No&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;change&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="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(C&lt;/span&gt;&lt;span class="mi"&gt;003&lt;/span&gt;&lt;span class="err"&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="err"&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="err"&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="err"&gt;└─────────────────────────┴──────────┴─────────┴────────────┴────────────┘&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reason&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stock shortage"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;has&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;been&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;recorded&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;note&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1004&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;also&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;audit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;log.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;All&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Yamada&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Trading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;were&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;already&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shipped,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;so&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;there&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;were&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cancellation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;targets.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it &lt;strong&gt;properly refuses&lt;/strong&gt; when it understands the situation. For example, if you try to cancel a shipped order, the server-side precondition blocks this action. Claude reports: &lt;em&gt;This order has already been shipped, so it could not be canceled.&lt;/em&gt; If you ask for a cancellation without providing a reason, since &lt;code&gt;reason&lt;/code&gt; is a required parameter, Claude will ask you for the reason.&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="err"&gt;❯&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cancel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1001&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="err"&gt;ontology&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="err"&gt;get_object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(MCP)(type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"O1001"&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"O1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipped"&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="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cannot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;canceled.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;current&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shipped.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cancel_order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;has&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;precondition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;defined&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;that&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;can&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;applied&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;open.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Even&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;executed,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;would&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;rejected&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;due&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;precondition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;failure,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;so&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;changes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;have&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;been&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;made.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;current&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;follows:&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="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&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="err"&gt;Customer&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="err"&gt;Amount&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="err"&gt;Status&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="err"&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="err"&gt;O&lt;/span&gt;&lt;span class="mi"&gt;1001&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="err"&gt;Yamada&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Trading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(C&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="err"&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="mi"&gt;120&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&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="err"&gt;shipped&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="err"&gt;└──────────┴───────────────────────┴─────────┴────────────┘&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;If&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;you&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;need&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reverse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shipped&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;order,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ontology&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;have&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;actions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;defined&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;returns&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shipment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reversal,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;so&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;you&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;would&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;need&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;handle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;through&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;different&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;business&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;workflow.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I like most is this &lt;em&gt;refusal&lt;/em&gt;. The anxiety about letting AI touch business data usually boils down to &lt;em&gt;Will it do something unnecessary?&lt;/em&gt; In this mechanism, the means to do unnecessary things simply does not exist. There is no tool to directly rewrite the status; state can only change through actions. It is not safe because the AI is smart—it is safe because the logic side is built to be safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Program Cannot Do
&lt;/h2&gt;

&lt;p&gt;Let me also clarify what it cannot do. This is a mix of things deliberately omitted to define the scope of the demo and things I simply have not gotten around to.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Creating and deleting objects.&lt;/strong&gt; &lt;em&gt;Register a new customer&lt;/em&gt; is not possible. Sample data is loaded via SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creating and deleting links.&lt;/strong&gt; &lt;em&gt;Link this order to Sato Industries&lt;/em&gt; is also not possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Range searches and partial matching.&lt;/strong&gt; For &lt;em&gt;orders over 100,000 yen&lt;/em&gt;, Claude would have to fetch everything and filter it himself. This breaks down as the number of records grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preconditions spanning multiple objects.&lt;/strong&gt; Conditions that look at linked items—like &lt;em&gt;cancellation allowed only if the customer is Gold&lt;/em&gt;—cannot be written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactions for batch execution.&lt;/strong&gt; Execution is one item at a time, so if something fails midway, previous items are not rolled back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions.&lt;/strong&gt; Who executed something is a fixed value. &lt;em&gt;Who can do what&lt;/em&gt; is not implemented.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This list is also the list of what to do next. Range searches and preconditions that look at linked items in particular can be added just by slightly expanding the YAML vocabulary. I will fill these in as I feel &lt;em&gt;this is missing&lt;/em&gt; while building.&lt;/p&gt;




&lt;h2&gt;
  
  
  Insights Gained from This Program
&lt;/h2&gt;

&lt;p&gt;It is a small sample, but running it revealed three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First:&lt;/strong&gt; What should be given to AI is not &lt;em&gt;data&lt;/em&gt; but &lt;em&gt;the definition of the world&lt;/em&gt;. If you give tables and SQL, the AI can do anything. Being able to do anything also means not knowing what it is allowed to do. When you give types and actions, the AI stops hesitating. Watching Claude read the result of &lt;code&gt;describe_ontology&lt;/code&gt; and then proceed with exactly the steps I expected is just like someone reading a manual before starting work—literally someone who reads instructions and then acts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second:&lt;/strong&gt; Five generic tools are enough. Initially I considered a design that created tools for each type. I did not do so because if I had to rewrite code every time I added a type to the YAML, there would be no point in separating it into YAML. In fact, I added a &lt;code&gt;Product&lt;/code&gt; type and a &lt;code&gt;restock&lt;/code&gt; action to the YAML, restarted the container, and Claude began handling the new type. Separating definitions from code makes changes easy and fast, and improves manageability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third:&lt;/strong&gt; Audit logs should be built in from the start as part of actions, not added later. &lt;code&gt;execute_action&lt;/code&gt; always leaves one line, whether it succeeds or is blocked by a precondition. Who, on what type, tried to do what, and what happened. In a world where AI operates, no one can explain &lt;em&gt;why this happened&lt;/em&gt; without this record. I have not implemented it yet, but once &lt;em&gt;who&lt;/em&gt; is added here, it will become nearly the complete audit record needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Good About Ontology
&lt;/h2&gt;

&lt;p&gt;Summarizing the discussion so far, the goodness of ontology lies in &lt;em&gt;constraints&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I believe that turning routine work into programs is the original purpose of computers. Having written programs for nearly forty years, that thinking itself has not changed at all. What changed with the arrival of AI is that &lt;em&gt;non-routine parts&lt;/em&gt; can now also be delegated to computers. For example: which customers, which orders, in what sequence to process. Things that humans normally think about and do manually can now be thought about by AI.&lt;/p&gt;

&lt;p&gt;However, delegation requires boundaries. What is allowed, and what is not. Communicating those boundaries through natural language prompts is unreliable. Rather than asking &lt;em&gt;Please do not cancel shipped orders&lt;/em&gt;, it is more certain to build it so cancellation is impossible. An ontology is a mechanism that writes those boundaries as declarations and has the system enforce them.&lt;/p&gt;

&lt;p&gt;There is another good thing. When you write boundaries, the business becomes visible. To write a single line like &lt;em&gt;only unshipped orders can be canceled&lt;/em&gt;, you need to confirm with the business people. Conversations emerge: &lt;em&gt;Are there cases where shipped orders can also be canceled?&lt;/em&gt; &lt;em&gt;Is that a different action called a return?&lt;/em&gt; This conversation is the most important and most easily skipped part when building systems. A single YAML can also become a tool to draw out that conversation. Also, this YAML itself could be called a business design document.&lt;/p&gt;

&lt;p&gt;I believe the job of an architect is not to choose technology, but to express business as structure. An ontology is a place to put that structure in a form AI can read. That is why, even in the age of AI—no, precisely because it is the age of AI—I believe this tool will be effective.&lt;/p&gt;




&lt;h2&gt;
  
  
  Something to Try
&lt;/h2&gt;

&lt;p&gt;If you have business data at hand, forget the table definitions for now and write out these three things on paper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What kinds of &lt;em&gt;things&lt;/em&gt; appear there?&lt;/li&gt;
&lt;li&gt;What words connect things to things?&lt;/li&gt;
&lt;li&gt;For those things, who, under what conditions, is allowed to do what?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can write the third one, that is already an ontology. If you cannot write it, that is where the business is ambiguous. Either way, you gain something.&lt;/p&gt;

&lt;p&gt;The sample is on GitHub. Start it up with Docker Compose and ask Claude: &lt;em&gt;Cancel all unshipped orders from Gold customers.&lt;/em&gt; And watch the shipped orders get properly refused. All the value of this mechanism is packed into that moment.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>claude</category>
      <category>mcp</category>
    </item>
    <item>
      <title>I Let Claude Code Grill Me, Then Built a Zenn-to-dev.to Crossposting Pipeline</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:20:49 +0000</pubDate>
      <link>https://dev.to/sho7650/i-let-claude-code-grill-me-then-built-a-zenn-to-devto-crossposting-pipeline-2p54</link>
      <guid>https://dev.to/sho7650/i-let-claude-code-grill-me-then-built-a-zenn-to-devto-crossposting-pipeline-2p54</guid>
      <description>&lt;p&gt;I built a pipeline that translates the articles I write on Zenn into English and publishes them on &lt;a href="https://dev.to"&gt;dev.to&lt;/a&gt; too. I translate locally with Claude Code, and when I push to &lt;code&gt;main&lt;/code&gt;, GitHub Actions publishes, updates, or unpublishes the article on dev.to.&lt;/p&gt;

&lt;p&gt;My starting point was asherish's "&lt;a href="https://zenn.dev/asherish/articles/blog-repo-setup" rel="noopener noreferrer"&gt;Blog repository setup&lt;/a&gt;." Its setup, which bridges Zenn and dev.to with GitHub Actions, was a direct reference. But by the time I finished, my setup had made different calls from the original in three places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No file recording dev.to article IDs; match by &lt;code&gt;canonical_url&lt;/code&gt; every time&lt;/li&gt;
&lt;li&gt;Only the original Japanese article holds the publication state&lt;/li&gt;
&lt;li&gt;Detect a stale translation using a hash of the original article&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a setup where &lt;strong&gt;CI writes nothing back to the repository, and the only state lives on dev.to&lt;/strong&gt;. In this post, I'll walk through the order in which I made those decisions, and how I used Claude Code along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  I settled the requirements by letting Claude Code ask me 30+ questions
&lt;/h2&gt;

&lt;p&gt;The request I first gave Claude Code was just these three lines (originally in Japanese):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I want to create a GitHub Actions workflow that, when an article is added to articles/ for zenn.dev and becomes "published", posts a translated version of it to dev.to.&lt;br&gt;
Use this as a reference.&lt;br&gt;
Don't decide anything unclear by guessing; check everything with me.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I ran this request through &lt;code&gt;grill-with-docs&lt;/code&gt; from &lt;a href="https://github.com/mattpocock/skills" rel="noopener noreferrer"&gt;Matt Pocock's skill collection for Claude Code&lt;/a&gt;. As the name suggests, it's a skill that grills the user with questions, closing off design branches one at a time. It doesn't ask everything at once; it &lt;strong&gt;only asks a batch of questions whose prerequisites are already settled&lt;/strong&gt;. For example, until "where does translation happen?" is decided, it won't ask "where do we store the translation API key?"&lt;/p&gt;

&lt;p&gt;Here are some of the questions I actually got:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Claude's recommendation&lt;/th&gt;
&lt;th&gt;My answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Should the 18 existing published articles be crossposted too?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If the original is edited after publishing, update dev.to too?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post to dev.to as a draft, or publish immediately?&lt;/td&gt;
&lt;td&gt;Draft&lt;/td&gt;
&lt;td&gt;Publish immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where should images be served from (the repo is private)?&lt;/td&gt;
&lt;td&gt;Make the repo public&lt;/td&gt;
&lt;td&gt;Don't bring images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translate in CI, or locally?&lt;/td&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;Local Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For several questions, my answer differed from the recommendation. Drafts, for instance, were recommended "so you can review the machine translation once." But since I'd decided to translate locally, the review already happens before commit. That "where does translation happen?" question was one Claude added later, after reading the reference article. The reference didn't translate in CI, which contradicted my request ("translate and post with Actions"). That one question decided half of the design that followed.&lt;/p&gt;

&lt;p&gt;Over five rounds, the questions went past 30. Each time an answer was settled, Claude added the terms to &lt;code&gt;CONTEXT.md&lt;/code&gt;. That file pins down what words like "original article," "English version," "publish," and "crosspost" mean in this repository. You could call it a ubiquitous language, in domain-driven design terms. For decisions that would be hard to reverse later, it also recorded two ADRs (Architecture Decision Records: short documents capturing a decision and its reasoning).&lt;/p&gt;

&lt;p&gt;As an aside, I'm writing a Zenn book about ADRs. I'll publish it once it's done.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;code&gt;grill-with-docs&lt;/code&gt; attaches a recommended answer to each question. You can keep going by just answering "sounds good" to the recommendation, so it isn't as tiring as the question count suggests. Conversely, if you don't want to accept recommendations blindly, make sure to read the reasoning attached to each one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Translation stays out of CI: I review locally, then commit
&lt;/h2&gt;

&lt;p&gt;My original, rough request was "translate and post with Actions," but in the end I took translation out of CI. The deciding factor was choosing to publish immediately on dev.to. If CI calls an LLM API to translate, English text that no human has ever read goes straight to publication.&lt;/p&gt;

&lt;p&gt;So I split the roles like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[Write the original article] --&amp;gt; B[/translate into English/]
  B --&amp;gt; C[Read the English version and commit]
  C --&amp;gt; D[Push to main]
  D --&amp;gt; E[Actions crossposts to dev.to]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Translation happens through the Claude Code skill &lt;code&gt;/translate &amp;lt;slug&amp;gt;&lt;/code&gt;. The skill writes the translated body, and a script called &lt;code&gt;translate-apply.ts&lt;/code&gt; converts Zenn syntax, adds frontmatter, and writes the result to &lt;code&gt;articles_en/&amp;lt;slug&amp;gt;.md&lt;/code&gt;. The skill doesn't commit. I'm the one who reads the English version and commits it, and that's the only review checkpoint.&lt;/p&gt;

&lt;p&gt;This way, I can safely use Claude Code on my subscription without setting up any translation API key. And there's no extra usage cost. The tradeoff is that if I forget to translate, the article just stays un-crossposted. I decided to catch that with the "on hold" warning described later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't store dev.to article IDs; look them up by canonical_url
&lt;/h2&gt;

&lt;p&gt;The reference setup stores the mapping between slugs and dev.to article IDs in &lt;code&gt;.devto-mapping.json&lt;/code&gt;, and Actions commits that file back. From the second run onward, it uses that ID to update (PUT).&lt;/p&gt;

&lt;p&gt;This approach caused me two problems. First, the Actions commit collides with my local &lt;code&gt;main&lt;/code&gt;. Unless I pull after every push, my next push gets rejected. Second, what if the post (POST) to dev.to succeeds but committing the mapping fails? On the next run, the ID isn't found, so the same article gets posted again.&lt;/p&gt;

&lt;p&gt;So in my setup, I chose not to keep a mapping at all. On every crosspost, it fetches my article list from the dev.to API and looks for the one whose &lt;code&gt;canonical_url&lt;/code&gt; matches the URL of the original Zenn article. &lt;code&gt;canonical_url&lt;/code&gt; is the URL that tells search engines "the original of this article is over here." Since every crossposted article always has one set, it works as a matching key.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
  participant GA as GitHub Actions
  participant API as dev.to API
  GA-&amp;gt;&amp;gt;API: GET /api/articles/me/all
  API--&amp;gt;&amp;gt;GA: Article list (including unpublished)
  GA-&amp;gt;&amp;gt;GA: Match against originals by canonical_url
  GA-&amp;gt;&amp;gt;API: POST or PUT (only articles with changes)&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Whether this works depends on how the list API on the &lt;code&gt;dev.to&lt;/code&gt; side behaves. If unpublished articles weren't included in the list, an article that had been unpublished once would be judged "not on dev.to" when republished, and get posted twice. The documentation alone wasn't enough, so I had Claude read the source of &lt;a href="https://github.com/forem/forem" rel="noopener noreferrer"&gt;Forem&lt;/a&gt;, which runs dev.to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/api/articles/me/all&lt;/code&gt; also returns unpublished articles (the docs say "Unpublished articles will be at the top of the list")&lt;/li&gt;
&lt;li&gt;The response includes &lt;code&gt;published&lt;/code&gt;, &lt;code&gt;body_markdown&lt;/code&gt;, &lt;code&gt;tag_list&lt;/code&gt; (an array), and &lt;code&gt;canonical_url&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Source: &lt;code&gt;app/views/api/v1/articles/me.json.jbuilder&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;body_markdown&lt;/code&gt; is stored exactly as sent; no frontmatter gets added&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That told me the diff check could also be done with a single list fetch. The costs are one list fetch per crosspost, and that if someone manually edits the canonical_url in the dev.to UI, matching breaks. The latter is written down in an ADR as an operational rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Only the original Japanese article holds the publication state
&lt;/h2&gt;

&lt;p&gt;In the reference setup, the English version's frontmatter also has &lt;code&gt;published&lt;/code&gt;, and CI verifies that it matches the Japanese version. If they don't match, publishing stops.&lt;/p&gt;

&lt;p&gt;In the option I chose, the English version doesn't have &lt;code&gt;published&lt;/code&gt; at all. Actions always reads the original article's &lt;code&gt;published&lt;/code&gt; and makes dev.to match it. If there's only one source to begin with, there's nothing to check for mismatches. Setting the original back to &lt;code&gt;published: false&lt;/code&gt; is all it takes to unpublish on dev.to as well.&lt;/p&gt;

&lt;p&gt;As a result, the English version's frontmatter has just three fields:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;articles_en/.md&lt;/strong&gt;&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Why&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Harness":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;note'&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ai&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mcp&lt;/span&gt;
&lt;span class="na"&gt;source_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;edf3ae74a6a6...&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens to each article is decided as shown below. Only "slugs that have an English version" are processed. The 18 existing articles have no English version, so they naturally fall out of scope without any exclusion list.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  A[Has English version] --&amp;gt; B{Original is published}
  B -- Yes --&amp;gt; C{Exists on dev.to}
  B -- Unpublished or deleted --&amp;gt; D{Published on dev.to}
  C -- No --&amp;gt; E[POST and publish immediately]
  C -- Yes --&amp;gt; F{Content changed}
  F -- Yes --&amp;gt; G[Update with PUT]
  F -- No --&amp;gt; H[Do nothing]
  D -- Yes --&amp;gt; I[Unpublish with PUT]
  D -- No --&amp;gt; H&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The diff check compares four things: title, body, tags, and publication state. The reference setup PUT every published article on every push, but that needlessly changes the updated timestamp on dev.to as well. It also pushes you closer to &lt;code&gt;dev.to&lt;/code&gt;'s rate limits (by default, 9 article creations and 30 updates per 30 seconds).&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch stale translations with a hash of the original
&lt;/h2&gt;

&lt;p&gt;Sometimes I fix the original after publishing and forget to regenerate the English version. To catch this, I record the original article's hash at the time the English version was made, as &lt;code&gt;source_hash&lt;/code&gt; in the English version. At crosspost time, it computes the current original's hash and warns if they don't match.&lt;/p&gt;

&lt;p&gt;So what should that hash be computed from? The first thing that comes to mind is a hash of the whole original file. But then just flipping &lt;code&gt;published: false&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; changes the hash. If you prepare the translation ahead of time and then publish, you'd get a "translation is stale" warning every single time.&lt;/p&gt;

&lt;p&gt;So I narrowed the hash input down to the title and body:&lt;/p&gt;

&lt;p&gt;$$&lt;br&gt;
h = \mathrm{SHA256}\left(\mathrm{JSON}({\,\mathit{title},\ \mathit{body}\,})\right)&lt;br&gt;
$$&lt;/p&gt;

&lt;p&gt;Changing &lt;code&gt;published&lt;/code&gt;, &lt;code&gt;topics&lt;/code&gt;, or &lt;code&gt;emoji&lt;/code&gt; leaves $h$ unchanged; it only changes when the body or title changes. The tests state exactly this property:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;scripts/lib/hash.test.ts&lt;/strong&gt;&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;published を切り替えてもハッシュは変わらない&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;published&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;published: false&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="s2"&gt;published: true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;computeSourceHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;computeSourceHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;original&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;A stale English version is still crossposted as-is, with a warning. If crossposting stopped instead, fixing even a single typo would leave dev.to un-updated until the translation was regenerated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin down Zenn syntax conversion with tests
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dev.to&lt;/code&gt; doesn't understand &lt;code&gt;Zenn&lt;/code&gt;'s own syntax, so conversion is needed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Zenn&lt;/th&gt;
&lt;th&gt;On dev.to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:::details タイトル&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{% details タイトル %}&lt;/code&gt; … &lt;code&gt;{% enddetails %}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;:::message&lt;/code&gt; (&lt;code&gt;alert&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;A blockquote prefixed with ℹ️ (⚠️)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fence info &lt;code&gt;ts:src/index.ts&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A bold &lt;code&gt;**src/index.ts**&lt;/code&gt; right before the code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@[card](URL)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A plain link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@[youtube](ID)&lt;/code&gt; etc.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{% embed URL %}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;![alt](/images/...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A note saying "see the original Zenn article for the image"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Images are replaced with a note because the repository where I manage my Zenn articles is private, so dev.to can't fetch images from it. Making the repository public was an option, but it would expose unpublished drafts too, so I passed on it.&lt;/p&gt;

&lt;p&gt;The conversion isn't left to Claude Code; it's a regex-based script. With an LLM, there's no guarantee the same input produces the same output every time. A deterministic program or script lets you pin its behavior down with tests.&lt;/p&gt;

&lt;p&gt;The trickiest part of the conversion was the constraint that &lt;strong&gt;nothing inside code blocks gets converted&lt;/strong&gt;. Sometimes, like in this very post, I write code examples that explain Zenn syntax itself:&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="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;md
&lt;/span&gt;:::details これは記法の説明なので、変換してはいけない
:::
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I went with a somewhat fiddly mechanism: before conversion, each code block is stashed away and replaced with a one-line placeholder (a number wrapped in the Unicode Private Use Area characters &lt;code&gt;U+E000&lt;/code&gt; and &lt;code&gt;U+E001&lt;/code&gt;), and restored after conversion finishes.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Stash-and-restore code (excerpt)
  &lt;p&gt;&lt;strong&gt;scripts/lib/convert.ts&lt;/strong&gt;&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertZennToDevto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ConvertOptions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stashCodeBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&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;converted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;convertLocalImages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;convertEmbeds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;convertDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;convertMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PLACEHOLDER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;quoteLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)],&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;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;This approach has a pitfall. Since &lt;code&gt;:::message&lt;/code&gt; is converted into a blockquote, the placeholder line inside it also starts with &lt;code&gt;&amp;gt;&lt;/code&gt;. But when the placeholder is restored, the second and subsequent lines of the code block don't have &lt;code&gt;&amp;gt;&lt;/code&gt;. The blockquote gets cut off mid-code. On restore, it looks at the leading &lt;code&gt;&amp;gt;&lt;/code&gt; and re-applies it to every line of the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you didn't decide shows up during implementation
&lt;/h2&gt;

&lt;p&gt;Writing tests first, gaps surfaced that even 30+ questions hadn't closed. No matter how good the architect on the team is, design can't define every implementation problem up front. Some things you only find out by actually implementing them, and that happens in the real world. It happens just as naturally in AI-driven development.&lt;/p&gt;

&lt;p&gt;The biggest one was how to handle "on hold." During the grilling phase, I'd decided that "a published article without an English version gets its crosspost put on hold, with a warning." But once I wrote the tests, all 18 existing articles matched that condition. As-is, every push would line up 18 warnings.&lt;/p&gt;

&lt;p&gt;Claude Code stopped implementing and came back with four options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Warn only about articles changed in that push&lt;/li&gt;
&lt;li&gt;Keep an exclusion list&lt;/li&gt;
&lt;li&gt;Don't warn&lt;/li&gt;
&lt;li&gt;Warn about everything every time (as agreed)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I chose 1. It takes the diff against the previous commit with &lt;code&gt;git diff&lt;/code&gt;, and warns only about changed originals that have no English version.&lt;/p&gt;

&lt;p&gt;After implementation, I also had a review agent look it over. Of its 10 findings, I fixed 7. The main four:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a force push removes the previous commit, &lt;code&gt;git diff&lt;/code&gt; fails and the whole job stops. Since the warning is a supporting feature, crossposting now continues even if it fails&lt;/li&gt;
&lt;li&gt;A broken YAML in just one original article stopped crossposting for every article. It's now treated as a per-article error&lt;/li&gt;
&lt;li&gt;If dev.to saves tags in a different order or changes the trailing newline, articles with no changes get updated every time. Those two are now excluded from the comparison&lt;/li&gt;
&lt;li&gt;Zenn lets you nest &lt;code&gt;:::message&lt;/code&gt; inside &lt;code&gt;::::details&lt;/code&gt;, but the outer one wasn't being converted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end there were 41 tests, with 99% line coverage and 93% branch coverage. Things with small impact, or that recover on a rerun, such as retrying on 5xx errors, were skipped, with the reasons written down.&lt;/p&gt;

&lt;h2&gt;
  
  
  On the first crosspost, the tags silently vanished
&lt;/h2&gt;

&lt;p&gt;I tried the first crosspost with "&lt;a href="https://zenn.dev/sho7650/articles/1f2b14df8975ea" rel="noopener noreferrer"&gt;"Is That a Harness Too?" Making Sense of Harness Engineering for AI Agents (Japanese original)&lt;/a&gt;." I committed the English version, merged the PR, and the push to &lt;code&gt;main&lt;/code&gt; triggered Actions. The article is published as "&lt;a href="https://dev.to/sho7650/is-that-a-harness-too-making-sense-of-harness-engineering-for-ai-agents-p15"&gt;"Is That a Harness Too?" Making Sense of Harness Engineering for AI Agents&lt;/a&gt;." The canonical_url pointed to the original Zenn article, and no Zenn syntax was left in the body.&lt;/p&gt;

&lt;p&gt;But when I actually ran it, there wasn't a single tag on it. The English version's frontmatter listed four: &lt;code&gt;ai&lt;/code&gt;, &lt;code&gt;agents&lt;/code&gt;, &lt;code&gt;llm&lt;/code&gt;, and &lt;code&gt;architecture&lt;/code&gt;. The API didn't return an error, so there was nothing in the Actions logs to tip me off.&lt;/p&gt;

&lt;p&gt;The cause was how the tags were sent. The dev.to API docs say &lt;code&gt;tags&lt;/code&gt; is "a comma-separated string." The first implementation followed that and sent &lt;code&gt;"ai,agents,llm,architecture"&lt;/code&gt;. But reading Forem's controller, it only accepts an array:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;app/controllers/concerns/api/articles_controller.rb&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;allowed_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:body_markdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;:main_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:canonical_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;tags: &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a key is declared as &lt;code&gt;{ tags: [] }&lt;/code&gt;, Rails strong parameters silently drop a string value instead of raising an error. I'd had Claude read the source to verify the list API, yet blindly trusted the docs for the API that sends articles. Honestly, though, this one's on the docs. Leaning on Claude Code this time is what led me to read the source code at all; a human developer would normally have no choice but to trust the docs unless there were prior reports. What a pain.&lt;/p&gt;

&lt;p&gt;This bug had one more annoying side effect. Since the tags on dev.to stayed empty, the diff check saw "tags differ" every time. Even with nothing changed, a PUT would keep firing on every push. I fixed it to send tags as an array and changed the test expectations to arrays too. Thanks to that, the tags are now assigned properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking back
&lt;/h2&gt;

&lt;p&gt;In one sentence, the setup &lt;strong&gt;keeps translation in human hands locally, and leaves CI with just one job: mirroring the original article's publication state to dev.to&lt;/strong&gt;. Since CI writes nothing back, there's no mapping file, no bot commits, and no collisions with my local &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Starting from the reference setup was the right call. Having that foundation is exactly what let me ask, for each decision like "commit a mapping file" or "keep &lt;code&gt;published&lt;/code&gt; in two places," whether it fit how I work. And the grilling skill is what laid out those questions without missing any.&lt;/p&gt;

&lt;p&gt;If you're thinking about running Zenn and dev.to side by side the same way, I'd suggest deciding two things first: "where does translation happen?" and "where does the dev.to-side state live?" The rest of the decisions should get much easier after that.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>githubactions</category>
      <category>typescript</category>
    </item>
    <item>
      <title>"Is That a Harness Too?" Making Sense of Harness Engineering for AI Agents</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Fri, 09 Oct 2026 05:16:52 +0000</pubDate>
      <link>https://dev.to/sho7650/is-that-a-harness-too-making-sense-of-harness-engineering-for-ai-agents-p15</link>
      <guid>https://dev.to/sho7650/is-that-a-harness-too-making-sense-of-harness-engineering-for-ai-agents-p15</guid>
      <description>&lt;h2&gt;
  
  
  When sales says "everything is a harness," they're not wrong. It's just too broad
&lt;/h2&gt;

&lt;p&gt;Lately I keep hearing the same lines from sales folks around me: "Oh, that's harness engineering," or "Are you really thinking about harness engineering?" What exactly do they mean by it? For some people, building an MCP server is harness engineering. So is wiring up RAG. So is adding guardrails. Everything is harness engineering.&lt;/p&gt;

&lt;p&gt;Let's get on the same page first.&lt;/p&gt;

&lt;p&gt;By harness engineering, I don't mean test harnesses, and I don't mean &lt;a href="https://harness.io/" rel="noopener noreferrer"&gt;Harness.io&lt;/a&gt;, the CI/CD product. I mean Harness Engineering in the sense of what it takes to turn an AI agent into a system that actually works in practice.&lt;/p&gt;

&lt;p&gt;With that settled: in the broad sense, the sales instinct isn't wrong. But taken at face value, it's far too broad to be useful as an IT architecture term. It ends up meaning roughly "we're doing something around AI agents."&lt;/p&gt;

&lt;p&gt;So I tried defining it again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Harness Engineering is the design of the execution environment and feedback loops around a probabilistic AI model, so that it becomes a "work-executing agent" that is "&lt;strong&gt;controllable&lt;/strong&gt;," "&lt;strong&gt;verifiable&lt;/strong&gt;," and "&lt;strong&gt;observable&lt;/strong&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't a technique for making AI smarter. It's a technique for taking an AI that is admittedly smart but unreliable, and containing it so it can do real work. Below is how I arrived at that one sentence, plus a quick test for when sales tells you "that's a harness."&lt;/p&gt;

&lt;h2&gt;
  
  
  A harness is the whole loop outside the model
&lt;/h2&gt;

&lt;p&gt;As of 2026, there's a formula you see everywhere:&lt;/p&gt;

&lt;p&gt;$$&lt;br&gt;
Agent = Model + Harness&lt;br&gt;
$$&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.langchain.com/blog/the-anatomy-of-an-agent-harness" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt; uses almost exactly this phrasing. &lt;a href="https://learn.microsoft.com/en-us/agent-framework/concepts/harness" rel="noopener noreferrer"&gt;Microsoft&lt;/a&gt; defines an Agent Harness as "the execution foundation that makes a language model function as an AI agent that can actually get work done." It's made up of the pieces responsible for conversation state, context, tool calls, approval policies, multi-step progress, and so on.&lt;/p&gt;

&lt;p&gt;Roughly sketched, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    E[User / Event] --&amp;gt; C
    M([Model])
    subgraph Harness
        C[Context]
        A[Action / Tool]
        R[Result / Observation]
        V{Validate / Evaluate}
        RT[Retry]
        F[Finish]
        ES[Escalate]
    end
    C --&amp;gt; M
    M --&amp;gt; A
    A --&amp;gt; R
    R --&amp;gt; V
    V --&amp;gt;|Failed| RT
    V --&amp;gt;|Done| F
    V --&amp;gt;|Risky| ES
    RT --&amp;gt; C&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This entire outer loop is the harness.&lt;/p&gt;

&lt;p&gt;The model's job is to figure out "what's probably the right next move." The harness's job is to decide what to show it, what to let it do, what to forbid, how to check the results, what to do on failure, and when to stop. The automation I've been building lately by &lt;a href="https://note.com/sho7650/n/n9f21daa9653d" rel="noopener noreferrer"&gt;combining&lt;/a&gt; &lt;a href="https://typesafe.ai/" rel="noopener noreferrer"&gt;Jev&lt;/a&gt; with n8n is, I'd argue, exactly what you'd call harness engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core is the control loop, not the tool connections
&lt;/h2&gt;

&lt;p&gt;Get this wrong and Harness Engineering turns into "every AI-adjacent technology, bundled."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/discover/agent-harness" rel="noopener noreferrer"&gt;Google Cloud&lt;/a&gt; describes building a harness as "wrapping a language model in scripts that manage its inputs, outputs, and tool execution." Tools are executed via MCP and the like, the results are appended to the conversation history, and everything is fed back to the model. For enterprise use, it's positioned as the layer that manages state, permissions, and tool execution, and because it owns all input and output, it also makes monitoring and cost tracking easier.&lt;/p&gt;

&lt;p&gt;So if you think about it in stages, the boundary comes into view. This alone is just "API integration":&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    LLM --&amp;gt; API&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Once you get here, you're gradually entering harness territory:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    LLM --&amp;gt; EX[Call API]
    EX --&amp;gt; CK[Check result]
    CK --&amp;gt; Q{Goal achieved?}
    Q --&amp;gt;|No| NX[Decide next action]
    Q --&amp;gt;|Yes| FIN[Finish]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;And if you've designed all the way to the third stage below, I think you can pretty clearly call it Harness Engineering:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    P[Check permissions] --&amp;gt; E[Execute]
    E --&amp;gt; V[Verify result]
    V --&amp;gt; FX[Fix if failed]
    FX --&amp;gt; ES[Escalate to a human if risky]
    ES --&amp;gt; LG[Store logs and eval data]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;It doesn't end once things are connected. You look at what came back, decide the next action, and take care of retries and cutoffs. That "taking care of it" part is the core.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP, RAG, and guardrails are all just parts of a harness
&lt;/h2&gt;

&lt;p&gt;In practice, it helps to break down what a harness decides like this:&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;What the harness decides&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instructions&lt;/td&gt;
&lt;td&gt;What goal the agent acts toward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;What to show the model this time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;What to pull from internal docs and DBs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;What it can execute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity / Permission&lt;/td&gt;
&lt;td&gt;Whose permissions it acts with, and how far&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration&lt;/td&gt;
&lt;td&gt;In what order and loops it runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State / Memory&lt;/td&gt;
&lt;td&gt;How state from previous runs is kept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrails&lt;/td&gt;
&lt;td&gt;What is forbidden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;How to check the result is correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry / Recovery&lt;/td&gt;
&lt;td&gt;How to roll back on failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human approval&lt;/td&gt;
&lt;td&gt;Where a human steps in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;What gets kept as logs and traces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation&lt;/td&gt;
&lt;td&gt;Whether the agent behaves as expected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Termination&lt;/td&gt;
&lt;td&gt;When the job counts as done&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/agent-framework/concepts/harness" rel="noopener noreferrer"&gt;Microsoft's current Agent Harness&lt;/a&gt; likewise groups planning, file memory, tool approval, OpenTelemetry, web search, skills, background agents, shell execution, looping, and more as harness capabilities.&lt;/p&gt;

&lt;p&gt;Looking at this table, it's clear that MCP is not the harness itself. MCP is one mechanism for connecting the tools a harness uses. RAG is the same: one part of Context / Retrieval. Guardrails are one part. Memory is one part.&lt;/p&gt;

&lt;p&gt;Whether you're talking with sales or with customers, keeping these separate makes life easier as an architect. "We connected MCP" and "we designed a harness" simply don't belong in the same sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt Engineering and Context Engineering both live inside the harness
&lt;/h2&gt;

&lt;p&gt;The relationship among the three is easiest to understand as nesting:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    H[Harness Engineering] --&amp;gt; P[Prompt / Instruction Engineering]
    H --&amp;gt; C[Context Engineering]
    C --&amp;gt; RAG
    C --&amp;gt; MEM[Memory]
    C --&amp;gt; PD[Progressive Disclosure]
    H --&amp;gt; T[Tool Engineering]
    T --&amp;gt; API
    T --&amp;gt; MCP
    H --&amp;gt; O[Orchestration]
    H --&amp;gt; G[Guardrails / Permissions]
    H --&amp;gt; EV[Evaluation / Verification]
    H --&amp;gt; OB[Observability / Recovery]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://martinfowler.com/articles/harness-engineering.html" rel="noopener noreferrer"&gt;Birgitta Böckeler's breakdown&lt;/a&gt;, published on everyone's favorite Martin Fowler site, splits Harness Engineering into Guides and Sensors. Her framing is scoped specifically to working with coding agents.&lt;/p&gt;

&lt;p&gt;A Guide is feedforward: it steers the agent in the right direction before it fails. Rules, documentation, system prompts, Skills, and tool specs all fall here.&lt;/p&gt;

&lt;p&gt;A Sensor is feedback: it observes what happened and flags mistakes. Tests, linters, logs, metrics, reviews by another AI, and so on.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    G["Guide (feedforward)"] --&amp;gt; AG[Agent]
    GO[Goal] --&amp;gt; AG
    AG --&amp;gt; AC[Action]
    AC --&amp;gt; S["Sensor (feedback)"]
    S --&amp;gt; AG&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Giving instructions is only half a harness. It becomes a harness only once you've built the part where the agent acts, you observe, and it corrects itself.&lt;/p&gt;

&lt;p&gt;Polishing prompts strengthens the Guide. If that's all you do, nobody will notice when the agent gets something wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why harnesses, why now? Because LLMs got smarter
&lt;/h2&gt;

&lt;p&gt;It's a bit paradoxical, but the reason is that models got smarter.&lt;/p&gt;

&lt;p&gt;Back when models were weak, it looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    HU[Human] --&amp;gt; AI
    AI --&amp;gt; AN[Answer]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;A human asks, the AI answers, and if it's wrong, the human catches it. That was enough.&lt;/p&gt;

&lt;p&gt;Once models are strong enough, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    AI --&amp;gt; S[Investigate]
    S --&amp;gt; J[Decide]
    J --&amp;gt; E[Execute]
    E --&amp;gt; C[Check result]
    C --&amp;gt; N[Next decision]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Now that they can act this autonomously, the question has shifted from "can the AI answer?" to "how much can we hand over to the AI?"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://openai.com/index/harness-engineering/" rel="noopener noreferrer"&gt;The Harness Engineering post OpenAI published in February 2026&lt;/a&gt; is about exactly this. When they had Codex write code, early progress was slow not because Codex lacked the ability, but because the environment wasn't described well enough. In the end, they built testing, verification, review, feedback handling, and failure recovery into the system side as part of the development loop.&lt;/p&gt;

&lt;p&gt;In other words, the problem moved from model capability to system engineering. That, I think, is why Harness Engineering is suddenly getting so much attention. And it's good news for architects: we can't touch the model's internals, but system engineering is our home turf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sales support, refunds, and incident response: where the boundary shows up
&lt;/h2&gt;

&lt;p&gt;Let's walk through some concrete examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sales support agent
&lt;/h3&gt;

&lt;p&gt;"Prep me for the next meeting with Company X."&lt;/p&gt;

&lt;p&gt;On its own, that's just using an LLM. Build a harness, and you get a flow like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Fetch Account from CRM] --&amp;gt; B[Fetch Contacts / Opportunities]
    B --&amp;gt; C[Fetch past meetings]
    C --&amp;gt; D[Check internal access rights]
    D --&amp;gt; E[Search external sources if needed]
    E --&amp;gt; F[Draft meeting hypotheses]
    F --&amp;gt; G[Cross-check against sources]
    G --&amp;gt; H[Draft CRM updates]
    H --&amp;gt; I[Human approves]
    I --&amp;gt; J[Update CRM]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The CRM itself is not the harness. The harness is the mechanism that controls when the CRM is accessed, under whose permissions, what gets fetched, and how much the agent is allowed to update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customer support agent
&lt;/h3&gt;

&lt;p&gt;Say a customer writes, "I was double-charged, refund me."&lt;/p&gt;

&lt;p&gt;A bare model would probably fire back "We're so sorry. We'll refund you" right away. Anyone can say that. An agent that can actually issue a refund looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Verify identity] --&amp;gt; B[Fetch order]
    B --&amp;gt; C[Check payment history]
    C --&amp;gt; D[Fetch refund policy]
    D --&amp;gt; E[Calculate refundable amount]
    E --&amp;gt; F[Threshold check]
    F --&amp;gt; G[Human approval if needed]
    G --&amp;gt; H[Call payment API]
    H --&amp;gt; I[Re-fetch to confirm the refund actually happened]
    I --&amp;gt; J[Update CRM]
    J --&amp;gt; K[Reply to customer]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;That step near the end, "re-fetch to confirm the refund actually happened," matters enormously. "We called the refund API" and "the refund went through" are two different things.&lt;/p&gt;

&lt;p&gt;In Harness Engineering, verification matters more than action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Incident response agent
&lt;/h3&gt;

&lt;p&gt;For "The website is slow. Find out why," the following flow is relatively safe:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Fetch metrics] --&amp;gt; B[Fetch logs]
    B --&amp;gt; C[Inspect traces]
    C --&amp;gt; D[Form hypotheses]
    D --&amp;gt; E[Investigate further]
    E --&amp;gt; F[Identify likely causes]
    F --&amp;gt; G[Draft a fix]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Because it only reads and thinks.&lt;/p&gt;

&lt;p&gt;But once you allow actions that affect running processes, the story changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The harness needs to check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is this a production change?&lt;/li&gt;
&lt;li&gt;What's the blast radius?&lt;/li&gt;
&lt;li&gt;Can it be rolled back?&lt;/li&gt;
&lt;li&gt;Does it need approval?&lt;/li&gt;
&lt;li&gt;Did things return to normal afterward?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only when you can answer all five does it hold up as an enterprise system.&lt;/p&gt;

&lt;p&gt;What all three examples share is that between the agent's "read" phase and its "change" phase, there's a layer of permissions, verification, and human approval. That layer in between is the harness itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  A harness is never "done"
&lt;/h2&gt;

&lt;p&gt;Here are the pros and cons of a harness side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Improves performance independently of model changes&lt;/td&gt;
&lt;td&gt;The system gets considerably more complex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lets you raise agent autonomy safely&lt;/td&gt;
&lt;td&gt;More to operate: traces, evals, permissions, state, and more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Improves reproducibility, auditability, explainability&lt;/td&gt;
&lt;td&gt;Model improvements can turn an old harness into a hindrance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reduces human review work&lt;/td&gt;
&lt;td&gt;Excessive rules can kill the model's capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feeds failures back into the next Guide / Sensor&lt;/td&gt;
&lt;td&gt;The harness itself needs testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third con is especially easy to miss.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.anthropic.com/engineering/managed-agents" rel="noopener noreferrer"&gt;Anthropic pointed out in April 2026&lt;/a&gt; that a harness encodes assumptions about "what Claude can't do on its own," so as models improve, those assumptions need to be revisited.&lt;/p&gt;

&lt;p&gt;They also gave a real example. They had added context resets to deal with Sonnet 4.5's habit of sensing the context limit and wrapping up work early. But with Opus 4.5, that habit disappeared, and the resets became dead weight.&lt;/p&gt;

&lt;p&gt;That's why a harness is never finished once built. You need to trim it down or rebuild it alongside model updates. What you're maintaining is "the parts built on the assumption that the AI will get things wrong."&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "everything is a harness" is that the word stops being useful
&lt;/h2&gt;

&lt;p&gt;In conversations with sales, this is probably the most important part.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://martinfowler.com/articles/harness-engineering.html" rel="noopener noreferrer"&gt;Böckeler's article&lt;/a&gt; itself says that "Agent = Model + Harness is a very broad definition, so it's worth narrowing it down by type of agent." She then explicitly limits her own definition to the bounded context of working with coding agents. I think that's the right approach.&lt;/p&gt;

&lt;p&gt;For example, calling "I built an MCP server" Harness Engineering isn't wrong.&lt;br&gt;
But the moment you label every item below as "Harness Engineering," the term becomes nearly synonymous with "we're doing something around AI agents." A word that doesn't tell you what it refers to is useless in a design conversation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;REST API development&lt;/li&gt;
&lt;li&gt;IAM&lt;/li&gt;
&lt;li&gt;RAG&lt;/li&gt;
&lt;li&gt;Prompt&lt;/li&gt;
&lt;li&gt;MCP&lt;/li&gt;
&lt;li&gt;Workflow&lt;/li&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;li&gt;Evaluation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So how about distinguishing them like this?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you're doing&lt;/th&gt;
&lt;th&gt;What to call it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Improving instructions&lt;/td&gt;
&lt;td&gt;Prompt / Instruction Engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selecting information, RAG, Memory&lt;/td&gt;
&lt;td&gt;Context Engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP / API connections&lt;/td&gt;
&lt;td&gt;Tool Integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Procedures, routing&lt;/td&gt;
&lt;td&gt;Agent Orchestration / Workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions, prohibitions&lt;/td&gt;
&lt;td&gt;Guardrails / Governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation&lt;/td&gt;
&lt;td&gt;Agent Evaluation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traces, metrics&lt;/td&gt;
&lt;td&gt;Agent Observability / AgentOps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combining all of these to control autonomous execution&lt;/td&gt;
&lt;td&gt;Harness Engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Harness Engineering isn't so much the name of an individual technology as a higher-level concept: designing the control system that integrates all of these to make an agent's behavior work. Framed that way, everything falls neatly into place.&lt;/p&gt;

&lt;p&gt;It's fine for sales to tell a customer "you need Harness Engineering." The architect's job is to break that one line down into the rows of the table above and show what's covered and what's missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eight questions to answer "Is that a harness?"
&lt;/h2&gt;

&lt;p&gt;When sales says "this is Harness Engineering," checking these in order seems to sort things out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;What does the agent have to achieve to be "done"?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;What can the agent see?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;What can the agent execute?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;What must it not execute?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;How are execution results verified?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;How does it retry / recover when it gets things wrong?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Where does it escalate to a human?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Can its decisions and actions be traced afterward?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If all eight are designed, then "I see, that is Harness Engineering" is a fair verdict.&lt;/p&gt;

&lt;p&gt;If, on the other hand, it's just "we connected the CRM via MCP," that's Tool Integration. It's one of the parts that make up a harness, but it isn't Harness Engineering as a whole. That's how I'd sort it.&lt;/p&gt;

&lt;p&gt;The order of these eight questions matters, too. For an agent with no defined "done" condition (question 1), there's no way to design "verification" (question 5) or "traceability" (question 8). That's why you always start with question 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harness Engineering is really just good old system design
&lt;/h2&gt;

&lt;p&gt;Harness Engineering is a new name, but from an architect's point of view, it's not all that foreign. Isn't it what we've been doing all along?&lt;/p&gt;

&lt;p&gt;Let's list what we've always done:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define boundaries&lt;/li&gt;
&lt;li&gt;Define permissions&lt;/li&gt;
&lt;li&gt;Manage state&lt;/li&gt;
&lt;li&gt;Detect failures&lt;/li&gt;
&lt;li&gt;Retry&lt;/li&gt;
&lt;li&gt;Roll back&lt;/li&gt;
&lt;li&gt;Monitor&lt;/li&gt;
&lt;li&gt;Test&lt;/li&gt;
&lt;li&gt;Escalate to humans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's system design, plain and simple. Designing batch reruns, compensating transactions, alerting for operations monitoring: all of it fits on this list.&lt;/p&gt;

&lt;p&gt;There is, however, one big difference.&lt;/p&gt;

&lt;p&gt;Traditionally, humans defined a program's behavior as code. In an agentic system, part of that behavior is delegated to a non-deterministic model. That's why you need a strong control system around it. That's the harness.&lt;/p&gt;

&lt;p&gt;I've been writing programs for over 40 years, and I believe that turning routine work into programs is what computers are fundamentally for. That belief hasn't changed in the age of AI. If anything, it's become clearer.&lt;/p&gt;

&lt;p&gt;Whatever can be made routine, you lock down with deterministic, definitive programs, just as before. Only the judgments that can't be made routine get handed to a probabilistic model. And the results of those delegated judgments get verified, again, by deterministic programs. A harness is the design of that "deterministic part." Let computers do what they're good at, and don't let uncertain things run loose as they are. Drawing that line has always been the architect's job.&lt;/p&gt;

&lt;p&gt;Seen this way, the difference among the three terms fits in a single line.&lt;/p&gt;

&lt;p&gt;If Prompt Engineering is "how you instruct the AI" and Context Engineering is "what you show the AI," then Harness Engineering is the craft of designing "what kind of world you put the AI to work in."&lt;/p&gt;

&lt;p&gt;With this definition, I think we can finally push back on the "everything is a harness" problem.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>architecture</category>
    </item>
    <item>
      <title>[Tailscale] Complete Guide to VPN Connection to Home LAN with Subnet Router</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Fri, 09 Oct 2026 02:23:39 +0000</pubDate>
      <link>https://dev.to/sho7650/tailscale-complete-guide-to-vpn-connection-to-home-lan-with-subnet-router-3g67</link>
      <guid>https://dev.to/sho7650/tailscale-complete-guide-to-vpn-connection-to-home-lan-with-subnet-router-3g67</guid>
      <description>&lt;h1&gt;
  
  
  Accessing Your Home LAN from Outside with Tailscale Subnet Router
&lt;/h1&gt;

&lt;p&gt;I want to access my home NAS or server while away from home. But VPN setup is difficult... I do not have a VPN router either... Tailscale solves these problems.&lt;/p&gt;

&lt;p&gt;In this article, I will explain how to build an environment using Docker that allows you to access your entire home LAN from outside using Tailscale Subnet Router functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What We Want to Achieve
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;VPN connection to home LAN (192.168.x.x) from smartphone/PC while away&lt;/li&gt;
&lt;li&gt;Name resolution for internal domains (*.example.local) using home DNS server&lt;/li&gt;
&lt;li&gt;Privacy protection so family members can use it but cannot see each other devices&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Users
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User&lt;/th&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;th&gt;Remarks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Admin&lt;/td&gt;
&lt;td&gt;Remote management, all LAN access&lt;/td&gt;
&lt;td&gt;Full admin privileges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Family&lt;/td&gt;
&lt;td&gt;Simple LAN access (NAS, etc.)&lt;/td&gt;
&lt;td&gt;Limited privileges, privacy protection between terminals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Functional Requirements
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Access to home LAN from outside&lt;/td&gt;
&lt;td&gt;Reachable to all devices in 192.168.x.0/24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inter-user communication blocking&lt;/td&gt;
&lt;td&gt;For privacy protection, family devices cannot communicate with each other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split DNS&lt;/td&gt;
&lt;td&gt;Only specific domains resolved by home DNS when connected to Tailscale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subnet Router only&lt;/td&gt;
&lt;td&gt;Exit Node (all traffic forwarding) not required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Technical Decisions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container runtime&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Easy management, reproducibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network mode&lt;/td&gt;
&lt;td&gt;host&lt;/td&gt;
&lt;td&gt;Required for Subnet Router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State persistence&lt;/td&gt;
&lt;td&gt;Named Volume&lt;/td&gt;
&lt;td&gt;Re-authentication unnecessary on restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health check&lt;/td&gt;
&lt;td&gt;Built-in endpoint&lt;/td&gt;
&lt;td&gt;Easy monitoring integration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Architecture Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overall Configuration Diagram
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-arch.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-arch.png" alt="Rough configuration overview" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Communication Flow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-sequence.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-sequence.png" alt="Not quite accurate but design was based on this diagram" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Component Roles
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Remarks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tailscale Container&lt;/td&gt;
&lt;td&gt;Subnet Router, forwards traffic from Tailnet to LAN&lt;/td&gt;
&lt;td&gt;Uses SNAT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS Server (Unbound)&lt;/td&gt;
&lt;td&gt;Internal domain name resolution&lt;/td&gt;
&lt;td&gt;Split DNS target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UFW/iptables&lt;/td&gt;
&lt;td&gt;Firewall, controls forwarding&lt;/td&gt;
&lt;td&gt;Important settings required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Importance of SNAT (Source Network Address Translation)
&lt;/h3&gt;

&lt;p&gt;Tailscale enables SNAT by default (--snat-subnet-routes=true). This is equivalent to MASQUERADE (IP masquerade / NAPT) in Linux iptables.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SNAT enabled (default)&lt;/td&gt;
&lt;td&gt;Source IP becomes Subnet Router IP&lt;/td&gt;
&lt;td&gt;No additional settings needed on LAN devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SNAT disabled&lt;/td&gt;
&lt;td&gt;Tailscale IP (100.x.x.x) is sent as-is&lt;/td&gt;
&lt;td&gt;Static routes must be set on LAN routers and devices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;SNAT enabled is recommended. No additional settings needed on LAN devices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Design Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Understanding Split DNS
&lt;/h3&gt;

&lt;p&gt;Split DNS is a mechanism to resolve only specific domains with specific DNS servers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-split.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-split.png" alt="If you have domains used only at home, DNS can be directed to specified locations" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normal internet access uses fast local DNS&lt;/li&gt;
&lt;li&gt;Only home internal domains resolved by home DNS&lt;/li&gt;
&lt;li&gt;No impact on internet speed even when connected to VPN&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2 Complex Relationship Between UFW and iptables
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;This is the biggest pitfall.&lt;/strong&gt; In environments using Docker, multiple firewall layers exist:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-iptables.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-iptables.png" alt="Where I got stuck the most" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings Requiring Attention&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Setting Location&lt;/th&gt;
&lt;th&gt;What to Set&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UFW INPUT&lt;/td&gt;
&lt;td&gt;ufw allow&lt;/td&gt;
&lt;td&gt;WireGuard port (41641/udp)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UFW FORWARD&lt;/td&gt;
&lt;td&gt;/etc/default/ufw&lt;/td&gt;
&lt;td&gt;DEFAULT_FORWARD_POLICY=ACCEPT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOCKER-USER&lt;/td&gt;
&lt;td&gt;/etc/ufw/after.rules&lt;/td&gt;
&lt;td&gt;RETURN rules for Tailscale CGNAT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3.3 DNS Server ACL Settings
&lt;/h3&gt;

&lt;p&gt;If using DNS servers like Unbound, you need to allow queries from Tailscale CGNAT (100.64.0.0/10).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With Split DNS, Tailscale clients send queries directly to the DNS server. At this time, the source IP becomes a CGNAT address (100.x.x.x).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.4 Auth Key Management
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; If state is persisted with Docker Named Volume, re-authentication is not required on container restart. Auth Key is only needed in the following cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial deployment&lt;/li&gt;
&lt;li&gt;When Named Volume is deleted&lt;/li&gt;
&lt;li&gt;When device is deleted from Tailscale Admin Console&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Configuration Details
&lt;/h2&gt;

&lt;p&gt;From here, I will explain specific configuration methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Tailscale Admin Console Settings
&lt;/h3&gt;

&lt;h4&gt;
  
  
  4.1.1 Issuing Auth Key
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Access Tailscale Admin Console&lt;/li&gt;
&lt;li&gt;Settings → Keys → Generate auth key&lt;/li&gt;
&lt;li&gt;Issue with the following settings:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Description&lt;/td&gt;
&lt;td&gt;home-router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expiration&lt;/td&gt;
&lt;td&gt;90 days (or as needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusable&lt;/td&gt;
&lt;td&gt;Off (for security)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ephemeral&lt;/td&gt;
&lt;td&gt;Off (persistent device)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-approved&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags&lt;/td&gt;
&lt;td&gt;tag:subnet-router&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Save the issued key securely (format: tskey-auth-xxxxx).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  4.1.2 ACL Settings
&lt;/h4&gt;

&lt;p&gt;Set the following in Admin Console → Access Controls:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"groups"&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;"group:admin"&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;"your-admin-email@example.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"group:family"&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;"family-email@example.com"&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;"tagOwners"&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;"tag:subnet-router"&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;"group:admin"&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;"hosts"&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;"home_network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192.168.x.0/24"&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;"acls"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"src"&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;"group:admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"group:family"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dst"&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;"home_network:*"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"src"&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;"tag:subnet-router"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dst"&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="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="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;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-acls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.shoz.in%2F2025%2FJul%2Ftailscale-acls.png" alt="This is how the ACL section looks on screen" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;group:admin and group:family can access home LAN&lt;/li&gt;
&lt;li&gt;By not defining inter-terminal communication rules, default deny is applied and mutual communication is blocked&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  4.1.3 DNS Settings (After Deployment)
&lt;/h4&gt;

&lt;p&gt;Set the following in Admin Console → DNS:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add Nameserver&lt;/td&gt;
&lt;td&gt;Custom: 192.168.x.53 (home DNS server IP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restrict to domain&lt;/td&gt;
&lt;td&gt;example.local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Override local DNS&lt;/td&gt;
&lt;td&gt;Off&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4.2 Server-Side Preparation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  4.2.1 Enabling IP Forwarding
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create configuration file&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/sysctl.d/99-tailscale.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Apply immediately&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/sysctl.d/99-tailscale.conf

&lt;span class="c"&gt;# Verify&lt;/span&gt;
sysctl net.ipv4.ip_forward
&lt;span class="c"&gt;# Output: net.ipv4.ip_forward = 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.2.2 TUN Device Verification
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify TUN device exists&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /dev/net/tun

&lt;span class="c"&gt;# Create if it does not exist&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /dev/net
&lt;span class="nb"&gt;sudo mknod&lt;/span&gt; /dev/net/tun c 10 200
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;666 /dev/net/tun
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.3 Docker Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  4.3.1 Directory Creation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/tailscale
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/tailscale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.3.2 Creating docker-compose.yml
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /opt/tailscale/docker-compose.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
services:
  tailscale:
    image: tailscale/tailscale:v1.94.1
    container_name: tailscale
    hostname: home-router
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - NET_RAW
    network_mode: host
    volumes:
      - tailscale_data:/var/lib/tailscale
      - /dev/net/tun:/dev/net/tun
    environment:
      - TS_AUTHKEY=tskey-auth-xxxxxxx # Set Auth Key here
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_ROUTES=192.168.x.0/24
      - TS_USERSPACE=false
      - TS_ACCEPT_DNS=false
      - TS_EXTRA_ARGS=--port=41641
      - TS_ENABLE_HEALTH_CHECK=true
      - TS_ENABLE_METRICS=true
      - TS_LOCAL_ADDR_PORT=[::]:9002

volumes:
  tailscale_data:
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuration Item Descriptions:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;image&lt;/td&gt;
&lt;td&gt;tailscale/tailscale:v1.94.1&lt;/td&gt;
&lt;td&gt;Use specific version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;network_mode&lt;/td&gt;
&lt;td&gt;host&lt;/td&gt;
&lt;td&gt;Required for Subnet Router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cap_add&lt;/td&gt;
&lt;td&gt;NET_ADMIN, NET_RAW&lt;/td&gt;
&lt;td&gt;Network operation privileges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TS_ROUTES&lt;/td&gt;
&lt;td&gt;192.168.x.0/24&lt;/td&gt;
&lt;td&gt;Subnet to advertise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TS_USERSPACE&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Use kernel mode (recommended)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TS_ACCEPT_DNS&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Do not use Tailscale DNS on server side&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Important Settings:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment Variable&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TS_AUTHKEY&lt;/td&gt;
&lt;td&gt;Authentication key&lt;/td&gt;
&lt;td&gt;Needed only for initial deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TS_STATE_DIR&lt;/td&gt;
&lt;td&gt;State storage location&lt;/td&gt;
&lt;td&gt;Authentication state is persisted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TS_ROUTES&lt;/td&gt;
&lt;td&gt;Advertised routes&lt;/td&gt;
&lt;td&gt;LAN subnet to be accessible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  4.3.3 Starting the Container
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/tailscale
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Check logs&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.3.4 Operation Verification
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check container status&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps | &lt;span class="nb"&gt;grep &lt;/span&gt;tailscale

&lt;span class="c"&gt;# Check Tailscale status&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;tailscale tailscale status

&lt;span class="c"&gt;# Expected output example:&lt;/span&gt;
&lt;span class="c"&gt;# 100.x.x.x  home-router  your-tailnet.ts.net  linux  -&lt;/span&gt;
&lt;span class="c"&gt;# 100.x.x.x  iphone       your-tailnet.ts.net  iOS    idle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.3.5 Route Approval
&lt;/h4&gt;

&lt;p&gt;After starting the container, enable 192.168.x.0/24 in Tailscale Admin Console → Machines → home-router → Edit route settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 iptables/UFW Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  4.4.1 Changing UFW Routed Policy
&lt;/h4&gt;

&lt;p&gt;Since Subnet Router forwards packets, UFW routed policy needs to be changed to allow.&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="c"&gt;# Check current settings&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status verbose
&lt;span class="c"&gt;# Default: deny (incoming), allow (outgoing), deny (routed) ← routed is deny&lt;/span&gt;

&lt;span class="c"&gt;# Edit /etc/default/ufw&lt;/span&gt;
&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/'&lt;/span&gt; /etc/default/ufw

&lt;span class="c"&gt;# Reload UFW&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw reload

&lt;span class="c"&gt;# Verify&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status verbose
&lt;span class="c"&gt;# Default: deny (incoming), allow (outgoing), allow (routed) ← OK if allow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.4.2 Allowing WireGuard Port
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 41641/udp comment &lt;span class="s2"&gt;"Tailscale WireGuard"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.4.3 DOCKER-USER Chain Configuration (Important)
&lt;/h4&gt;

&lt;p&gt;When using Docker, the DOCKER-USER chain filters traffic from outside. Traffic from Tailscale CGNAT (100.64.0.0/10) needs to be allowed.&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="c"&gt;# Edit /etc/ufw/after.rules&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/ufw/after.rules &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'

# BEGIN TAILSCALE DOCKER INTEGRATION
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16
-A DOCKER-USER -j RETURN -s 100.64.0.0/10

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END TAILSCALE DOCKER INTEGRATION
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Reload UFW&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw reload

&lt;span class="c"&gt;# Verify&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-L&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Without &lt;code&gt;-A DOCKER-USER -j RETURN -s 100.64.0.0/10&lt;/code&gt;, UDP packets (including DNS) from Tailscale clients to LAN will be dropped.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5 Unbound (DNS Server) Configuration
&lt;/h3&gt;

&lt;p&gt;When using Split DNS, the DNS server needs to allow queries from Tailscale CGNAT (100.64.0.0/10).&lt;/p&gt;

&lt;h4&gt;
  
  
  4.5.1 Unbound ACL Configuration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Edit /etc/unbound/unbound.conf.d/access-control.conf&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/unbound/unbound.conf.d/access-control.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
server:
    # Localhost
    access-control: 127.0.0.0/8 allow

    # LAN
    access-control: 192.168.x.0/24 allow

    # Docker networks
    access-control: 172.16.0.0/12 allow

    # Tailscale CGNAT (for Split DNS)
    access-control: 100.64.0.0/10 allow

    # Deny all others
    access-control: 0.0.0.0/0 refuse
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Restart Unbound&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart unbound

&lt;span class="c"&gt;# Verify&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status unbound
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4.5.2 DNS Server UFW Configuration
&lt;/h4&gt;

&lt;p&gt;If the DNS server is on a separate machine, UFW on that machine also needs settings to allow DNS queries from Tailscale CGNAT.&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="c"&gt;# Run on DNS server side&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 100.64.0.0/10 to any port 53 proto tcp comment &lt;span class="s2"&gt;"DNS TCP from Tailscale"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 100.64.0.0/10 to any port 53 proto udp comment &lt;span class="s2"&gt;"DNS UDP from Tailscale"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Verification Procedures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Server-Side Verification
&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;# 1. Container startup verification&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps | &lt;span class="nb"&gt;grep &lt;/span&gt;tailscale
&lt;span class="c"&gt;# Expected: tailscale container in Up state&lt;/span&gt;

&lt;span class="c"&gt;# 2. Tailscale authentication verification&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;tailscale tailscale status
&lt;span class="c"&gt;# Expected: home-router displayed with 192.168.x.0/24 in Routes&lt;/span&gt;

&lt;span class="c"&gt;# 3. IP forwarding verification&lt;/span&gt;
sysctl net.ipv4.ip_forward
&lt;span class="c"&gt;# Expected: net.ipv4.ip_forward = 1&lt;/span&gt;

&lt;span class="c"&gt;# 4. Health check&lt;/span&gt;
curl http://localhost:9002/healthz
&lt;span class="c"&gt;# Expected: 200 OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.2 Client-Side Verification
&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;# 1. Tailscale connection verification&lt;/span&gt;
tailscale status
&lt;span class="c"&gt;# Expected: Connection successful, 100.x.x.x IP obtained&lt;/span&gt;

&lt;span class="c"&gt;# 2. LAN connectivity verification&lt;/span&gt;
ping 192.168.x.10
&lt;span class="c"&gt;# Expected: Response received&lt;/span&gt;

&lt;span class="c"&gt;# 3. DNS verification (Split DNS)&lt;/span&gt;
dig grafana.example.local
&lt;span class="c"&gt;# Expected: Returns internal IP like 192.168.x.10&lt;/span&gt;

&lt;span class="c"&gt;# 4. Service access&lt;/span&gt;
curl https://grafana.example.local/
&lt;span class="c"&gt;# Expected: Normal response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.3 Security Verification
&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;# Inter-terminal communication blocking verification (from Admin device to Family device)&lt;/span&gt;
ping 100.x.x.x  &lt;span class="c"&gt;# Tailscale IP of Family device&lt;/span&gt;
&lt;span class="c"&gt;# Expected: Timeout (communication not possible)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Additional Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 Important Settings Not Documented in Design Documents
&lt;/h3&gt;

&lt;p&gt;Important configuration items discovered during actual construction that were missing from documentation:&lt;/p&gt;

&lt;h4&gt;
  
  
  6.1.1 DNS Server UFW Rules
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; DNS queries timeout even with Split DNS configured&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; DNS server UFW was blocking queries from Tailscale CGNAT (100.64.0.0/10)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Add the following to DNS server UFW
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 100.64.0.0/10 to any port 53 proto tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 100.64.0.0/10 to any port 53 proto udp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  6.1.2 DOCKER-USER Chain Configuration
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; ping works but DNS (UDP port 53) times out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; DROP rules in DOCKER-USER chain were dropping UDP packets from Tailscale
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-A DOCKER-USER -j DROP -p udp --dport 0:32767 -d 192.168.0.0/16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Add the following to /etc/ufw/after.rules
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-A DOCKER-USER -j RETURN -s 100.64.0.0/10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  6.1.3 UFW Routed Policy
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; Tailscale connects but all communication to LAN times out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; UFW routed policy was deny by default, blocking packet forwarding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Change to &lt;code&gt;DEFAULT_FORWARD_POLICY="ACCEPT"&lt;/code&gt; in /etc/default/ufw&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.2 Troubleshooting
&lt;/h3&gt;

&lt;h4&gt;
  
  
  When Split DNS Does Not Work
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check client settings&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;iOS/Android: Check if Use Tailscale DNS is enabled in Tailscale app settings&lt;/li&gt;
&lt;li&gt;macOS/Windows: Check for conflicts between system DNS settings and Tailscale settings&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Admin Console settings&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is Restrict to domain correctly configured?&lt;/li&gt;
&lt;li&gt;Is Override local DNS Off?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check with packet capture&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;# Run on Subnet Router side&lt;/span&gt;
   &lt;span class="nb"&gt;sudo &lt;/span&gt;tcpdump &lt;span class="nt"&gt;-i&lt;/span&gt; any &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'port 53'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  When ping Works But Cannot Access Specific Services
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check DOCKER-USER chain&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-L&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Check if RETURN rule for 100.64.0.0/10 exists&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Auth Key Expiration
&lt;/h4&gt;

&lt;p&gt;Since state is maintained with Named Volume, this is usually not a problem. However, a new Auth Key is required in the following cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When Named Volume is deleted&lt;/li&gt;
&lt;li&gt;When device is deleted from Tailscale Admin Console&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.3 Operational Notes
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Auth Key Rotation
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Required Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container restart&lt;/td&gt;
&lt;td&gt;None (state is persisted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Named Volume deletion&lt;/td&gt;
&lt;td&gt;Issue new Auth Key, restart container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device deletion from Admin Console&lt;/td&gt;
&lt;td&gt;Issue new Auth Key, restart container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Version Update Procedure
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Check Tailscale Release Notes&lt;/li&gt;
&lt;li&gt;Verify new version in test environment&lt;/li&gt;
&lt;li&gt;Update image tag in docker-compose.yml&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  6.4 Reference Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/kb/1019/subnets/" rel="noopener noreferrer"&gt;Tailscale Subnet Routers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/kb/1282/docker/" rel="noopener noreferrer"&gt;Tailscale Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/kb/1054/dns/" rel="noopener noreferrer"&gt;Tailscale DNS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/kb/1018/acls/" rel="noopener noreferrer"&gt;Tailscale ACLs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This completes the Tailscale Subnet Router construction. You can now securely access your home LAN from outside.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>networking</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Obsidian AI Exporter: The Current Evolution Supporting Multiple AIs</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Fri, 20 Mar 2026 01:51:03 +0000</pubDate>
      <link>https://dev.to/sho7650/obsidian-ai-exporter-the-current-evolution-supporting-multiple-ais-32kn</link>
      <guid>https://dev.to/sho7650/obsidian-ai-exporter-the-current-evolution-supporting-multiple-ais-32kn</guid>
      <description>&lt;p&gt;Opportunities to interact with generative AI like Gemini and Claude in daily life have been increasing. When doing research or brainstorming, you often find brilliant ideas that make you go "aha!" during exchanges with AI, right?&lt;/p&gt;

&lt;p&gt;But the moment you close your browser tab, those precious conversations flow away into the depths of your history. I wanted to keep my dialogues with AI as my own "knowledge asset" that I could search and link to. With that thought in mind, I released the Chrome extension "Obsidian AI Exporter" two months ago.&lt;/p&gt;

&lt;p&gt;Thanks to everyone, many people have started using it, and over these two months I have received bug reports and feature requests and have made various updates. Today, I want to share with you the new features of this greatly evolved extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Kind of Tool Is This?
&lt;/h2&gt;

&lt;p&gt;Obsidian AI Exporter is an extension that extracts conversations with AI in your browser with one click and saves them as clean Obsidian documents.&lt;/p&gt;

&lt;p&gt;It originally started as Gemini-only, but since I personally use multiple AIs, it now supports all the generative AI services I use. These days, you don't have to stick to a single platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Major New Features Added
&lt;/h2&gt;

&lt;p&gt;Over the two months since release, it has evolved to be more flexible and more convenient.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Expanded AI Platform Support
&lt;/h3&gt;

&lt;p&gt;In addition to Gemini, it now supports major AI services, including Claude, ChatGPT, and Perplexity. No matter which AI you are conversing with, just press the "Sync" button that appears in the bottom right of the same screen to save your entire conversation or research results.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Support for Saving Methods Other Than Obsidian
&lt;/h3&gt;

&lt;p&gt;Although it bears the name Obsidian, you no longer necessarily need to use Obsidian. By configuring the Markdown function from the settings screen, you have the option to download as a pure Markdown file to your computer or copy to clipboard and paste into another editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Appending to Existing Notes and Adding Unique Features
&lt;/h3&gt;

&lt;p&gt;An append mode has been added to write only new conversations to existing notes. Furthermore, fine adjustments tailored to each AI's characteristics have been made, such as saving the report portion of Gemini's Deep Research and the collapsible display of web search results in Claude.&lt;/p&gt;

&lt;p&gt;It also supports the LaTeX format, so formulas are properly recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Your Important Data in Your Own Hands
&lt;/h2&gt;

&lt;p&gt;What this extension is committed to is balancing privacy and convenience. Without going through external servers, all processing is completed in your browser and local environment. SSL is supported, but Obsidian-side configuration is necessary.&lt;/p&gt;

&lt;p&gt;The more AI becomes our excellent partner, the more the dialogues exchanged there should become irreplaceable assets. Before they fly away and disappear, try pulling them to your hands with one click.&lt;/p&gt;

&lt;p&gt;I am placing links to the Chrome Web Store and GitHub below. I would be delighted if you could install it and try out how it works.&lt;/p&gt;

&lt;p&gt;Chrome Web Store:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://chromewebstore.google.com/detail/obsidian-ai-exporter/edemgeigfbodiehkjhjflleipabgbdeh" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2Fp9M5kLl7FSEKDysWM8FQvbRigSt6LfDfsn50xohlxB_xj6LeUA2nbmfPR0VQB1CQCHCe7Vk8HVBTQ7rQvpPEfSCJ_g%3Ds128-rj-sc0x00ffffff" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://chromewebstore.google.com/detail/obsidian-ai-exporter/edemgeigfbodiehkjhjflleipabgbdeh" rel="noopener noreferrer" class="c-link"&gt;
            Obsidian AI Exporter - Chrome Web Store
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Export AI conversations from Gemini, Claude, ChatGPT, and Perplexity to Obsidian via Local REST API
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fssl.gstatic.com%2Fchrome%2Fwebstore%2Fimages%2Ficon_48px.png"&gt;
          chromewebstore.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/sho7650" rel="noopener noreferrer"&gt;
        sho7650
      &lt;/a&gt; / &lt;a href="https://github.com/sho7650/obsidian-AI-exporter" rel="noopener noreferrer"&gt;
        obsidian-AI-exporter
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Obsidian AI Exporter&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Chrome Extension that exports AI conversations from Google Gemini, Claude AI, ChatGPT, and Perplexity to Obsidian via the Local REST API.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/sho7650/obsidian-AI-exporter/README.ja.md" rel="noopener noreferrer"&gt;日本語版はこちら&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://opensource.org/licenses/MIT" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://chromewebstore.google.com/detail/obsidian-ai-exporter/edemgeigfbodiehkjhjflleipabgbdeh" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9152c7975ff2c59f475e1b8e39bfaa0c35d0e03584233ae41a20e927876473e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4368726f6d652d457874656e73696f6e2d626c7565" alt="Chrome Web Store"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-platform support&lt;/strong&gt;: Export from Google Gemini, Claude AI, ChatGPT, and Perplexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click export&lt;/strong&gt;: Floating "Sync" button on supported AI pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple output options&lt;/strong&gt;: Save to Obsidian, download as file, or copy to clipboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep Research support&lt;/strong&gt;: Export Gemini Deep Research and Claude Extended Thinking reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact support&lt;/strong&gt;: Extract Claude Artifacts with inline citations and sources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool content support&lt;/strong&gt;: Optionally include Claude's web search results and tool activity as collapsible &lt;code&gt;[!ABSTRACT]&lt;/code&gt; callouts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append mode&lt;/strong&gt;: Only new messages are added to existing notes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obsidian callouts&lt;/strong&gt;: Formatted output with &lt;code&gt;[!QUESTION]&lt;/code&gt; and &lt;code&gt;[!NOTE]&lt;/code&gt; callouts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YAML frontmatter&lt;/strong&gt;: Metadata including title, source, URL, dates, and tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-scroll&lt;/strong&gt;: Automatically loads all messages in long Gemini conversations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform-based organization&lt;/strong&gt;: Use &lt;code&gt;{platform}&lt;/code&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/sho7650/obsidian-AI-exporter" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;I plan to continue adding convenient features little by little. If you have impressions from trying it or requests for features you want, please feel free to leave a comment on a GitHub issue or note.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>AI Era: The Power of Writing Meditation - Connecting with Notebooks</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Wed, 11 Mar 2026 01:34:12 +0000</pubDate>
      <link>https://dev.to/sho7650/ai-era-the-power-of-writing-meditation-connecting-with-notebooks-1mi4</link>
      <guid>https://dev.to/sho7650/ai-era-the-power-of-writing-meditation-connecting-with-notebooks-1mi4</guid>
      <description>&lt;p&gt;Lately, I have been feeling a bit overwhelmed by the evolution of AI.&lt;/p&gt;

&lt;p&gt;My reliance on AI has made my desires unmanageable. I can immediately start on ideas I have and let troublesome tasks be handled automatically. I feel it is wonderful that getting advice has lowered the hurdle to taking the first step.&lt;/p&gt;

&lt;p&gt;However, it is also true that when I pause to reflect, moments of feeling lost have increased, wondering what exactly I am doing all this for.&lt;/p&gt;

&lt;p&gt;During such times when I am about to be swallowed by waves of information and ideas, what saves me is not a digital screen but an old-fashioned analog notebook.&lt;/p&gt;

&lt;p&gt;I pour my heart into a blank notebook, reconnecting with my life and purpose. What should I be doing now? What should I be thinking about? Should I calm my mind and deal with the task at hand, or should I move toward a future vision? Above all, I need to make a decision about what not to do.&lt;/p&gt;

&lt;p&gt;When it comes to these things, no matter how excellent an AI I ask, answers simply will not come back. Unless you are going to walk a life decided by AI, if there is a path you want to walk on your feet, you absolutely need time to open your notebook and deeply face yourself.&lt;/p&gt;

&lt;p&gt;For me, this time of self-dialogue is similar to meditation.&lt;/p&gt;

&lt;p&gt;Time spent at my desk endlessly writing out characters and time spent slowly running while letting my thoughts wander. Both are beloved moments that I cannot do without.&lt;/p&gt;

&lt;p&gt;However, when thoughts are just circling in my head, working memory fills up, and I am driven by the urge to write them out somewhere. That is precisely why a notebook as a receptacle for thought is optimal.&lt;/p&gt;

&lt;p&gt;You might think you cannot take notes while running.&lt;/p&gt;

&lt;p&gt;That is certainly true, and I have not yet found a good way to record things while running. But I have come to interpret that ideas I have thoroughly thought through while running and still vividly remember when I get home are what is truly important to me right now.&lt;/p&gt;

&lt;p&gt;I may forget trivial triggers, but essential things will surely remain in my heart. Believing that, I only note down the crystals that remain after finishing my run and connect them to the next step.&lt;/p&gt;

&lt;p&gt;I sit at an empty desk, staring at my notebook, tirelessly crafting characters. I love this tranquil time.&lt;/p&gt;

&lt;p&gt;We intentionally pause in a time of abundance and convenience. Writing characters in a notebook allows you to meditate and listen to your own heart's voice. Would you like to try it once as well? I highly recommend it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mentalhealth</category>
      <category>productivity</category>
      <category>writing</category>
    </item>
    <item>
      <title>Accelerating AI and the Courage to Pause: Reclaim Your Core Values Through Essential Thinking</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Mon, 02 Mar 2026 05:57:02 +0000</pubDate>
      <link>https://dev.to/sho7650/accelerating-ai-and-the-courage-to-pause-reclaim-your-core-values-through-essential-thinking-2eai</link>
      <guid>https://dev.to/sho7650/accelerating-ai-and-the-courage-to-pause-reclaim-your-core-values-through-essential-thinking-2eai</guid>
      <description>&lt;p&gt;Lately, I have been hearing the term "AI fatigue" everywhere.&lt;/p&gt;

&lt;p&gt;Bold claims are flying around that AI will change the world and that conventional tools are no longer needed. However, behind all this, I feel that the amount of activity each of us needs to handle has actually increased to an abnormal degree.&lt;/p&gt;

&lt;p&gt;When I look back on my past personal development experiences, the pace was much slower. For someone like me who does not code as a main profession, touching programs every day is not easy. Creating one class a day was enough, and as the term "weekend programmer" suggested, development was something that progressed little by little, taking time.&lt;/p&gt;

&lt;p&gt;But now, with the help of AI, I have three coding projects running simultaneously. New features are added every day, and issues that come up can be resolved within the same day. This is a speed that my former self could never have imagined.&lt;/p&gt;

&lt;p&gt;However, when I stop and reflect, I begin to wonder if this is really what I wanted to do.&lt;/p&gt;

&lt;p&gt;I started using AI to save time and improve efficiency, but before I knew it, I was being chased by the outputs that AI keeps producing one after another. To keep up with AI, I have been forcing myself to do unnecessary things. As a result, many outputs have been produced, but my mind has been tossed around and exhausted by them. This is not the future I wished for.&lt;/p&gt;

&lt;p&gt;When it comes to work, I have found a certain answer for how to engage with AI. My goal is not mere efficiency but improving quality. As long as I keep that in mind, AI functions as a very reliable partner.&lt;/p&gt;

&lt;p&gt;The problem is how I spend my private time. Being driven by code that AI has written or analysis reports that AI has organized feels somehow wrong.&lt;/p&gt;

&lt;p&gt;So I decided to return to the concept of essentialism. What is truly important to me? In which areas can I use AI to genuinely enhance my motivation and results? I need to reassess these things.&lt;/p&gt;




&lt;p&gt;It is natural to take responsibility for what you have created, but you must not be crushed by that responsibility and lose sight of your original purpose. Being controlled by something you started as a pastime or hobby is putting the cart before the horse.&lt;/p&gt;

&lt;p&gt;Therefore, to protect my own time, I established new rules.&lt;/p&gt;

&lt;p&gt;First, I place what is essentially important to me at the center of my schedule. Then, for everything else that I could do but is not urgent, I confine it to specific time blocks.&lt;/p&gt;

&lt;p&gt;Specifically, I dedicate my morning hours at the start of the day to my most mentally demanding important work and my core activities. I make sure to finish essential tasks within the morning. Then, I allocate tasks that do not require as much concentration and hobby-related activities to afternoon time blocks. If they do not get finished within that time, I simply carry them over to the next day.&lt;/p&gt;




&lt;p&gt;In the past, being controlled by other people was a source of frustration, but now that may have simply transformed into being controlled by AI.&lt;/p&gt;

&lt;p&gt;Precisely because I have obtained such a convenient tool, I want to have a clear sense of what I want to do and what I should do. Rather than matching myself to the speed of AI, I want to incorporate AI into my own comfortable rhythm. In this way, I intend to continue protecting my own way of moving forward.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>coding</category>
      <category>mentalhealth</category>
      <category>productivity</category>
    </item>
    <item>
      <title>When I Hired Multiple Generative AIs as Subordinates, Parallel Development Processing Never Stopped</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Wed, 11 Feb 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/sho7650/when-i-hired-multiple-generative-ais-as-subordinates-parallel-development-processing-never-stopped-390i</link>
      <guid>https://dev.to/sho7650/when-i-hired-multiple-generative-ais-as-subordinates-parallel-development-processing-never-stopped-390i</guid>
      <description>&lt;p&gt;Today is a relaxed introduction to recent Shossan updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Major Update to Obsidian AI Exporter
&lt;/h2&gt;

&lt;p&gt;The tool I have been publicly releasing for saving Gemini chat results to Obsidian, called Obsidian AI Exporter, has been significantly powered up.&lt;/p&gt;

&lt;p&gt;In my previous introduction, it was Gemini-only, but now it supports four sites: ChatGPT, Claude (including Deep Research), and Perplexity. In addition to Obsidian integration, you can now save as pure Markdown files or copy to clipboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="//chromewebstore.google.com"&gt;Obsidian AI Exporter - Chrome Web Store&lt;/a&gt;&lt;br&gt;
Export AI conversations from Gemini, Claude, ChatGPT, and Perplexity to Obsidian&lt;/p&gt;

&lt;p&gt;While I think it will be in the maintenance phase unless there are major UI changes, I am still looking to add more useful features. If you have any feature requests, please let me know!&lt;/p&gt;

&lt;p&gt;Currently, I am considering automatic tagging, expanding the append feature, which is still not quite there, and Wiki Link support for Obsidian.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Building Your Own News Curation Site with n8n
&lt;/h2&gt;

&lt;p&gt;I wanted a site where I could read the latest overseas news summarized in a format convenient for me, so I developed my own curation platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbxa7mrg59e6rlvcn43i9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbxa7mrg59e6rlvcn43i9.png" alt=" " width="800" height="940"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mechanism uses n8n to fetch updates from specific RSS feeds and URL lists, then an LLM (generative AI) translates and summarizes the content into Japanese. This is then registered to a display app I built with Docker.&lt;/p&gt;

&lt;p&gt;By managing specific news URLs as unique keys, it prevents duplicate posts with a simple mechanism. The main body is simple and just displays content, but I designed it with loose coupling so that LLMs and feeds can be easily changed. I have also published the source code, so please take a look if you are interested.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyll7oqg4n2t9k51u4bb9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyll7oqg4n2t9k51u4bb9.png" alt=" " width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Around v2, the check/ingest/post article parts are the developed app portions.&lt;/p&gt;

&lt;p&gt;I named it tech-news as is since it is for personal use, but really the content to post can be anything as long as it is Markdown, so it is like a simple blog. Since I am developing it component by component for easy reuse, I am feeling like it has become more versatile than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Revamping Home VPN Environment with Tailscale
&lt;/h2&gt;

&lt;p&gt;On the infrastructure side, I am organizing my home access environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="//tailscale.com"&gt;Tailscale | Secure Connectivity for AI, IoT &amp;amp; Multi-Cloud&lt;/a&gt;&lt;br&gt;
The connectivity platform for devs, IT, and security teams.&lt;/p&gt;

&lt;p&gt;I had been using the VPN function of my Yamaha router, but considering security and convenience, I am currently introducing and configuring Tailscale.&lt;/p&gt;

&lt;p&gt;Being able to safely access home resources via NAT traversal without opening VPN ports externally is safe and comfortable. The app is also easy to use, and functionally very detailed settings are possible. I have no idea why it is free for up to 3 users.&lt;/p&gt;

&lt;p&gt;Currently, I am configuring a router with Docker on my home Linux server and managing settings with Ansible. An environment is coming together where I can work without being conscious of the boundary between home and the internet even when out.&lt;/p&gt;

&lt;h2&gt;
  
  
  From the Joy of Researching to the Joy of Realizing
&lt;/h2&gt;

&lt;p&gt;Running multiple projects in parallel like this, I can feel the changes in development style in the AI era firsthand.&lt;/p&gt;

&lt;p&gt;In the past, I found joy in the trial and error of researching technologies one by one. But now, I find it fun to watch my ideas take shape one after another, using multiple generative AIs as my subordinates. Is this a change due to age, or is it new excitement brought about by technological evolution?&lt;/p&gt;

&lt;p&gt;The image of what I wanted to do becomes realized with the help of AI. Grinning at that process, I am giving instructions to my new subordinates again today.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>How to Transform 3 Years of Thinking into Assets: Accelerate Feedback with Claude Cowork</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Wed, 11 Feb 2026 13:30:16 +0000</pubDate>
      <link>https://dev.to/sho7650/how-to-transform-3-years-of-thinking-into-assets-accelerate-feedback-with-claude-cowork-kl0</link>
      <guid>https://dev.to/sho7650/how-to-transform-3-years-of-thinking-into-assets-accelerate-feedback-with-claude-cowork-kl0</guid>
      <description>&lt;h1&gt;
  
  
  Claude Cowork: The Ultimate Engine for Objectively Viewing Your Own Thought Evolution
&lt;/h1&gt;

&lt;p&gt;I had been pondering whether Claude Cowork actually has any practical uses.&lt;/p&gt;

&lt;p&gt;Then I discovered yet another new way to use it. Cowork is a work companion that autonomously executes multiple tasks based on massive amounts of data that surpass what chat can handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Cowork: The Ultimate Engine for Objectively Viewing Your Own Thought Evolution
&lt;/h2&gt;

&lt;p&gt;As you know, Claude Cowork is not merely a chat tool. It is an AI agent that directly accesses your local file system (in my case, Obsidian) and autonomously completes tasks.&lt;/p&gt;

&lt;p&gt;You can either place Obsidian in a folder that Cowork can access, or use the combination of Local REST API + Obsidian MCP Tools to have Cowork retrieve information via MCP—both approaches work effectively.&lt;/p&gt;

&lt;p&gt;What makes this tool truly disruptive is its ability to execute against three years of my living thought data with precision and speed comparable to the Observe and Orient phases of the OODA loop (Observe, Orient, Decide, Act). Sounds pretentious, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling High-Dimensional Differential Extraction Across Time—Something Difficult to Do Alone
&lt;/h2&gt;

&lt;p&gt;Why bother using Claude Cowork? Because I know how vague human memory is and how conveniently it gets rewritten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking Through the Limits of Observe:&lt;/strong&gt; It is humanly impossible to extract every single description on a specific theme from three years of notes numbering in the thousands. Information obtained through word searches is only a fraction. You need to extract information while considering intent as well. Cowork autonomously repeats searching and browsing, comprehensively observing all data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deepening Orient:&lt;/strong&gt; The thinking habits of my three-years-ago self versus my current self, the subjects of interest, and changes in word choice. By having AI identify these differences, I can confront my own growth or stagnation as objective facts that I could never have noticed on my own.&lt;/p&gt;

&lt;p&gt;This is not for reminiscing about the past, but for gaining an advanced metacognitive environment to use my past self as a stepping stone toward new ideas.&lt;/p&gt;

&lt;p&gt;This is an effective environment that I could not achieve simply by using Obsidian as RAG with Dify. What was limited to merely retrieving information has now evolved into comprehensive information retrieval and analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Three Years of Obsidian as Fuel to Gain Insights for the Future
&lt;/h2&gt;

&lt;p&gt;I have actually shared my three years of Obsidian data with Claude Cowork and am running workflows like the following:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 (Observe):&lt;/strong&gt; Scan all notes containing my views on generative AI from 2023 to the present (2026).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 (Orient):&lt;/strong&gt; Analyze chronologically at which points and through which technological breakthroughs my thinking changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 (Insight):&lt;/strong&gt; From that trajectory of change, have it present blind spots in thinking that I should focus on in the next year.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[Image of spinning the OOA loop with Cowork]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Previously, doing this required spending several days reading through past notes. However, with Claude Cowork, while generating intermediate analysis files in the working folder, it writes up a map of my own thinking in just minutes. This is not mere summarization—it is prototyping for the future through dialogue with my past self.&lt;/p&gt;

&lt;p&gt;This is an efficient and effective way to reuse PKM (Personal Knowledge Management).&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuously Updating Your Thinking Together with an AI Colleague
&lt;/h2&gt;

&lt;p&gt;In these uncertain times ahead, there is no time to cling to past success stories. On the other hand, data about what you thought in the past and how you experimented is your own unique asset that belongs to no one else.&lt;/p&gt;

&lt;p&gt;Stop using Claude Cowork as just a summarization tool.&lt;/p&gt;

&lt;p&gt;Cowork is literally a partner for increasing the speed of your thinking and running the OODA loop faster than anyone else. Whether you let three years of data sleep as mere records or turn it into a power plant for your next ideas—that key depends on how you master Cowork as an agent.&lt;/p&gt;

&lt;p&gt;I will continue to gaze into this abyss of the ultimate mirror and refine my thinking day after day.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>mcp</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Illusion of My Own Efficiency: How Claude Exposed My Arrogance and Lack of Planning at Night</title>
      <dc:creator>Takashi Abe</dc:creator>
      <pubDate>Fri, 06 Feb 2026 07:21:29 +0000</pubDate>
      <link>https://dev.to/sho7650/the-illusion-of-my-own-efficiency-how-claude-exposed-my-arrogance-and-lack-of-planning-at-night-43lg</link>
      <guid>https://dev.to/sho7650/the-illusion-of-my-own-efficiency-how-claude-exposed-my-arrogance-and-lack-of-planning-at-night-43lg</guid>
      <description>&lt;h1&gt;
  
  
  Conversations with AI are not for improving skills, but for recognizing your own arrogance
&lt;/h1&gt;

&lt;p&gt;I, Shossan, have a certain pride in having automated my environment reasonably well and controlling it efficiently. However, through analyzing my own shell history while conversing with the trending Claude Cowork, what was thrust before me was an image of myself—utterly unplanned and full of biases. How embarrassing.&lt;/p&gt;

&lt;p&gt;Being prompted by AI to improve is a shock close to defeat as an engineer. However, I am convinced that this very defeat is the only gateway to the next stage of growth. Gemini's words are too harsh.&lt;/p&gt;

&lt;h2&gt;
  
  
  People mistake unconscious repetition for effort
&lt;/h2&gt;

&lt;p&gt;Why couldn't I notice my own inefficiencies? It's because a bias called familiarity had crept into my daily work.&lt;/p&gt;

&lt;p&gt;This time, when I had Claude cowork analyze my past command history, the following three blind spots were exposed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collapse of time management:&lt;/strong&gt; Activity peaked at 21:00, a time when I should have been resting. This wasn't diligence—it was merely repaying debt caused by lack of planning during the day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hollow automation:&lt;/strong&gt; I manually executed ansible-playbook 31 times. While thinking I was using automation tools, the execution process itself was extremely analog and dependent on manual work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool dependency:&lt;/strong&gt; By using convenient tools (Keyboard Maestro, Stream Deck, Claude Code, etc.), I had arbitrarily decided that there was no more room for improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cold data-driven insights and the automation functions I immediately implemented
&lt;/h2&gt;

&lt;p&gt;According to Claude's analysis, my activity is concentrated at 8:00 and 21:00. The fact that I was hammering away at commands at 21:00 after my bath is evidence of a bad habit that degrades sleep quality and diminishes the next day's performance. I truly never expected such results. I was genuinely surprised.&lt;/p&gt;

&lt;p&gt;You should sleep after your bath. If you have to execute commands at that time, it means your planning has failed. Accepting this fact, I immediately adopted the improvement proposals that Claude suggested.&lt;/p&gt;

&lt;p&gt;For example, here is a function for managing my custom LaunchAgents that start with com.oshiire.. Until now, I had been typing commands while trying to remember paths or searching through history, but now this svc function takes over that cognitive load.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ~/.config/fish/functions/svc.fish
function svc --description "LaunchAgent service control"
  set -l action $argv[1]
  set -l name $argv[2]
  set -l plist ~/Library/LaunchAgents/com.oshiire.$name.plist

  switch $action
    case start
      launchctl load -w $plist
    case stop
      launchctl unload -w $plist
    case restart
      launchctl unload -w $plist; and launchctl load -w $plist
    case status
      launchctl list | grep $name
    case '*'
      echo "Usage: svc [start|stop|restart|status] [service_name]"
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, Claude proposed a short function called ap for ansible-playbook. This eliminates small decisions like argument specification mistakes and forgotten options. Such thoughtful consideration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking into the mirror called AI, strategically updating yourself continuously
&lt;/h2&gt;

&lt;p&gt;If you just end up praising AI's improvement suggestions as amazing, you are merely a consumer. For me, the learning from this experience lies in how to convert the negative of having AI point things out to me into the positive of solving through systems.&lt;/p&gt;

&lt;p&gt;Abandon the assumption that you are doing well, and set aside time to regularly dissect yourself with objective data. Use AI not just as a code generator but as a behavioral analysis partner.&lt;/p&gt;

&lt;p&gt;From here, I will continue to cut away waste, one step at a time. Why don't you also try exposing your terminal history and behavioral history—your embarrassing parts—to AI?&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
