<?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>Forem: tumf</title>
    <description>The latest articles on Forem by tumf (@tumf).</description>
    <link>https://forem.com/tumf</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F814438%2F10b17619-7336-404f-8327-624b5619493f.png</url>
      <title>Forem: tumf</title>
      <link>https://forem.com/tumf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tumf"/>
    <language>en</language>
    <item>
      <title>Web Adapter Tool Agent: Turn Self-Learning Skills into "98% Average Token Reduction on Revisits," Measured</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Mon, 09 Mar 2026 02:25:02 +0000</pubDate>
      <link>https://forem.com/tumf/web-adapter-tool-agent-turn-self-learning-skills-into-98-average-token-reduction-on-revisits-3mi4</link>
      <guid>https://forem.com/tumf/web-adapter-tool-agent-turn-self-learning-skills-into-98-average-token-reduction-on-revisits-3mi4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-03-09&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/3/9/web-adapter-tool-agent-architecture/" rel="noopener noreferrer"&gt;Web→Adapter→Tool→Agent: 自己学習型スキルで『再訪を実測で平均98%トークン削減』する&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you build web data extraction by having an LLM read raw HTML every time and "just figure it out," it usually ends up expensive, slow, and brittle.&lt;/p&gt;

&lt;p&gt;It gets worse for use cases that revisit the same site repeatedly - news monitoring, documentation tracking, price change detection, and so on. You end up repeating the same failure modes over and over.&lt;/p&gt;

&lt;p&gt;Problems like this are often better solved not with ever more heroic scraping tricks, but by accepting a simpler approach: once an extraction method works, freeze it as a reusable tool and keep using it from then on.&lt;/p&gt;

&lt;p&gt;This article summarizes a design that turns scraping into a learned tool through a Web→Adapter→Tool→Agent transformation pipeline.&lt;/p&gt;

&lt;p&gt;The original inspiration was &lt;a href="https://blog.tumf.dev/posts/diary/2026/3/1/web2cli-every-website-is-a-unix-command/" rel="noopener noreferrer"&gt;web2cli&lt;/a&gt; (&lt;a href="https://github.com/jb41/web2cli" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;), which I introduced in an earlier article. If you take the idea of "Every website is a Unix command" and push it toward agent operations - revisits, token usage, and drift - it tends to converge in this direction.&lt;/p&gt;

&lt;p&gt;More recently, along that line of thought, I added a &lt;strong&gt;self-learning skill&lt;/strong&gt; called &lt;a href="https://github.com/tumf/self-learning-web-adapter" rel="noopener noreferrer"&gt;self-learning-web-adapter&lt;/a&gt; (&lt;code&gt;skill&lt;/code&gt;: a package of procedures and tools given to an agent). The skill itself lives in &lt;a href="https://github.com/tumf/self-learning-web-adapter/tree/main/skills/self-learning-web-adapter" rel="noopener noreferrer"&gt;skills/self-learning-web-adapter&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this hurts: passing raw HTML directly to an LLM increases cost
&lt;/h2&gt;

&lt;p&gt;First, let’s align on the premise. By "LLM," I mean a Large Language Model that works not only on text, but also as an &lt;code&gt;agent&lt;/code&gt; - a system where the LLM calls external tools to get work done.&lt;/p&gt;

&lt;p&gt;When you hand raw HTML to an LLM and ask it to extract information, the following costs pile up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;token&lt;/code&gt; cost is high&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;latency&lt;/code&gt; (processing wait time) increases&lt;/li&gt;
&lt;li&gt;it breaks when the DOM (&lt;code&gt;Document Object Model&lt;/code&gt;: the idea of treating HTML as a tree structure) changes even slightly&lt;/li&gt;
&lt;li&gt;retries increase when extraction fails, which makes it even more expensive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Personally, my real feeling is: "Fine for the first time, maybe, but I do not want to repeat the same exploration on the second run and beyond."&lt;/p&gt;

&lt;h2&gt;
  
  
  Direction of the solution: confine exploration to one pass, make execution lightweight
&lt;/h2&gt;

&lt;p&gt;The idea is simple. Stop re-scraping the Web from scratch every time, and transform it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;website
  ↓ (exploration: one pass)
adapter
  ↓ (freeze it)
tool / CLI
  ↓ (reuse)
agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this model, what the LLM does each time is no longer "interpret raw HTML," but "call a tool."&lt;/p&gt;

&lt;p&gt;When it works well, the LLM input can be compressed down to a few hundred tokens of JSON (&lt;code&gt;JavaScript Object Notation&lt;/code&gt;: a structured data format).&lt;/p&gt;

&lt;p&gt;As a reference point, if you compare "raw HTML" with "adapter output" for a specific site such as a blog or marketing page, you can sometimes see input token reductions in the &lt;strong&gt;95% to 99% range&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is better not to oversell this. The first learning pass has its own cost, and results vary by site. But the overall direction is very stable: if the workload revisits the same site often, the payoff is usually easy to recover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurement: how many tokens do revisits actually save?
&lt;/h2&gt;

&lt;p&gt;Since the obvious question is "Does it really shrink that much?", here is a simple measurement.&lt;/p&gt;

&lt;p&gt;For token counting, I used &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken&lt;/a&gt; (&lt;code&gt;tokenizer&lt;/code&gt;: a mechanism that splits strings into tokens), counted with &lt;code&gt;o200k_base&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The comparison uses three patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pass raw HTML directly to the LLM&lt;/li&gt;
&lt;li&gt;pass JSON output from a trained adapter to the LLM&lt;/li&gt;
&lt;li&gt;pass JSON output from a web2cli-style wrapper to the LLM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Training used three articles per site, and evaluation used one different article as a holdout set.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;HTML tokens&lt;/th&gt;
&lt;th&gt;Adapter tokens&lt;/th&gt;
&lt;th&gt;web2cli tokens&lt;/th&gt;
&lt;th&gt;Reduction vs Adapter&lt;/th&gt;
&lt;th&gt;Reduction vs web2cli&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blog.python.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15,057&lt;/td&gt;
&lt;td&gt;265&lt;/td&gt;
&lt;td&gt;351&lt;/td&gt;
&lt;td&gt;98.24%&lt;/td&gt;
&lt;td&gt;97.67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blog.rust-lang.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7,656&lt;/td&gt;
&lt;td&gt;263&lt;/td&gt;
&lt;td&gt;361&lt;/td&gt;
&lt;td&gt;96.56%&lt;/td&gt;
&lt;td&gt;95.28%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vercel.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;224,735&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;335&lt;/td&gt;
&lt;td&gt;99.89%&lt;/td&gt;
&lt;td&gt;99.85%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On average, the input token reduction looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;average reduction rate (direct adapter output): 98.23%&lt;/li&gt;
&lt;li&gt;average reduction rate (web2cli-style command output): 97.60%&lt;/li&gt;
&lt;li&gt;average reduction amount (direct adapter output): 82,221.7 tokens / page&lt;/li&gt;
&lt;li&gt;median reduction amount (direct adapter output): 14,792 tokens / page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are two key takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Raw HTML alone can be tens of thousands to hundreds of thousands of tokens, depending on the site&lt;/li&gt;
&lt;li&gt;Once learned, the system can compress only the needed information into a few hundred tokens of JSON&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For especially heavy pages like &lt;code&gt;vercel.com&lt;/code&gt;, it reduced more than 220k tokens per page.&lt;/p&gt;

&lt;p&gt;A few caveats are worth noting too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;this is still a small-scale measurement over only three sites&lt;/li&gt;
&lt;li&gt;the extracted fields are mainly limited to &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, and &lt;code&gt;published&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the first access includes learning cost, so the real benefit appears on revisits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rough estimate can be made with this formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;saved_cost = saved_tokens_per_page * pages_per_month / 1_000_000 * model_input_price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your workload is mostly lightweight articles, it is safer to reason from the median value (14,792 tokens / page). If you deal with many SPAs (&lt;code&gt;Single Page Application&lt;/code&gt;: a web app that navigates within a single page) or marketing pages, it may skew closer to the average value (82,221.7 tokens / page).&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an Adapter? A contract that encapsulates site-specific differences
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;Adapter&lt;/code&gt; is a configuration plus a set of rules that captures "for this &lt;code&gt;host&lt;/code&gt; (a domain like &lt;code&gt;example.com&lt;/code&gt;), extract data this way."&lt;/p&gt;

&lt;p&gt;The important point is that the adapter remains not as &lt;strong&gt;LLM reasoning&lt;/strong&gt;, but as an &lt;strong&gt;extraction contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For an &lt;code&gt;article&lt;/code&gt; page such as a blog post, these are the typical fields you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt; (title)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;author&lt;/code&gt; (author)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;published&lt;/code&gt; (publication datetime)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also make the extraction &lt;code&gt;strategy&lt;/code&gt; explicit - which information source should be prioritized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://json-ld.org/" rel="noopener noreferrer"&gt;JSON-LD&lt;/a&gt; (&lt;code&gt;JSON for Linking Data&lt;/code&gt;: structured metadata embeddable in HTML)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ogp.me/" rel="noopener noreferrer"&gt;Open Graph protocol&lt;/a&gt; (&lt;code&gt;OG&lt;/code&gt;: a meta tag specification for social sharing) and ordinary &lt;code&gt;meta&lt;/code&gt; tags&lt;/li&gt;
&lt;li&gt;CSS selectors (&lt;code&gt;CSS selector&lt;/code&gt;: a notation for targeting HTML elements)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another practical point is the ability to determine mechanically whether something has broken.&lt;/p&gt;

&lt;p&gt;This is where a DOM fingerprint comes in (&lt;code&gt;DOM fingerprint&lt;/code&gt;: a signature of DOM structure). During training, you save structural features of the DOM. At runtime, if the current page deviates from that signature, you treat it as &lt;code&gt;drift&lt;/code&gt; (structural change) and send it to retraining.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Tool/CLI? A one-line "web interface"
&lt;/h2&gt;

&lt;p&gt;You can leave an adapter as-is, but if you want agents to use it, it is easier to lower it all the way down into a CLI (&lt;code&gt;Command Line Interface&lt;/code&gt;: a tool callable from the terminal).&lt;/p&gt;

&lt;p&gt;The ideal is simply: "pass a URL, get back JSON."&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;# Example: extract a rust-lang blog article as structured data (conceptual)&lt;/span&gt;
site-article https://blog.rust-lang.org/2026/03/05/some-post.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have this form, prompt design on the agent side becomes much simpler.&lt;/p&gt;

&lt;p&gt;You can write instructions like: "Call this command, then use only &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;published&lt;/code&gt; from the returned JSON."&lt;/p&gt;

&lt;p&gt;A similar idea exists in web2cli, which turns the Web into commands. If you take that idea - "Every website is a Unix command" - and adapt it for agent operations such as revisits, token usage, and drift, you end up roughly in this direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: running the self-learning skill &lt;code&gt;self-learning-web-adapter&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;From here on, this section gets more "skill-oriented."&lt;/p&gt;

&lt;p&gt;This skill is designed for repeatedly reading the same host. It encapsulates site-specific differences into an adapter and reuses them.&lt;/p&gt;

&lt;p&gt;Its goals are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: after the second run, I do not want to repeat scraping exploration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt;: return URL -&amp;gt; structured JSON (&lt;code&gt;title&lt;/code&gt;/&lt;code&gt;author&lt;/code&gt;/&lt;code&gt;published&lt;/code&gt; + health diagnostics)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prereq&lt;/strong&gt;: Python 3.10+, network reachability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt;: &lt;code&gt;python3 skills/self-learning-web-adapter/scripts/web_adapter_cli.py run &amp;lt;url&amp;gt;&lt;/code&gt; outputs JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1) Setup
&lt;/h3&gt;

&lt;p&gt;If you only want to use the skill, you do not need to clone the repository.&lt;/p&gt;

&lt;p&gt;Adding the skill can be done with &lt;code&gt;npx&lt;/code&gt; (&lt;code&gt;npm package runner&lt;/code&gt;: a mechanism for running a CLI temporarily), which comes with &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add tumf/self-learning-web-adapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dependencies are minimal. For HTML parsing, install &lt;a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc/" rel="noopener noreferrer"&gt;Beautiful Soup&lt;/a&gt; (an HTML parser).&lt;/p&gt;

&lt;p&gt;(Python dependencies are not resolved by &lt;code&gt;npx&lt;/code&gt;, so this part must be installed separately.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; pip
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;beautifulsoup4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: the commands below assume that &lt;code&gt;skills/self-learning-web-adapter/&lt;/code&gt; has been added directly under the current directory. If it was installed elsewhere, just adjust the path.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Prepare training samples (3 or more from the same host)
&lt;/h3&gt;

&lt;p&gt;The rule for this skill is simple: training (&lt;code&gt;learn&lt;/code&gt;) requires "3 or more URLs from the same host."&lt;/p&gt;

&lt;p&gt;For a blog, it is usually safer to choose around three articles from the same author or category.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Learn -&amp;gt; run to freeze the behavior
&lt;/h3&gt;

&lt;p&gt;If you pass representative pages from the same host, the adapter is saved to &lt;code&gt;adapter_registry/&amp;lt;host&amp;gt;.json&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 skills/self-learning-web-adapter/scripts/web_adapter_cli.py learn &amp;lt;url1&amp;gt; &amp;lt;url2&amp;gt; &amp;lt;url3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once training succeeds, run it against a different URL (a holdout page not used in training).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 skills/self-learning-web-adapter/scripts/web_adapter_cli.py run &amp;lt;holdout-url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output contains not only extraction results, but also diagnostic fields such as &lt;code&gt;signature_known&lt;/code&gt; and &lt;code&gt;extraction_health.score&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is one reason this leans toward a "skill": it turns not only extraction, but also failure handling, into a reusable tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Drift checks and retraining
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;check&lt;/code&gt; returns JSON just like &lt;code&gt;run&lt;/code&gt;, but it is intended to answer: "Does this look like it needs retraining?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 skills/self-learning-web-adapter/scripts/web_adapter_cli.py check &amp;lt;url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;needs_retrain: true&lt;/code&gt; is set, send it through retraining.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 skills/self-learning-web-adapter/scripts/web_adapter_cli.py retrain &amp;lt;host&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5) Export to a web2cli-style command
&lt;/h3&gt;

&lt;p&gt;This is where it starts to feel like a real skill.&lt;/p&gt;

&lt;p&gt;You take a trained adapter and lower it into a single web2cli-style command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 skills/self-learning-web-adapter/scripts/web_adapter_cli.py export-command &amp;lt;host&amp;gt;
python3 skills/self-learning-web-adapter/scripts/web_adapter_cli.py commands
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exported commands are placed in &lt;code&gt;web2cli_commands/&lt;/code&gt;, and &lt;code&gt;web2cli_commands/index.json&lt;/code&gt; becomes the &lt;code&gt;registry&lt;/code&gt; (the command index).&lt;/p&gt;

&lt;p&gt;From the agent’s point of view, this is the moment when "a site has become a tool."&lt;/p&gt;

&lt;h2&gt;
  
  
  Design intuition: how to bias toward skills that work well
&lt;/h2&gt;

&lt;p&gt;The following patterns tend to work well in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Suspect JSON-LD first&lt;/li&gt;
&lt;li&gt;Then fall back to Open Graph and ordinary &lt;code&gt;meta&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Treat CSS selectors as the last escape hatch&lt;/li&gt;
&lt;li&gt;Treat failures not as "exceptions," but as "health checks" (&lt;code&gt;check&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Do not aim for perfection immediately; narrow the fields you want first (start with something like &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;date&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the anti-patterns look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trusting a CSS selector that happened to work on one page, without evidence&lt;/li&gt;
&lt;li&gt;embedding extraction logic in the agent prompt and executing it every time&lt;/li&gt;
&lt;li&gt;fixing breakage as a one-off patch and never preserving it as a learned artifact&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion: turn "reading the Web" into a tooling problem
&lt;/h2&gt;

&lt;p&gt;With the Web→Adapter→Tool→Agent model, scraping changes from "try hard to read the page" into "build a reusable tool."&lt;/p&gt;

&lt;p&gt;This transformation is especially effective for workloads that revisit the same site repeatedly.&lt;/p&gt;

&lt;p&gt;Here are a few concrete next steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick one domain you read often, and narrow the required fields to three (&lt;code&gt;title&lt;/code&gt; / &lt;code&gt;published&lt;/code&gt; / &lt;code&gt;url&lt;/code&gt; is a good start)&lt;/li&gt;
&lt;li&gt;Build a working extractor with the priority order JSON-LD -&amp;gt; &lt;code&gt;meta&lt;/code&gt; -&amp;gt; CSS&lt;/li&gt;
&lt;li&gt;Add a DOM fingerprint and &lt;code&gt;check&lt;/code&gt;, then move toward a design that automatically retrains when it breaks&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tumf/self-learning-web-adapter" rel="noopener noreferrer"&gt;self-learning-web-adapter (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tumf/self-learning-web-adapter/tree/main/skills/self-learning-web-adapter" rel="noopener noreferrer"&gt;self-learning-web-adapter skill directory (&lt;code&gt;skills/self-learning-web-adapter&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/tumf/self-learning-web-adapter/main/skills/self-learning-web-adapter/SKILL.md" rel="noopener noreferrer"&gt;SKILL.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/tumf/self-learning-web-adapter/main/skills/self-learning-web-adapter/references/token-savings.md" rel="noopener noreferrer"&gt;Token Savings (measurement notes)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/tumf/self-learning-web-adapter/main/skills/self-learning-web-adapter/references/adapter-format.md" rel="noopener noreferrer"&gt;Adapter Format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.tumf.dev/posts/diary/2026/3/1/web2cli-every-website-is-a-unix-command/" rel="noopener noreferrer"&gt;This blog: web2cli: Every website is a Unix command - Handle "work that can be done with HTTP alone" before browser automation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jb41/web2cli" rel="noopener noreferrer"&gt;web2cli (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://json-ld.org/" rel="noopener noreferrer"&gt;JSON-LD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://schema.org/" rel="noopener noreferrer"&gt;Schema.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ogp.me/" rel="noopener noreferrer"&gt;The Open Graph protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc/" rel="noopener noreferrer"&gt;Beautiful Soup documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>http</category>
      <category>github</category>
    </item>
    <item>
      <title>Summary of the Web3 Industry in 2025: Technologies Implemented as Products</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Fri, 06 Feb 2026 00:43:51 +0000</pubDate>
      <link>https://forem.com/tumf/summary-of-the-web3-industry-in-2025-technologies-implemented-as-products-242j</link>
      <guid>https://forem.com/tumf/summary-of-the-web3-industry-in-2025-technologies-implemented-as-products-242j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-01-02&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/1/2/web3-industry-2025-recap/" rel="noopener noreferrer"&gt;Web3業界2025年総括: プロダクトとして実装された技術たち&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Looking back at the &lt;a href="https://blog.tumf.dev/tags/blockchain/" rel="noopener noreferrer"&gt;Blockchain&lt;/a&gt; industry in 2025, the most symbolic development was that "&lt;strong&gt;technologies became tangible as products&lt;/strong&gt;."&lt;/p&gt;

&lt;p&gt;After long discussions, Account Abstraction was implemented on the mainnet as &lt;strong&gt;EIP-7702&lt;/strong&gt;, fragmented Layer 2 solutions connected as &lt;strong&gt;Superchain&lt;/strong&gt;, and DeFi integrated application layers through &lt;strong&gt;Hooks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, we will focus on the &lt;strong&gt;technical implementations that operated on the mainnet and transformed user experiences&lt;/strong&gt;, rather than on "specification formulation" or "testnets," as we reflect on 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  January: Bitcoin Evolves into a "Payment + Asset" Layer
&lt;/h2&gt;

&lt;p&gt;2025 began with Bitcoin evolving from a mere "digital gold" into an infrastructure capable of practical asset payments on the Lightning Network.&lt;/p&gt;

&lt;h3&gt;
  
  
  January: Full Operation of Taproot Assets on Mainnet
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://docs.lightning.engineering/the-lightning-network/taproot-assets" rel="noopener noreferrer"&gt;Taproot Assets&lt;/a&gt; developed by Lightning Labs began to be supported by major wallets (such as Strike and Phoenix).&lt;/p&gt;

&lt;p&gt;The technical highlight is that it became possible to &lt;strong&gt;embed asset metadata within the Taproot script tree while maintaining Bitcoin's UTXO model&lt;/strong&gt;, allowing it to be treated as state transitions on Lightning channels. This enabled users to enjoy the experience of "paying Gas fees in BTC while instantly settling stablecoins" on &lt;strong&gt;Bitcoin-native security&lt;/strong&gt;, rather than relying on L2 or sidechains.&lt;/p&gt;

&lt;h2&gt;
  
  
  February: The "Wall" Between Layer 2s Technically Disappears
&lt;/h2&gt;

&lt;p&gt;February was the month when the fragmentation between Layer 2s began to be resolved at the protocol level, rather than through bridges or external services.&lt;/p&gt;

&lt;h3&gt;
  
  
  February: Implementation of Optimism Superchain Interoperability
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://www.optimism.io/" rel="noopener noreferrer"&gt;Optimism&lt;/a&gt; ecosystem (including Base, Zora, Mode, etc.) activated native interoperability features.&lt;/p&gt;

&lt;p&gt;Unlike traditional "Lock &amp;amp; Mint" bridges, this was achieved through a design where &lt;strong&gt;all OP Stack chains share a single bridge contract on L1&lt;/strong&gt;. As a result, users could complete &lt;strong&gt;cross-chain transactions&lt;/strong&gt; with just one click, such as purchasing NFTs on Zora using USDC on Base, without even being aware of "switching chains" on their wallets.&lt;/p&gt;

&lt;h2&gt;
  
  
  March: DeFi Incorporates "Apps"
&lt;/h2&gt;

&lt;p&gt;March was the month when DeFi protocols evolved from mere "exchanges" to "execution environments for financial logic."&lt;/p&gt;

&lt;h3&gt;
  
  
  March: Emergence of Uniswap v4 "Hooks" Ecosystem
&lt;/h3&gt;

&lt;p&gt;A few months after the release of &lt;a href="https://uniswap.org/" rel="noopener noreferrer"&gt;Uniswap v4&lt;/a&gt;, pools utilizing the true value of &lt;strong&gt;Hooks&lt;/strong&gt; began to operate one after another.&lt;/p&gt;

&lt;p&gt;Technically, this involves a mechanism to &lt;strong&gt;call external contracts at timing such as &lt;code&gt;beforeSwap&lt;/code&gt;, &lt;code&gt;afterSwap&lt;/code&gt;, and &lt;code&gt;beforeModifyPosition&lt;/code&gt;&lt;/strong&gt; during pool creation. By March 2025, the following Hooks were put into practical use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TWAMM Hook&lt;/strong&gt;: Automatically time-distributes large orders to minimize price impact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit Order Hook&lt;/strong&gt;: On-chain limit orders managed by the pool itself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Fee Hook&lt;/strong&gt;: Automatically adjusts swap fees based on volatility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a result, DEXs evolved from "automated vending machines without order books" to "programmable liquidity layers" with functionalities comparable to CEXs (centralized exchanges).&lt;/p&gt;

&lt;h2&gt;
  
  
  April: Revolution in Wallet Experience (Pectra Upgrade)
&lt;/h2&gt;

&lt;p&gt;April saw the large Ethereum upgrade "&lt;strong&gt;Pectra&lt;/strong&gt;" (Prague-Electra) applied to the mainnet, fundamentally changing the nature of wallets.&lt;/p&gt;

&lt;h3&gt;
  
  
  April: Smart Account Transformation of EOA via EIP-7702
&lt;/h3&gt;

&lt;p&gt;The highlight of Pectra was the introduction of &lt;a href="https://eips.ethereum.org/EIPS/eip-7702" rel="noopener noreferrer"&gt;EIP-7702&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This feature allows &lt;strong&gt;temporary "setting" of smart contract code only during transaction execution&lt;/strong&gt; for existing EOAs (such as standard addresses like Metamask). As a result, users could immediately utilize the following functionalities without moving assets to a new smart contract wallet (SCW):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gas fee sponsorship&lt;/strong&gt;: The application side bears the Gas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch processing&lt;/strong&gt;: Execute approval (Approve) and swap in one signature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session keys&lt;/strong&gt;: Issuance of temporary keys that permit only specific operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This moment marked the technical resolution of the biggest hurdle of "having to recreate wallets."&lt;/p&gt;

&lt;h2&gt;
  
  
  May: Marketization of Shared Security
&lt;/h2&gt;

&lt;p&gt;May was the month when security itself began to circulate as a "product."&lt;/p&gt;

&lt;h3&gt;
  
  
  May: EigenLayer AVS Goes Live
&lt;/h3&gt;

&lt;p&gt;Multiple &lt;strong&gt;AVS&lt;/strong&gt; (Actively Validated Services) began mainnet operations on &lt;a href="https://www.eigenlayer.xyz/" rel="noopener noreferrer"&gt;EigenLayer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ethereum validators reused (Restaking) their staked ETH to also secure other services. In May, not only &lt;strong&gt;EigenDA&lt;/strong&gt; (data availability layer) but also decentralized sequencers, oracles, and bridge monitoring networks began operating as AVS, establishing a pattern of building "middleware with Ethereum-level security without gathering unique validator sets."&lt;/p&gt;

&lt;h2&gt;
  
  
  June: "Programming" of RWA
&lt;/h2&gt;

&lt;p&gt;June was the month when real-world assets (RWA) were not only tokenized but also incorporated as building blocks in DeFi.&lt;/p&gt;

&lt;h3&gt;
  
  
  June: BlackRock BUIDL's DeFi Integration
&lt;/h3&gt;

&lt;p&gt;BlackRock's tokenized fund "&lt;a href="https://www.blackrock.com/us/individual/solutions/digital-assets" rel="noopener noreferrer"&gt;BUIDL&lt;/a&gt;" became available for atomic swaps with stablecoins like USDC and as collateral in lending protocols.&lt;/p&gt;

&lt;p&gt;Technically, it is a &lt;strong&gt;permissioned token&lt;/strong&gt; that allows interaction with whitelisted smart contracts (DEX pools and lending pools). This enabled a workflow for institutional investors to "earn yields from U.S. Treasury bonds while being able to instantly liquidate and redirect to crypto investments as needed," all on-chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  July: "In-App Apps" in Decentralized Social Networks
&lt;/h2&gt;

&lt;p&gt;July was the month when social media transformed from "a place to view posts" to "a place to use apps."&lt;/p&gt;

&lt;h3&gt;
  
  
  July: Adoption of Farcaster Frames v2
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Frames v2&lt;/strong&gt; extension of the decentralized SNS protocol &lt;a href="https://www.farcaster.xyz/" rel="noopener noreferrer"&gt;Farcaster&lt;/a&gt; became widespread.&lt;/p&gt;

&lt;p&gt;This standard (an extension of OpenGraph tags) allows &lt;strong&gt;interactive mini-apps&lt;/strong&gt; to be embedded within posts on feeds. By July, users could complete actions like "minting NFTs," "playing games," "voting in polls," and "making small payments" within the feed, enabling Web3 actions without switching apps. The wallet signing process was also integrated within the Frame, significantly reducing UX friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  August: Establishing Reliability in Off-Chain Computation
&lt;/h2&gt;

&lt;p&gt;August marked the practical stage of technologies that verify computations done outside the blockchain.&lt;/p&gt;

&lt;h3&gt;
  
  
  August: Expansion of ZK Coprocessor Adoption
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ZK Coprocessors&lt;/strong&gt; (zero-knowledge coprocessors) such as &lt;a href="https://www.axiom.xyz/" rel="noopener noreferrer"&gt;Axiom&lt;/a&gt; and &lt;a href="https://brevis.network/" rel="noopener noreferrer"&gt;Brevis&lt;/a&gt; were adopted by major DeFi protocols.&lt;/p&gt;

&lt;p&gt;These coprocessors perform aggregations of all past transaction histories and complex calculations off-chain, submitting only the &lt;strong&gt;ZK proof&lt;/strong&gt; that the results are correct on-chain. This made it possible to implement logic that was previously gas-prohibitive for traditional smart contracts, such as "applying VIP rates based on transaction volume over the past year" and "calculating complex derivative prices."&lt;/p&gt;

&lt;h2&gt;
  
  
  September: Proof of Parallel Processing EVM's Capabilities
&lt;/h2&gt;

&lt;p&gt;September saw the emergence of implementations that broke through the performance limits of the EVM (Ethereum Virtual Machine).&lt;/p&gt;

&lt;h3&gt;
  
  
  September: Monad's Mainnet Launch
&lt;/h3&gt;

&lt;p&gt;The L1 chain &lt;a href="https://monad.xyz/" rel="noopener noreferrer"&gt;Monad&lt;/a&gt;, characterized by parallel execution EVM, was launched.&lt;/p&gt;

&lt;p&gt;Monad achieved &lt;strong&gt;10,000 TPS&lt;/strong&gt; while maintaining compatibility with existing Ethereum tools through optimistic parallel execution and asynchronous I/O access. This proved that use cases such as high-frequency trading (HFT) and on-chain games, which were previously only possible on non-EVM chains like Solana, could now be realized within the EVM ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  October: Establishment of Intent-Centric Architecture
&lt;/h2&gt;

&lt;p&gt;October was the month when users were liberated from "creating transactions."&lt;/p&gt;

&lt;h3&gt;
  
  
  October: Standardization of UniswapX / CowSwap
&lt;/h3&gt;

&lt;p&gt;Formats represented by &lt;a href="https://uniswap.org/whitepaper-uniswapx.pdf" rel="noopener noreferrer"&gt;UniswapX&lt;/a&gt; and &lt;a href="https://cow.fi/" rel="noopener noreferrer"&gt;CowSwap&lt;/a&gt; emerged, where users only sign their &lt;strong&gt;intent&lt;/strong&gt; (e.g., "I want to swap token A for B") and delegate the actual route exploration and gas payment to a third party known as a solver.&lt;/p&gt;

&lt;p&gt;Technically, the adoption of standard specifications like &lt;a href="https://eips.ethereum.org/EIPS/eip-7683" rel="noopener noreferrer"&gt;ERC-7683&lt;/a&gt; (Cross Chain Intents) progressed, creating an environment where solvers could find and execute optimal routes even across different chains. Users no longer needed to worry about "which chain has gas."&lt;/p&gt;

&lt;h2&gt;
  
  
  November: Starknet's Quantum Resistance and Throughput
&lt;/h2&gt;

&lt;p&gt;November was the month when Starknet, a leader in ZK-Rollups, implemented significant performance improvements.&lt;/p&gt;

&lt;h3&gt;
  
  
  November: Starknet v0.14 "Quantum Leap"
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.starknet.io/" rel="noopener noreferrer"&gt;Starknet&lt;/a&gt; conducted a major upgrade, implementing parallel processing for sequencers and optimizing proof generation. As a result, transaction costs dropped even further from when EIP-4844 was introduced, making micro-payments and on-chain games (Autonomous Worlds) feasible at realistic costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  December: Towards the Next Phase of Ethereum (Glamsterdam)
&lt;/h2&gt;

&lt;p&gt;December was the month when specifications for the upcoming "Glamsterdam" upgrade, scheduled for 2026, were solidified. (&lt;a href="https://blog.tumf.dev/posts/diary/2025/12/12/ethereum-glamsterdam-upgrade-2026/" rel="noopener noreferrer"&gt;For a detailed preview article, click here&lt;/a&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  December: Agreement on Implementation of ePBS (EIP-7732)
&lt;/h3&gt;

&lt;p&gt;The implementation details of &lt;strong&gt;ePBS&lt;/strong&gt; (Enshrined Proposer-Builder Separation / &lt;a href="https://eips.ethereum.org/EIPS/eip-7732" rel="noopener noreferrer"&gt;EIP-7732&lt;/a&gt;), which will be central to the next upgrade, were agreed upon.&lt;/p&gt;

&lt;p&gt;This will incorporate the separation of block construction, which currently relies on external software like MEV-Boost, at the protocol level (Enshrine). This will enhance censorship resistance and simplify the role of validators. By the end of 2025, development of client implementations based on this specification began in earnest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: UX is "Concealed," and Infrastructure is "Integrated"
&lt;/h2&gt;

&lt;p&gt;Reflecting on Web3 technologies in 2025, it was a year where &lt;strong&gt;technical implementations aimed at "concealing complexity from users"&lt;/strong&gt; came together. Notably, the establishment of L2s and the proliferation of Intents significantly reduced &lt;strong&gt;the opportunities for users to be concerned about gas fees&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EIP-7702&lt;/strong&gt;: Abstraction of private key management and gas fee payments (easing the burden on applications)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Superchain / AggLayer&lt;/strong&gt;: Concealing boundaries between chains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intents&lt;/strong&gt;: Concealing transaction failure risks and gas management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks&lt;/strong&gt;: Encapsulating the complexity of financial products behind the scenes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2026, consumer-facing applications (Consumer Crypto) offering experiences comparable to Web2 apps will flourish on top of these "invisible infrastructures."&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-7702" rel="noopener noreferrer"&gt;EIP-7702: Set EOA account code for one transaction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.uniswap.org/contracts/v4/concepts/overview" rel="noopener noreferrer"&gt;Uniswap v4 Core Concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.optimism.io/superchain" rel="noopener noreferrer"&gt;Optimism Superchain Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.eigenlayer.xyz/" rel="noopener noreferrer"&gt;EigenLayer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.farcaster.xyz/learn/what-is-farcaster/frames" rel="noopener noreferrer"&gt;Farcaster Frames Spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.monad.xyz/" rel="noopener noreferrer"&gt;Monad Technical Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-7732" rel="noopener noreferrer"&gt;EIP-7732: Enshrined Proposer-Builder Separation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>web3</category>
      <category>casual</category>
    </item>
    <item>
      <title>Bold Predictions for 2026 from the Intersection of AI and Web3: The Era of Agents with Wallets</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Fri, 06 Feb 2026 00:42:45 +0000</pubDate>
      <link>https://forem.com/tumf/bold-predictions-for-2026-from-the-intersection-of-ai-and-web3-the-era-of-agents-with-wallets-5ac7</link>
      <guid>https://forem.com/tumf/bold-predictions-for-2026-from-the-intersection-of-ai-and-web3-the-era-of-agents-with-wallets-5ac7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-01-03&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/1/3/ai-web3-2026-bold-predictions/" rel="noopener noreferrer"&gt;AI×Web3から見える2026年の大胆予測: エージェントがウォレットを持つ時代&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reflecting on the AI and Web3 industries in 2025, each has undergone a unique evolution. However, what will truly become interesting in 2026 is when these two domains &lt;strong&gt;begin to interact&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;AI agents will operate smart contracts, on-chain AI will run on decentralized infrastructure, and the token economy will accelerate AI development. With the technological foundations established by 2025, 2026 will be the year when this fusion transitions from "experimentation" to "practical application."&lt;/p&gt;

&lt;p&gt;In this article, we will boldly predict the "future where AI and Web3 intersect," taking into account the recap of the AI industry in 2025 (&lt;a href="https://blog.tumf.dev/posts/diary/2026/1/1/ai-industry-2025-recap/" rel="noopener noreferrer"&gt;A tumultuous year that began with the DeepSeek shock&lt;/a&gt;) and the recap of the Web3 industry (&lt;a href="https://blog.tumf.dev/posts/diary/2026/1/2/web3-industry-2025-recap/" rel="noopener noreferrer"&gt;Technologies implemented as products&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Prediction 1: AI Agents Will Have Wallets and Conduct Economic Activities Autonomously ★★★★☆
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Technological Foundation is Ready
&lt;/h3&gt;

&lt;p&gt;By 2025, the necessary elements for the fusion of AI agents and blockchain have been established.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preparations on the AI Side:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ability for agents to utilize multiple tools (e.g., &lt;a href="https://blog.tumf.dev/tags/mcp/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;, &lt;a href="https://github.com/a2aproject/A2A" rel="noopener noreferrer"&gt;A2A Protocol&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Long-duration autonomous execution (Copilot agent mode, Kiro autonomous agent)&lt;/li&gt;
&lt;li&gt;Default inference capabilities (o3, Claude 4, Gemini 2.5, GPT-5)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Preparations on the Web3 Side:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Revolutionizing the wallet experience (Smart account for EOAs via EIP-7702)&lt;/li&gt;
&lt;li&gt;Abstraction of gas fees (Sponsoring, session keys)&lt;/li&gt;
&lt;li&gt;Simplification of cross-chain transactions (Superchain Interoperability, Intents)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What Will Happen in 2026
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q1-Q2: Introduction of Agent-Specific Wallet Protocols&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Based on existing smart accounts (&lt;a href="https://eips.ethereum.org/EIPS/eip-7702" rel="noopener noreferrer"&gt;EIP-7702&lt;/a&gt;, &lt;a href="https://eips.ethereum.org/EIPS/eip-4337" rel="noopener noreferrer"&gt;ERC-4337&lt;/a&gt;), &lt;strong&gt;agent-specific wallet specifications&lt;/strong&gt; will emerge. Key features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Budget Limits&lt;/strong&gt;: Ability to set daily spending caps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist&lt;/strong&gt;: Interaction only with specific smart contracts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Logs&lt;/strong&gt;: All transactions can be reviewed by humans&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emergency Stop&lt;/strong&gt;: Ability to halt operations immediately in case of issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Q3-Q4: Expansion of Use Cases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Examples of economic activities conducted autonomously by agents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DeFi Automated Operations&lt;/strong&gt;: Moving funds across multiple protocols for yield optimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NFT Proxy Purchases&lt;/strong&gt;: Bidding and purchasing on &lt;a href="https://opensea.io/" rel="noopener noreferrer"&gt;OpenSea&lt;/a&gt; or Blur based on user instructions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Sales&lt;/strong&gt;: Selling reports or models generated by agents on-chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Reward Distribution&lt;/strong&gt;: Automatically distributing rewards for tasks completed by multiple agents&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Factors Accelerating Realization
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Agent Collaboration via Model Context Protocol (MCP)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In December 2025, Anthropic donated MCP to the &lt;a href="https://www.linuxfoundation.org/press/linux-foundation-announces-the-formation-of-the-agentic-ai-foundation" rel="noopener noreferrer"&gt;Agentic AI Foundation under the Linux Foundation&lt;/a&gt;. This will standardize wallet operations as an MCP server, allowing unified access from multiple AI agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flexibility of EIP-7702&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EIP-7702 allows existing EOAs to apply smart contract code "only during transaction execution." This enables agents to have &lt;strong&gt;advanced permissions only when necessary&lt;/strong&gt;, minimizing security risks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prediction 2: Practical Implementation of On-Chain AI Inference Market ★★★☆☆
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Maturity of Decentralized AI Infrastructure
&lt;/h3&gt;

&lt;p&gt;In 2025, the proliferation of open-weight models (e.g., &lt;a href="https://www.deepseek.com/" rel="noopener noreferrer"&gt;DeepSeek&lt;/a&gt;, Qwen, Kimi K2, gpt-oss) and inference engines (vLLM, SGLang) made the option to &lt;strong&gt;run AI in-house&lt;/strong&gt; realistic.&lt;/p&gt;

&lt;p&gt;Simultaneously, the Web3 side saw the &lt;strong&gt;marketization of shared security&lt;/strong&gt; (EigenLayer AVS), enabling a system where "Ethereum validators ensure the security of middleware as a side job."&lt;/p&gt;

&lt;h3&gt;
  
  
  What Will Happen in 2026
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q2-Q3: Providing AI Inference as AVS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using the Restaking mechanism of &lt;a href="https://www.eigenlayer.xyz/" rel="noopener noreferrer"&gt;EigenLayer&lt;/a&gt;, AI inference services will emerge as &lt;strong&gt;Actively Validated Services (AVS)&lt;/strong&gt;.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Node operators prepare GPU clusters and host specific models (Qwen3, Llama, etc.) with inference engines (vLLM/SGLang)&lt;/li&gt;
&lt;li&gt;Staked ETH in EigenLayer serves as collateral to guarantee the accuracy of inference results&lt;/li&gt;
&lt;li&gt;Users send inference requests via on-chain and receive results&lt;/li&gt;
&lt;li&gt;Nodes providing fraudulent inferences will be slashed (stake confiscation)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Practical Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DeFi protocols delegating price predictions and risk assessments to on-chain AI&lt;/li&gt;
&lt;li&gt;NFT projects executing image generation in a verifiable on-chain manner&lt;/li&gt;
&lt;li&gt;AI analyzing proposal content in DAO voting and providing summaries to voters&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Realizing Verifiability
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Combination with ZK Proofs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In August 2025, ZK Coprocessors like &lt;a href="https://www.axiom.xyz/" rel="noopener noreferrer"&gt;Axiom&lt;/a&gt; and &lt;a href="https://brevis.network/" rel="noopener noreferrer"&gt;Brevis&lt;/a&gt; were put into practical use. In 2026, this will also apply to AI inference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Executing AI inference off-chain&lt;/li&gt;
&lt;li&gt;Submitting only the &lt;strong&gt;ZK proof&lt;/strong&gt; that the inference was conducted correctly on-chain&lt;/li&gt;
&lt;li&gt;Ensuring verifiability while reducing gas costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will address the challenge of "Can we trust AI outputs?" through the verification mechanisms of blockchain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prediction 3: Token Economy Accelerating AI Development ★★☆☆☆
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Clarification of Data and Model Ownership
&lt;/h3&gt;

&lt;p&gt;Until 2025, the rights concerning training data and trained AI models were ambiguous. In 2026, &lt;strong&gt;clarification of ownership through Blockchain&lt;/strong&gt; will progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q2-Q3: Data NFTs and Model NFTs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data NFTs&lt;/strong&gt;: Tokenizing training datasets and licensing usage rights&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model NFTs&lt;/strong&gt;: Tokenizing trained models and selling access rights to inference APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributor Tokens&lt;/strong&gt;: Automatically distributing rewards to data providers and annotators based on usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example Scenario:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A hospital providing a medical image dataset registers as the owner of the data NFT&lt;/li&gt;
&lt;li&gt;An AI startup trains a model using that data&lt;/li&gt;
&lt;li&gt;Each time the model is commercialized, a smart contract automatically distributes royalties to the hospital&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Sustainability of Open Source AI
&lt;/h3&gt;

&lt;p&gt;In 2025, DeepSeek-R1 and Qwen3 were released as open-source, but recovering development costs remained a challenge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q3-Q4: "Open but Monetized" Model&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model is open-weight (available to anyone)&lt;/li&gt;
&lt;li&gt;For commercial use, a mechanism to &lt;strong&gt;pay licensing fees via on-chain&lt;/strong&gt; is established&lt;/li&gt;
&lt;li&gt;Payments are automated (via smart contracts) and highly transparent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows for a balance between "being open" and "sustainable development."&lt;/p&gt;

&lt;h2&gt;
  
  
  Prediction 4: AI Accelerating UX Improvements in Web3 Products ★★★★★
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenges in 2025
&lt;/h3&gt;

&lt;p&gt;The biggest challenge for Web3 was the &lt;strong&gt;complexity of UX&lt;/strong&gt;. While technical solutions are being developed through EIP-7702 and Intents, psychological barriers remain, such as "not knowing what can be done" and "fear of failure."&lt;/p&gt;

&lt;h3&gt;
  
  
  What Will Happen in 2026
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q2-Q4: AI Assistants Becoming Web3 "Translators"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Case 1: Natural Language DeFi Operations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;User: "Buy $1000 worth of this token and operate it in a safe pool."&lt;/p&gt;

&lt;p&gt;AI Agent (internal processing):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Selecting the optimal DEX (e.g., &lt;a href="https://uniswap.org/" rel="noopener noreferrer"&gt;UniswapX&lt;/a&gt; / &lt;a href="https://cow.fi/" rel="noopener noreferrer"&gt;CowSwap&lt;/a&gt; based on intents)&lt;/li&gt;
&lt;li&gt;Calculating slippage and gas fees&lt;/li&gt;
&lt;li&gt;Presenting to the user for approval&lt;/li&gt;
&lt;li&gt;Executing the transaction (automatically retrying in case of failure)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Use Case 2: Automating Risk Explanations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before executing a smart contract, AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performs static analysis of the contract code&lt;/li&gt;
&lt;li&gt;References past transaction history (via ZK Coprocessors)&lt;/li&gt;
&lt;li&gt;Explains risks in natural language (e.g., "This pool is audited but has a low collateral ratio.")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Case 3: Simplifying Cross-Chain Operations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;User: "I want to buy an NFT on Optimism with USDC from Arbitrum."&lt;/p&gt;

&lt;p&gt;AI Agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Constructs a cross-chain transaction using Superchain Interoperability&lt;/li&gt;
&lt;li&gt;Optimizes gas fees (L2 bridge vs direct swap)&lt;/li&gt;
&lt;li&gt;Completes with a single signature&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Supporting Technologies
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Application of Vibe Coding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In February 2025, Andrej Karpathy proposed &lt;a href="https://www.forbes.com/sites/nishatalagala/2025/03/30/what-is-vibe-coding-and-why-should-you-care/" rel="noopener noreferrer"&gt;Vibe Coding&lt;/a&gt;, emphasizing "conveying intent" over "writing code."&lt;/p&gt;

&lt;p&gt;In 2026, this concept will be applied to Web3, allowing &lt;strong&gt;users to simply express "what they want to do," and agents will construct the optimal transaction.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prediction 5: Implementation of Collective Intelligence through "Decentralized AI + DAO" ★★★☆☆
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI Supporting DAO Decision-Making
&lt;/h3&gt;

&lt;p&gt;By 2025, DAO voting participation rates were low (often below 10%), and the quality of decision-making was a concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q3-Q4: AI-Powered Governance&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Proposal Summarization&lt;/strong&gt;: AI automatically summarizes lengthy proposals, reducing the burden on voters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impact Analysis&lt;/strong&gt;: Running simulations of the financial impact and changes in tokenomics if a proposal is passed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Individual Recommendations&lt;/strong&gt;: Recommending proposals that align with each voter's past voting history&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Implementation Example:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AI like &lt;a href="https://notebooklm.google/" rel="noopener noreferrer"&gt;NotebookLM&lt;/a&gt; analyzes DAO proposals&lt;/li&gt;
&lt;li&gt;A research AI like Deep Research collects related past discussions and external information&lt;/li&gt;
&lt;li&gt;Results are stored on-chain for voter review&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Training Decentralized AI
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q4: Federated Learning × Blockchain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Federated Learning, where multiple nodes collaborate to train AI models, will be managed on the blockchain.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Each node learns with local data (data is not shared)&lt;/li&gt;
&lt;li&gt;Only the learning results (gradients) are submitted on-chain&lt;/li&gt;
&lt;li&gt;Smart contracts aggregate gradients and update the global model&lt;/li&gt;
&lt;li&gt;Token rewards are distributed based on contribution&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;Enables training large models while maintaining data privacy&lt;/li&gt;
&lt;li&gt;Contributions are transparently recorded, making incentive design easier&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion: 2026 Will Be the Year of "Fusion"
&lt;/h2&gt;

&lt;p&gt;By 2025, AI and Web3 have each built a &lt;strong&gt;practical foundation&lt;/strong&gt; in their respective domains.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI has evolved from a "smart answerer" to an "agent that integrates research, operation, and generation."&lt;/li&gt;
&lt;li&gt;Web3 has progressed from "specification formulation" to "products operating on the mainnet."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2026, these two will &lt;strong&gt;interact and create practical value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summary of Predictions:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AI agents will have wallets and conduct economic activities autonomously&lt;/strong&gt; ★★★★☆ (Q1-Q2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical implementation of the on-chain AI inference market&lt;/strong&gt; ★★★☆☆ (Q2-Q3)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token economy accelerating AI development&lt;/strong&gt; ★★☆☆☆ (Q2-Q3)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI accelerating UX improvements in Web3 products&lt;/strong&gt; ★★★★★ (Q2-Q4)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementation of collective intelligence through "decentralized AI + DAO"&lt;/strong&gt; ★★★☆☆ (Q3-Q4)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Of course, these are all "bold predictions." Many technical challenges (security, scalability, regulation) remain.&lt;/p&gt;

&lt;p&gt;However, what we see from the reflections of 2025 is that rather than announcements of "models released" or "specifications decided," it is the &lt;strong&gt;actual implementations that run on the mainnet and change user experiences&lt;/strong&gt; that will drive the next change.&lt;/p&gt;

&lt;p&gt;Let’s witness together the transition of the fusion of AI and Web3 from "experimentation" to "practical application" in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.tumf.dev/posts/diary/2026/1/1/ai-industry-2025-recap/" rel="noopener noreferrer"&gt;AI Industry 2025 Recap: A Tumultuous Year That Began with the DeepSeek Shock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.tumf.dev/posts/diary/2026/1/2/web3-industry-2025-recap/" rel="noopener noreferrer"&gt;Web3 Industry 2025 Recap: Technologies Implemented as Products&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-7702" rel="noopener noreferrer"&gt;EIP-7702: Set EOA account code for one transaction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-4337" rel="noopener noreferrer"&gt;ERC-4337: Account Abstraction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/a2aproject/A2A" rel="noopener noreferrer"&gt;A2A Protocol - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.deepseek.com/" rel="noopener noreferrer"&gt;DeepSeek&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.eigenlayer.xyz/" rel="noopener noreferrer"&gt;EigenLayer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linuxfoundation.org/press/linux-foundation-announces-the-formation-of-the-agentic-ai-foundation" rel="noopener noreferrer"&gt;Agentic AI Foundation - Linux Foundation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.axiom.xyz/" rel="noopener noreferrer"&gt;Axiom - ZK Coprocessor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://brevis.network/" rel="noopener noreferrer"&gt;Brevis Network&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.optimism.io/superchain" rel="noopener noreferrer"&gt;Optimism Superchain Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://notebooklm.google/" rel="noopener noreferrer"&gt;NotebookLM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>casual</category>
    </item>
    <item>
      <title>docker-android: A Docker Environment for Controlling Android Emulators from a Web Browser</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Fri, 06 Feb 2026 00:41:35 +0000</pubDate>
      <link>https://forem.com/tumf/docker-android-a-docker-environment-for-controlling-android-emulators-from-a-web-browser-9lg</link>
      <guid>https://forem.com/tumf/docker-android-a-docker-environment-for-controlling-android-emulators-from-a-web-browser-9lg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-01-04&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/1/4/docker-android-web-browser-remote-control/" rel="noopener noreferrer"&gt;docker-android: WebブラウザからAndroidエミュレータを操作するDocker環境&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/budtmo/docker-android" rel="noopener noreferrer"&gt;docker-android&lt;/a&gt; is an open-source project that allows you to run Android emulators inside a &lt;a href="https://blog.tumf.dev/tags/docker/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; container and control them remotely via a web browser. This enables the automation of &lt;a href="https://blog.tumf.dev/tags/test/" rel="noopener noreferrer"&gt;testing&lt;/a&gt; in CI/CD pipelines and the creation of a scalable Android testing infrastructure in cloud environments without the need to install Android Studio.&lt;/p&gt;

&lt;p&gt;In this article, we will cover an overview of docker-android, how to set it up, basic usage, and practical use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is docker-android?
&lt;/h2&gt;

&lt;p&gt;docker-android is a Docker-based Android emulator environment developed by budtmo. Its main features are as follows:&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web Browser Access&lt;/strong&gt;: Directly control the Android screen from the browser using noVNC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Version Support&lt;/strong&gt;: Supports Android versions from 5.0 to the latest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD Integration&lt;/strong&gt;: Easily integrates with Jenkins, GitLab CI, GitHub Actions, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appium Support&lt;/strong&gt;: Can be integrated with test automation frameworks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Supports scale-out with Kubernetes or Docker Swarm.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;docker-android consists of the following components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TB
    Browser[Webブラウザ]
    noVNC[noVNC Server&amp;lt;br/&amp;gt;Port: 6080]
    VNC[VNC Server]
    Emulator[Android Emulator&amp;lt;br/&amp;gt;AVD]
    Appium[Appium Server&amp;lt;br/&amp;gt;Port: 4723&amp;lt;br/&amp;gt;オプション]
    ADB[ADB Server&amp;lt;br/&amp;gt;Port: 5554/5555]

    Browser --&amp;gt;|HTTP| noVNC
    noVNC --&amp;gt;|VNC Protocol| VNC
    VNC --&amp;gt; Emulator
    Appium --&amp;gt; ADB
    ADB --&amp;gt; Emulator

    style Browser fill:#e1f5ff
    style Emulator fill:#a5d6a7
    style noVNC fill:#fff59d
    style Appium fill:#ffcc80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Main components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android Emulator&lt;/strong&gt;: Android SDK's AVD (Android Virtual Device)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;noVNC&lt;/strong&gt;: A web interface that allows VNC to be accessed from a browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appium Server&lt;/strong&gt;: A WebDriver server for test automation (optional).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ADB (Android Debug Bridge)&lt;/strong&gt;: For debugging and command execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.tumf.dev/tags/docker/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;&lt;/strong&gt;: Version 20.10 or higher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware Virtualization&lt;/strong&gt;: Intel VT-x or AMD-V must be enabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: Minimum of 4GB (8GB or more recommended).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Basic Startup Method
&lt;/h3&gt;

&lt;p&gt;The simplest way to start an Android 11 emulator is with the following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 6080:6080 &lt;span class="nt"&gt;-p&lt;/span&gt; 5554:5554 &lt;span class="nt"&gt;-p&lt;/span&gt; 5555:5555 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; android-container &lt;span class="se"&gt;\&lt;/span&gt;
  budtmo/docker-android:emulator_11.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After starting, accessing &lt;code&gt;http://localhost:6080&lt;/code&gt; in your browser will display the Android screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Port Descriptions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6080&lt;/td&gt;
&lt;td&gt;noVNC (Web Interface)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5554&lt;/td&gt;
&lt;td&gt;ADB Console Port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5555&lt;/td&gt;
&lt;td&gt;ADB Debug Port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4723&lt;/td&gt;
&lt;td&gt;Appium Server (when using Appium image)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Basic Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Operating from a Web Browser
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Access &lt;code&gt;http://localhost:6080&lt;/code&gt; in your browser.&lt;/li&gt;
&lt;li&gt;Click, drag, and swipe on the screen to control Android.&lt;/li&gt;
&lt;li&gt;Keyboard input is also possible.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Operating via ADB
&lt;/h3&gt;

&lt;p&gt;You can access the Android inside the container from the Docker host using ADB.&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;# ADB connection&lt;/span&gt;
adb connect localhost:5555

&lt;span class="c"&gt;# Check device list&lt;/span&gt;
adb devices

&lt;span class="c"&gt;# Install an app&lt;/span&gt;
adb &lt;span class="nb"&gt;install &lt;/span&gt;my-app.apk

&lt;span class="c"&gt;# Shell access&lt;/span&gt;
adb shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration with Docker Compose
&lt;/h3&gt;

&lt;p&gt;If you want to start multiple emulators, using &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; is convenient.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;android-11&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;budtmo/docker-android:emulator_11.0&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6080:6080"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5554:5554"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5555:5555"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DEVICE=Samsung Galaxy S10&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATAPARTITION=4g&lt;/span&gt;
    &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="na"&gt;android-13&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;budtmo/docker-android:emulator_13.0&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6081:6080"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5556:5554"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5557:5555"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DEVICE=Pixel &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATAPARTITION=4g&lt;/span&gt;
    &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Startup command:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This will start two emulators, Android 11 and Android 13, accessible on different ports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Automation with Appium
&lt;/h2&gt;

&lt;p&gt;docker-android also provides an image with an integrated &lt;a href="https://appium.io/" rel="noopener noreferrer"&gt;Appium&lt;/a&gt; server. Appium is an open-source framework for automating mobile application testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting the Appium Image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 6080:6080 &lt;span class="nt"&gt;-p&lt;/span&gt; 4723:4723 &lt;span class="nt"&gt;-p&lt;/span&gt; 5555:5555 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; appium-android &lt;span class="se"&gt;\&lt;/span&gt;
  budtmo/docker-android:emulator_11.0_appium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example Test Code in Python + Appium
&lt;/h3&gt;

&lt;p&gt;Here is an example of Python code to test an Android app using Appium.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;appium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;appium.options.android&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UiAutomator2Options&lt;/span&gt;

&lt;span class="c1"&gt;# Appium server configuration
&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UiAutomator2Options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Android&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;emulator-5554&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/path/to/your/app.apk&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# Connect to Appium server
&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Remote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://localhost:4723&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Example test execution
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Get element and click
&lt;/span&gt;    &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;com.example:id/button&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Text input
&lt;/span&gt;    &lt;span class="n"&gt;input_field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;com.example:id/input&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;input_field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hello, docker-android!&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test successful&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example of Using in CI/CD Pipeline (GitHub Actions)
&lt;/h3&gt;

&lt;p&gt;Here’s an example of running tests using docker-android in &lt;a href="https://blog.tumf.dev/posts/diary/2025/1/6/github-actions-as-cron/" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&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="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Android UI Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start Android emulator&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;docker run -d -p 4723:4723 -p 5555:5555 \&lt;/span&gt;
            &lt;span class="s"&gt;--name android-emulator \&lt;/span&gt;
            &lt;span class="s"&gt;budtmo/docker-android:emulator_11.0_appium&lt;/span&gt;

          &lt;span class="s"&gt;# Wait for the emulator to start&lt;/span&gt;
          &lt;span class="s"&gt;sleep 30&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install appium-python-client pytest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pytest tests/test_android.py&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stop emulator&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker stop android-emulator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Customization via Environment Variables
&lt;/h2&gt;

&lt;p&gt;docker-android allows you to customize the emulator's behavior using the following environment variables.&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;Default Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEVICE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Device model name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Samsung Galaxy S10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATAPARTITION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Data partition size&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2g&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EMULATOR_TIMEOUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Startup timeout (seconds)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;300&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RELAXED_SECURITY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Relax security for Appium&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Usage example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 6080:6080 &lt;span class="nt"&gt;-p&lt;/span&gt; 5555:5555 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DEVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Pixel 6"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DATAPARTITION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4g"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  budtmo/docker-android:emulator_13.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Utilizing in Cloud Environments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Example Configuration on AWS ECS
&lt;/h3&gt;

&lt;p&gt;docker-android can also run in container orchestration environments like &lt;a href="https://aws.amazon.com/ecs/" rel="noopener noreferrer"&gt;AWS ECS&lt;/a&gt; and GCP Cloud Run.&lt;/p&gt;

&lt;p&gt;Here is an example of an AWS ECS task definition (excerpt).&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;"family"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"android-emulator-task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"containerDefinitions"&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;"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;"android-emulator"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"budtmo/docker-android:emulator_11.0_appium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"essential"&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;"portMappings"&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;"containerPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tcp"&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;"containerPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4723&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tcp"&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;"environment"&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;"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;"DEVICE"&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;"Samsung Galaxy S10"&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;"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;"DATAPARTITION"&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;"4g"&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;"requiresCompatibilities"&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;"FARGATE"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"networkMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"awsvpc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2048"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4096"&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;h3&gt;
  
  
  Example Configuration on Kubernetes
&lt;/h3&gt;

&lt;p&gt;Example of a Deployment in &lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&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="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android-emulator&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android-emulator&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android-emulator&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;budtmo/docker-android:emulator_11.0_appium&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6080&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4723&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4Gi"&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
          &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2Gi"&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DEVICE&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pixel&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATAPARTITION&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4g"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android-emulator-service&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android-emulator&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;novnc&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6080&lt;/span&gt;
    &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6080&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appium&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4723&lt;/span&gt;
    &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4723&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LoadBalancer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance and Resource Management
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Resource Requirements
&lt;/h3&gt;

&lt;p&gt;Recommended resources per emulator:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU&lt;/strong&gt;: 2 cores or more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: 4GB or more (8GB recommended for Android 11 and later)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk&lt;/strong&gt;: 10GB or more&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Considerations for Parallel Execution
&lt;/h3&gt;

&lt;p&gt;When starting multiple emulators simultaneously, keep the following points in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid port number conflicts.&lt;/li&gt;
&lt;li&gt;Properly allocate resources on the host machine.&lt;/li&gt;
&lt;li&gt;Set resource limits using Docker's &lt;code&gt;--cpus&lt;/code&gt; and &lt;code&gt;--memory&lt;/code&gt; options.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example of parallel execution:&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;# Emulator 1&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 6080:6080 &lt;span class="nt"&gt;-p&lt;/span&gt; 5555:5555 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt; &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4g"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; android-1 &lt;span class="se"&gt;\&lt;/span&gt;
  budtmo/docker-android:emulator_11.0

&lt;span class="c"&gt;# Emulator 2&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 6081:6080 &lt;span class="nt"&gt;-p&lt;/span&gt; 5556:5555 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt; &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4g"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; android-2 &lt;span class="se"&gt;\&lt;/span&gt;
  budtmo/docker-android:emulator_11.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  If the Emulator Fails to Start
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Hardware virtualization is disabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Enable Intel VT-x or AMD-V in the BIOS. For Linux, check if KVM is available.&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 if KVM is available&lt;/span&gt;
egrep &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'(vmx|svm)'&lt;/span&gt; /proc/cpuinfo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  If noVNC Cannot Connect
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Port mapping is incorrect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Verify that the ports are correctly mapped with the &lt;code&gt;-p&lt;/code&gt; option.&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 port usage&lt;/span&gt;
docker port android-container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Memory Insufficient Error
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Insufficient memory allocated to the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Increase memory with the &lt;code&gt;--memory&lt;/code&gt; option or reduce the &lt;code&gt;DATAPARTITION&lt;/code&gt; environment variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 6080:6080 &lt;span class="nt"&gt;-p&lt;/span&gt; 5555:5555 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"8g"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DATAPARTITION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2g"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  budtmo/docker-android:emulator_11.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Comparison with Android Studio Emulator
&lt;/h2&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;docker-android&lt;/th&gt;
&lt;th&gt;Android Studio Emulator&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ease of Setup&lt;/td&gt;
&lt;td&gt;◎ (Only Docker)&lt;/td&gt;
&lt;td&gt;△ (Requires Android Studio)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD Integration&lt;/td&gt;
&lt;td&gt;◎ (Easy)&lt;/td&gt;
&lt;td&gt;△ (Complex setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote Access&lt;/td&gt;
&lt;td&gt;◎ (Via browser)&lt;/td&gt;
&lt;td&gt;× (Requires VNC, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;○ (Slightly slower)&lt;/td&gt;
&lt;td&gt;◎ (Native-like performance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;◎ (Containerized)&lt;/td&gt;
&lt;td&gt;△ (Manual management)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU Support&lt;/td&gt;
&lt;td&gt;△ (Limited support*)&lt;/td&gt;
&lt;td&gt;◎ (Full support)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;GPU passthrough configuration is required in container environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While docker-android is suitable for automated testing in CI/CD and cloud environments, the &lt;a href="https://developer.android.com/studio/run/emulator" rel="noopener noreferrer"&gt;Android Studio Emulator&lt;/a&gt; performs better for local development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Using docker-android significantly simplifies the setup of Android emulator environments, making it easy to automate testing in CI/CD pipelines and build scalable testing infrastructures in cloud environments.&lt;/p&gt;

&lt;p&gt;Key benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No need for Android Studio, allowing control of Android from a browser.&lt;/li&gt;
&lt;li&gt;High reproducibility of environments as it can be managed as a Docker container.&lt;/li&gt;
&lt;li&gt;Powerful test automation can be achieved in combination with Appium.&lt;/li&gt;
&lt;li&gt;Scalable with Kubernetes or ECS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are considering automated testing in CI/CD or building an Android testing infrastructure in cloud environments, be sure to give it a try.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.tumf.dev/posts/diary/2025/1/6/github-actions-as-cron/" rel="noopener noreferrer"&gt;Using GitHub Actions as a cron job&lt;/a&gt; - How to set up periodic execution in CI/CD.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.tumf.dev/posts/diary/2025/12/11/bucketeer-feature-flag-platform/" rel="noopener noreferrer"&gt;Bucketeer: CyberAgent's Feature Flag Platform&lt;/a&gt; - An example of building a development environment using Docker Compose.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.tumf.dev/posts/diary/2025/12/28/engineer-year-end-cleanup-part2/" rel="noopener noreferrer"&gt;Year-End Cleanup for Engineers (Part 2)&lt;/a&gt; - About removing unnecessary resources in Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/budtmo/docker-android" rel="noopener noreferrer"&gt;docker-android GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://appium.io/" rel="noopener noreferrer"&gt;Official Appium Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://appium.io/docs/en/latest/" rel="noopener noreferrer"&gt;Official Appium Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/novnc/noVNC" rel="noopener noreferrer"&gt;noVNC Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/studio/command-line/adb" rel="noopener noreferrer"&gt;Android Debug Bridge (adb) Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/studio/run/emulator" rel="noopener noreferrer"&gt;Official Android Emulator Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Official Docker Compose Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Official Kubernetes Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/actions" rel="noopener noreferrer"&gt;Official GitHub Actions Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Vibium: A Browser Automation Tool Optimized for AI Agents Over Playwright</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Fri, 06 Feb 2026 00:40:08 +0000</pubDate>
      <link>https://forem.com/tumf/vibium-a-browser-automation-tool-optimized-for-ai-agents-over-playwright-5hai</link>
      <guid>https://forem.com/tumf/vibium-a-browser-automation-tool-optimized-for-ai-agents-over-playwright-5hai</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-01-05&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/1/5/vibium-selenium-creator-browser-automation/" rel="noopener noreferrer"&gt;Vibium: PlaywrightよりAIエージェントに最適化されたブラウザ自動化ツール&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Jason Huggins, the creator of Selenium, has announced a new browser automation tool, &lt;a href="https://github.com/VibiumDev/vibium" rel="noopener noreferrer"&gt;Vibium&lt;/a&gt;, which comes approximately 20 years after Selenium. In this article, we will discuss Vibium's design philosophy, its differences from Playwright and Puppeteer, and why a new tool was necessary in the era of AI agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Vibium?
&lt;/h2&gt;

&lt;p&gt;Vibium is a browser automation infrastructure designed for &lt;a href="https://blog.tumf.dev/tags/ai/" rel="noopener noreferrer"&gt;AI&lt;/a&gt; agents. All of the following features are integrated into a single binary of about 10MB:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser Lifecycle Management&lt;/strong&gt;: Detection and launching of Chrome&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebDriver BiDi Proxy&lt;/strong&gt;: Communication with the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://blog.tumf.dev/tags/mcp/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; Server&lt;/strong&gt;: Integration with LLM agents (like Claude Code)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Waiting&lt;/strong&gt;: Polling until elements appear&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots&lt;/strong&gt;: PNG captures of the viewport&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The standout feature is that integration with &lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; can be completed with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add vibium &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; vibium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this single line, Claude Code can directly manipulate the browser. Chrome is automatically downloaded, eliminating the need for manual setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Selenium to Vibium: 20 Years of Evolution
&lt;/h2&gt;

&lt;p&gt;Jason Huggins created &lt;a href="https://www.selenium.dev/" rel="noopener noreferrer"&gt;Selenium&lt;/a&gt; in 2004, paving the way for browser automation. Since then, the industry has evolved with Selenium WebDriver, Puppeteer, and Playwright, but what prompted Huggins to create a tool again?&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenges with Existing Tools
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Selenium WebDriver&lt;/strong&gt; (2011 onwards) is mature but has the following issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex setup (driver management, browser version compatibility)&lt;/li&gt;
&lt;li&gt;Boilerplate code required for element waiting&lt;/li&gt;
&lt;li&gt;Lack of consideration for integration with AI agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; (2020 onwards) and &lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;Puppeteer&lt;/a&gt; (2018 onwards) addressed these issues using the Chrome DevTools Protocol (CDP). However:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CDP is a Chrome-specific protocol (not standardized)&lt;/li&gt;
&lt;li&gt;Additional abstraction layers are needed to support multiple browsers&lt;/li&gt;
&lt;li&gt;MCP server functionality needs to be implemented separately&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Choice of WebDriver BiDi
&lt;/h3&gt;

&lt;p&gt;Vibium adopts the &lt;a href="https://w3c.github.io/webdriver-bidi/" rel="noopener noreferrer"&gt;WebDriver BiDi&lt;/a&gt; protocol. This is a next-generation protocol being developed as a W3C standard, combining the best aspects of Selenium WebDriver and CDP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional Communication&lt;/strong&gt;: Real-time reception of events from the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardization&lt;/strong&gt;: Works across Chrome, Firefox, and Safari (by specification)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-Level Access&lt;/strong&gt;: Direct access to network, console, and DOM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Huggins stated in an interview on the &lt;a href="https://testguild.com/podcast/automation/a559-jason/" rel="noopener noreferrer"&gt;TestGuild Podcast&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WebDriver BiDi is a protocol that has learned all the lessons from CDP that made Puppeteer and Playwright great.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Create Vibium Instead of Using Playwright?
&lt;/h2&gt;

&lt;p&gt;So, why create a new tool instead of using Playwright? The primary reason is &lt;strong&gt;differences in design philosophy&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. AI Agent-First Design
&lt;/h3&gt;

&lt;p&gt;Vibium is optimized for AI agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Built-in MCP Server&lt;/strong&gt;: Instant integration with Claude Code, Gemini, and local LLMs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stdio Communication&lt;/strong&gt;: Conforms to the standard communication protocol for LLM agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple API&lt;/strong&gt;: Minimal methods that are easy for AI to understand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In contrast, Playwright is designed for human test engineers, requiring separate implementation for MCP integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Zero Setup Philosophy
&lt;/h3&gt;

&lt;p&gt;The design goal of Vibium is to be "&lt;strong&gt;invisible binary&lt;/strong&gt;":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Just running npm install vibium makes this work&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;browserSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vibium&lt;/span&gt;&lt;span class="dl"&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;vibe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;browserSync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Downloading the browser, placing drivers, and setting paths are all automated. This emphasizes a developer experience that prioritizes "getting it running first" in the AI era.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Simplicity of a Single Binary
&lt;/h3&gt;

&lt;p&gt;Vibium achieves everything with a single Go binary of about 10MB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                         LLM / Agent                         │
│          (Claude Code, Codex, Gemini, Local Models)         │
└─────────────────────────────────────────────────────────────┘
                      ▲
                      │ MCP Protocol (stdio)
                      ▼
           ┌─────────────────────┐
           │   Vibium Clicker    │
           │                     │
           │  ┌───────────────┐  │
           │  │  MCP Server   │  │
           │  └───────▲───────┘  │         ┌──────────────────┐
           │          │          │         │                  │
           │  ┌───────▼───────┐  │WebSocket│                  │
           │  │  BiDi Proxy   │  │◄───────►│  Chrome Browser  │
           │  └───────────────┘  │  BiDi   │                  │
           │                     │         │                  │
           └─────────────────────┘         └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright consists of multiple npm packages and browser binaries, leading to complex dependencies. Vibium chose simplicity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Use Cases for Vibium
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Using as an AI Agent
&lt;/h3&gt;

&lt;p&gt;After integration with Claude Code, you can issue commands in natural language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Go to example.com and click the first link"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code will automatically invoke the following MCP tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browser_launch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Launch the browser (visible by default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browser_navigate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Navigate to a URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browser_find&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Find elements using CSS selectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browser_click&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Click an element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browser_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Input text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browser_screenshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Take a screenshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browser_quit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Close the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Using as a JavaScript Library
&lt;/h3&gt;

&lt;p&gt;You can also use it directly as an npm package:&lt;/p&gt;

&lt;h4&gt;
  
  
  Synchronous API (REPL Friendly)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&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;browserSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vibium&lt;/span&gt;&lt;span class="dl"&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;vibe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;browserSync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&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;png&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshot.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;png&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Asynchronous API
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&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;browser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vibium&lt;/span&gt;&lt;span class="dl"&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;vibe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&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;png&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshot.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;png&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Automatic Waiting Mechanism
&lt;/h3&gt;

&lt;p&gt;Vibium automatically waits until elements are displayed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This will automatically poll until the element appears&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button.submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Playwright and Selenium, explicit wait code was necessary, but Vibium waits intelligently by default, simplifying the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform Support
&lt;/h2&gt;

&lt;p&gt;Vibium supports the following platforms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;x64&lt;/td&gt;
&lt;td&gt;✅ Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;x64 (Intel)&lt;/td&gt;
&lt;td&gt;✅ Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;arm64 (Apple Silicon)&lt;/td&gt;
&lt;td&gt;✅ Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;x64&lt;/td&gt;
&lt;td&gt;✅ Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;During installation, the appropriate binary for the platform is automatically selected, and Chrome's cache is stored in the following locations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: &lt;code&gt;~/.cache/vibium/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: &lt;code&gt;~/Library/Caches/vibium/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: &lt;code&gt;%LOCALAPPDATA%\vibium\&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Roadmap: Plans Beyond v2
&lt;/h2&gt;

&lt;p&gt;The current v1 focuses on "integration of AI and browsers," but the &lt;a href="https://github.com/VibiumDev/vibium/blob/main/V2-ROADMAP.md" rel="noopener noreferrer"&gt;v2 roadmap&lt;/a&gt; outlines the following features that are released or planned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python Client&lt;/strong&gt;: Released in December 2025 (&lt;code&gt;pip install vibium&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java Client&lt;/strong&gt;: Planned for enterprise use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cortex&lt;/strong&gt;: Memory and navigation layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retina&lt;/strong&gt;: Recording extension&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video Recording&lt;/strong&gt;: Capturing test execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Locator&lt;/strong&gt;: Smarter element searching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are extensions of the vision of "AI agents handling browsers more naturally."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Vibium Now?
&lt;/h2&gt;

&lt;p&gt;The reason Jason Huggins created a new tool after about 20 years since Selenium is due to the &lt;strong&gt;paradigm shift&lt;/strong&gt; brought about by the emergence of AI agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  From Testing Tools to AI Tools
&lt;/h3&gt;

&lt;p&gt;Selenium was created for test automation. The same goes for Playwright and Puppeteer. However, AI agents like Claude Code, Gemini, and ChatGPT use &lt;strong&gt;different approaches&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of executing human-written test scripts, AI makes dynamic judgments&lt;/li&gt;
&lt;li&gt;Instead of fixed selectors, elements are identified based on visual information and context&lt;/li&gt;
&lt;li&gt;Browser operations are part of task achievement, not the ultimate goal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tool optimized for this new usage was needed. That is Vibium.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Rise of the MCP Ecosystem
&lt;/h3&gt;

&lt;p&gt;The Model Context Protocol (MCP) is an integration standard for AI agents and tools proposed by &lt;a href="https://www.anthropic.com/" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;. Vibium was designed from the ground up as an MCP server, allowing for instant integration with Claude Code, Cursor, and other MCP-compliant AI editors.&lt;/p&gt;

&lt;p&gt;This represents a shift in thinking from "creating a tool and then figuring out how to integrate it" to "designing a tool with integration in mind."&lt;/p&gt;

&lt;h3&gt;
  
  
  A Return to Simplicity
&lt;/h3&gt;

&lt;p&gt;Over 20 years, browser automation tools have become feature-rich but also complex. Vibium regains simplicity by focusing on "only the truly necessary features":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single binary&lt;/li&gt;
&lt;li&gt;Zero setup&lt;/li&gt;
&lt;li&gt;Minimal API&lt;/li&gt;
&lt;li&gt;Automatic waiting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This philosophy aligns with the recent trend of "reducing complexity" seen in tools like exo and dotenvx.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token Efficiency Comparison Experiment Between Playwright and Vibium
&lt;/h2&gt;

&lt;p&gt;For AI agents, the important factor is the amount of token consumption required to achieve a task. We executed the same task with both tools and measured token efficiency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experimental Conditions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Task&lt;/strong&gt;: "Access example.com and take a screenshot of the page"&lt;/p&gt;

&lt;p&gt;We used OpenCode's token counter to measure actual token consumption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measured Results: Token Consumption
&lt;/h3&gt;

&lt;h4&gt;
  
  
  In the Case of Vibium
&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;# Executed tool calls&lt;/span&gt;
1. browser_launch         &lt;span class="c"&gt;# Launch the browser&lt;/span&gt;
2. browser_navigate       &lt;span class="c"&gt;# Navigate to https://example.com&lt;/span&gt;
3. browser_screenshot     &lt;span class="c"&gt;# Take a screenshot&lt;/span&gt;
4. browser_quit          &lt;span class="c"&gt;# Close the browser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Consumed Tokens: 240 tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Examples of responses from each tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;browser_launch&lt;/code&gt;: "Browser launched (headless: false)" (7 words)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;browser_navigate&lt;/code&gt;: "Navigated to &lt;a href="https://example.com/" rel="noopener noreferrer"&gt;https://example.com/&lt;/a&gt;" (4 words)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;browser_screenshot&lt;/code&gt;: "Screenshot saved to /path/to/file.png" (5 words)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;browser_quit&lt;/code&gt;: "Browser session closed" (3 words)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  In the Case of Playwright
&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;# Executed tool calls&lt;/span&gt;
1. browser_navigate       &lt;span class="c"&gt;# Navigate to https://example.com&lt;/span&gt;
2. browser_take_screenshot &lt;span class="c"&gt;# Take a screenshot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Consumed Tokens: 2,061 tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Examples of responses from each tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;browser_navigate&lt;/code&gt;: 

&lt;ul&gt;
&lt;li&gt;Executed code snippet: &lt;code&gt;await page.goto('https://example.com');&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Page information (URL, title)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entire accessibility tree&lt;/strong&gt; (in YAML format, hundreds to thousands of words)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;browser_take_screenshot&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;Executed code snippet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshot image data&lt;/strong&gt; (consumed tokens via Vision API)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Surprising Result: Vibium is 8.6 Times More Efficient
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Playwright: 2,061 tokens (2 tool calls)
Vibium:       240 tokens (4 tool calls)

Efficiency Ratio: Vibium achieves the same task at about 1/8.6 the tokens of Playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;{{&amp;lt; figure-desc src="/images/vibium-selenium-creator-browser-automation/token-comparison-race.png" alt="Token Efficiency Comparison: Playwright vs. Vibium Race" &amp;gt;}}&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Such a Difference?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Reasons Playwright Consumes More Tokens&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Sending of Accessibility Tree&lt;/strong&gt;: &lt;code&gt;browser_navigate&lt;/code&gt; returns the entire DOM structure of the page in YAML format every time.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;generic [ref=e2]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;heading "Example Domain" [level=1] [ref=e3]&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;paragraph [ref=e4]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This domain is for use...&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;paragraph [ref=e5]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
       &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;link "Learn more" [ref=e6]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
         &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;/url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://iana.org/domains/example&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This data alone consumes hundreds of tokens.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Displaying Executed Code&lt;/strong&gt;: Displays actual Playwright code for debugging purposes.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({...});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Image Data&lt;/strong&gt;: Screenshots are returned as images and processed by the Vision API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Reasons Vibium is Efficient&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Minimal Responses&lt;/strong&gt;: Only success/failure messages (averaging fewer than 5 words).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images Stored Locally&lt;/strong&gt;: Screenshots return only the file path (no token consumption).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Code Display&lt;/strong&gt;: Only simple status messages are returned.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Impact on Complex Tasks
&lt;/h3&gt;

&lt;p&gt;Even for a simple page like example.com, an 8.6x difference emerges. In actual web applications (e.g., dashboards, admin panels), this difference will widen even further:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page Complexity&lt;/th&gt;
&lt;th&gt;Playwright Consumption&lt;/th&gt;
&lt;th&gt;Vibium Consumption&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple (example.com)&lt;/td&gt;
&lt;td&gt;2,061 tokens&lt;/td&gt;
&lt;td&gt;240 tokens&lt;/td&gt;
&lt;td&gt;8.6x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium (blog post)&lt;/td&gt;
&lt;td&gt;Estimated 5,000–10,000&lt;/td&gt;
&lt;td&gt;240–300&lt;/td&gt;
&lt;td&gt;16–33x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex (admin panel)&lt;/td&gt;
&lt;td&gt;Estimated 10,000–50,000&lt;/td&gt;
&lt;td&gt;240–400&lt;/td&gt;
&lt;td&gt;25–125x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;ul&gt;
&lt;li&gt;Vibium is designed to return "only the necessary information for AI."&lt;/li&gt;
&lt;li&gt;Playwright returns "information for humans to debug."&lt;/li&gt;
&lt;li&gt;Vibium's approach is overwhelmingly advantageous for reducing operational costs for AI agents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These experimental results clearly illustrate why Vibium was created separately from Playwright. In the era of AI agents, "how efficient" is more important than "how feature-rich."&lt;/p&gt;

&lt;h2&gt;
  
  
  Differentiating Between Playwright and Vibium
&lt;/h2&gt;

&lt;p&gt;Both tools are excellent, but the optimal choice varies depending on the use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cases Where Playwright is More Suitable
&lt;/h3&gt;

&lt;p&gt;Playwright is better suited for scenarios such as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Human-Written E2E Tests&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixed test scripts executed in CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Existing Playwright test suites&lt;/li&gt;
&lt;li&gt;Need for debugging information (accessibility tree, executed code)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Cross-Browser Testing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running the same code across Chrome, Firefox, and Safari&lt;/li&gt;
&lt;li&gt;Verifying differences in behavior between browsers&lt;/li&gt;
&lt;li&gt;Emulating mobile browsers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Advanced DOM Manipulation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex operations with Shadow DOM or iframes&lt;/li&gt;
&lt;li&gt;Intercepting and mocking network requests&lt;/li&gt;
&lt;li&gt;Fine control over browser contexts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Integration with Existing Ecosystems&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Official tools like Playwright Test Runner, Playwright Inspector&lt;/li&gt;
&lt;li&gt;Benefits of TypeScript type definitions (auto-completion, type checking)&lt;/li&gt;
&lt;li&gt;Official support and community from Microsoft&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: Complex E2E Test Scenario&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="c1"&gt;// Playwright's strength: Detailed control&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Complex payment flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&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="c1"&gt;// Mocking network requests&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/api/payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"success": true}&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="c1"&gt;// Operations across multiple tabs&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;newPage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[target="_blank"]&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="c1"&gt;// Manipulating elements within Shadow DOM&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shadowHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;custom-element&lt;/span&gt;&lt;span class="dl"&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;shadowButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;shadowHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluateHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cases Where Vibium is More Suitable
&lt;/h3&gt;

&lt;p&gt;Conversely, Vibium is optimal for scenarios such as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Automation by AI Agents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLMs (Claude, Gemini, etc.) operating the browser&lt;/li&gt;
&lt;li&gt;Executing tasks based on natural language instructions&lt;/li&gt;
&lt;li&gt;Emphasizing token cost efficiency in operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Dynamic Browser Operations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tasks where steps are not predetermined&lt;/li&gt;
&lt;li&gt;Situations requiring actions to change based on user input&lt;/li&gt;
&lt;li&gt;Prioritizing "reaching the goal" over fixed procedures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Simple Scripts and REPL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interactively operating the browser in Node.js REPL&lt;/li&gt;
&lt;li&gt;Direct invocation from Python scripts&lt;/li&gt;
&lt;li&gt;Writing simply with synchronous API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Zero Setup is Essential&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimizing dependencies in CI environments&lt;/li&gt;
&lt;li&gt;Keeping Docker container sizes small&lt;/li&gt;
&lt;li&gt;Quick prototyping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: AI Agent Task&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vibium's strength: Simplicity and AI integration&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;browserSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vibium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// REPL-friendly synchronous API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;browserSync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// AI determines the next step&lt;/span&gt;
&lt;span class="c1"&gt;// "Find the login button and click it"&lt;/span&gt;
&lt;span class="c1"&gt;// → Automatically waits if the element is not found&lt;/span&gt;
&lt;span class="c1"&gt;// → If still not found, requests AI to reassess&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Criteria for Differentiation
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Playwright&lt;/th&gt;
&lt;th&gt;Vibium&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Executor&lt;/td&gt;
&lt;td&gt;Human-written scripts&lt;/td&gt;
&lt;td&gt;AI agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nature of Tests&lt;/td&gt;
&lt;td&gt;Deterministic (fixed steps)&lt;/td&gt;
&lt;td&gt;Dynamic (context-dependent judgments)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need for Debugging&lt;/td&gt;
&lt;td&gt;High (detailed information needed)&lt;/td&gt;
&lt;td&gt;Low (results-focused)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token Cost&lt;/td&gt;
&lt;td&gt;Not a concern&lt;/td&gt;
&lt;td&gt;Important&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-Browser&lt;/td&gt;
&lt;td&gt;Essential&lt;/td&gt;
&lt;td&gt;Chrome-centric is fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing Assets&lt;/td&gt;
&lt;td&gt;Playwright code available&lt;/td&gt;
&lt;td&gt;Zero start&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Practical Suggestion: Use Both&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In many projects, the ideal approach is to differentiate as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed tests in CI/CD&lt;/strong&gt; → Playwright (stability-focused)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploratory testing and demos&lt;/strong&gt; → Vibium (flexibility-focused)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI assistant integration&lt;/strong&gt; → Vibium (token efficiency-focused)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are excellent tools, and the decision should be based on "which is more suitable" rather than "which is better."&lt;/p&gt;

&lt;p&gt;{{&amp;lt; figure-desc src="/images/vibium-selenium-creator-browser-automation/tool-selection-crossroads.png" alt="Tool Selection Crossroads: Differentiating Playwright and Vibium" &amp;gt;}}&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The reasons Vibium was created anew rather than using Playwright can be summarized in the following three points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AI Agent-First Design&lt;/strong&gt;: Built-in MCP server, stdio communication, simple API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adoption of WebDriver BiDi&lt;/strong&gt;: A standardized next-generation protocol&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Setup Philosophy&lt;/strong&gt;: Single binary, automatic browser downloads, immediate functionality&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The evolution from Selenium to Playwright aimed at creating "better testing tools." In contrast, Vibium pioneers a new category as "infrastructure for AI to operate browsers."&lt;/p&gt;

&lt;p&gt;As demonstrated by the token efficiency experiment, Vibium adopts a new approach where "AI agents visually understand the web." This contrasts with the traditional DOM manipulation-centric tools.&lt;/p&gt;

&lt;p&gt;If you're interested in browser automation in the AI agent era, be sure to try out Vibium.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/VibiumDev/vibium" rel="noopener noreferrer"&gt;Vibium GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vibium.com/" rel="noopener noreferrer"&gt;Vibium Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://w3c.github.io/webdriver-bidi/" rel="noopener noreferrer"&gt;WebDriver BiDi Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.selenium.dev/" rel="noopener noreferrer"&gt;Selenium Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;Puppeteer Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://testguild.com/podcast/automation/a559-jason/" rel="noopener noreferrer"&gt;TestGuild Podcast: Jason Huggins Interview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Agentic CLI Design: 7 Principles for Designing CLI as a Protocol for AI Agents</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Fri, 06 Feb 2026 00:38:38 +0000</pubDate>
      <link>https://forem.com/tumf/agentic-cli-design-7-principles-for-designing-cli-as-a-protocol-for-ai-agents-2c10</link>
      <guid>https://forem.com/tumf/agentic-cli-design-7-principles-for-designing-cli-as-a-protocol-for-ai-agents-2c10</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-02-06&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/2/6/agentic-cli-design-principles/" rel="noopener noreferrer"&gt;Agentic CLI Design: CLIをAIエージェント向けプロトコルとして設計する7つの原則&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CLI tools have long been designed as interfaces for human interaction with terminals. However, with the rise of &lt;a href="https://blog.tumf.dev/tags/llm/" rel="noopener noreferrer"&gt;LLMs&lt;/a&gt; (Large Language Models) and &lt;a href="https://blog.tumf.dev/tags/ai/" rel="noopener noreferrer"&gt;AI agents&lt;/a&gt; (programs that autonomously invoke tools to progress tasks), there is a new role being demanded of CLIs. This new role is to be designed as a "protocol/API that agents can safely, reliably, and repeatedly invoke."&lt;/p&gt;

&lt;p&gt;Recently, I have had more opportunities to have agents run CLI commands. Issues that might not bother humans, such as "stopping at confirmation prompts," "logs mixing with stdout making parsing impossible," and "accidentally repeating the same operation," can occur quite normally when interacting with agents.&lt;/p&gt;

&lt;p&gt;In this article, I will summarize the design concept I propose called "Agentic CLI Design." This redefines CLI from "a UI operated by humans" to "a protocol invoked by agents," establishing seven design principles that ensure functionality based on assumptions of failure, re-execution, and non-interactivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Agentic CLI Design?
&lt;/h2&gt;

&lt;p&gt;Agentic CLI Design consists of design principles for CLIs that allow LLMs/agents to execute commands safely and reliably in a non-interactive, iterative, and failure-prone environment.&lt;/p&gt;

&lt;p&gt;Rather than optimizing for "tactility" or "ease of use" for humans, it focuses on ensuring that &lt;strong&gt;machines can read, judge, re-execute, and recover&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Success Conditions
&lt;/h3&gt;

&lt;p&gt;The success conditions for Agentic CLI Design are that agents must meet the following criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No confusion&lt;/strong&gt;: Options are clearly presented, allowing for judgment on the next action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No destruction&lt;/strong&gt;: Default to safety, requiring explicit confirmation for destructive operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No blockage&lt;/strong&gt;: Able to complete non-interactively, with clear timeout/retry policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatable&lt;/strong&gt;: Idempotent, ensuring safety when re-executed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-repairing&lt;/strong&gt;: Observable, allowing for judgment of recovery procedures from errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7 Principles
&lt;/h2&gt;

&lt;p&gt;Agentic CLI Design is composed of the following seven principles (Principle 1 to Principle 7).&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 1: Machine-readable
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Principle&lt;/strong&gt;: Output is structured and provided in a format that machines can reliably parse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design Checks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Options for &lt;code&gt;--json&lt;/code&gt; / &lt;code&gt;--output json|yaml|text&lt;/code&gt; are available.&lt;/li&gt;
&lt;li&gt;Strict adherence to standard output (stdout) = results / standard error output (stderr) = logs/progress (do not mix).&lt;/li&gt;
&lt;li&gt;Errors are also structured (preferably in JSON).&lt;/li&gt;
&lt;li&gt;Schema is stable (breaking changes managed via &lt;code&gt;schemaVersion&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The CLI for &lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt; called &lt;a href="https://kubernetes.io/docs/reference/kubectl/" rel="noopener noreferrer"&gt;kubectl&lt;/a&gt; supports JSON output. The &lt;a href="https://docs.aws.amazon.com/cli/" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; also has &lt;code&gt;--output json&lt;/code&gt;.&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;# Structured output on success&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;-o&lt;/span&gt; json

&lt;span class="c"&gt;# Example using JSON output&lt;/span&gt;
aws ec2 describe-instances &lt;span class="nt"&gt;--output&lt;/span&gt; json 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Human-friendly "readable tables" are secondary. Agents need to be able to reliably parse JSON or YAML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimum Recommended Response&lt;/strong&gt; (JSON):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On success&lt;/strong&gt;:&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;"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;"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;"items.list"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&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;"data"&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;"items"&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="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;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-05T08:00:00Z"&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;"nextCursor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;strong&gt;On failure&lt;/strong&gt;:&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;"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;false&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;"items.list"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&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;"error"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rate_limited"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="nl"&gt;"retryAfterMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&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;h3&gt;
  
  
  Principle 2: Non-interactive by default
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Principle&lt;/strong&gt;: Do not assume interactive prompts, allowing for completion in headless environments (running without screens or interactive operations, such as CI or job runners).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design Checks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Options for &lt;code&gt;--yes&lt;/code&gt; / &lt;code&gt;--force&lt;/code&gt; / &lt;code&gt;--no-confirm&lt;/code&gt; / &lt;code&gt;--non-interactive&lt;/code&gt; are available.&lt;/li&gt;
&lt;li&gt;Must be able to complete in environments without TTY.&lt;/li&gt;
&lt;li&gt;If interaction is necessary, it must be explicitly opted in.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Having pre-execution options like in &lt;a href="https://developer.hashicorp.com/terraform" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; makes agent operations easier.&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;# Execute without interaction&lt;/span&gt;
terraform apply &lt;span class="nt"&gt;-auto-approve&lt;/span&gt;

&lt;span class="c"&gt;# Explicitly in non-interactive mode&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents cannot respond to "Y/N?" prompts. All choices must be specified in advance via options. It is also crucial that the process does not stop in environments without TTY.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; (OAuth/headless) Key Points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If possible, prioritize &lt;strong&gt;Device Authorization Grant&lt;/strong&gt; (RFC 8628).&lt;/li&gt;
&lt;li&gt;Provide &lt;code&gt;auth status --json&lt;/code&gt; for agents to confirm prerequisites.&lt;/li&gt;
&lt;li&gt;Support migration to headless environments with &lt;code&gt;auth export&lt;/code&gt; / &lt;code&gt;auth import&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;--non-interactive&lt;/code&gt;, return "error + next steps" without asking for confirmation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 3: Idempotent &amp;amp; Replayable
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Principle&lt;/strong&gt;: It is safe to execute the same command multiple times, and the results are predictable.&lt;/p&gt;

&lt;p&gt;Idempotence means that repeating the same operation does not change the result. Agents may "hit the same command again" due to timeouts or network interruptions. Therefore, a design that avoids accidents during re-execution is necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design Checks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept dedupe-key / client-request-id for sending/creating.&lt;/li&gt;
&lt;li&gt;Allow choosing behaviors for "already created": &lt;code&gt;--if-exists skip|update|error&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Clearly indicate paging for retrieval: &lt;code&gt;--limit&lt;/code&gt; &lt;code&gt;--cursor&lt;/code&gt; &lt;code&gt;--all&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:&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;# Idempotent creation (skip if already exists)&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; deployment.yaml

&lt;span class="c"&gt;# For a CLI hitting an HTTP API, explicitly provide a request ID (deduplication key)&lt;/span&gt;
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.example.com/v1/items &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Idempotency-Key: 01JHXXXX...'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"example"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents may re-execute the same command due to network errors or timeouts. A design that ensures safety during re-execution is essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 4: Safe-by-default
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Principle&lt;/strong&gt;: Destructive operations are not executed by default and require explicit confirmation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design Checks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Destructive operations can enforce &lt;code&gt;--dry-run&lt;/code&gt; / &lt;code&gt;--confirm &amp;lt;id&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deletion requires &lt;code&gt;--force&lt;/code&gt;, ensuring no accidents by default.&lt;/li&gt;
&lt;li&gt;Minimize permissions/scope, returning "next steps" when insufficient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:&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;# Dry-run for pre-confirmation&lt;/span&gt;
terraform plan

&lt;span class="c"&gt;# Execution requires explicit approval&lt;/span&gt;
terraform apply

&lt;span class="c"&gt;# Prepare a preview before destructive operations&lt;/span&gt;
kubectl diff &lt;span class="nt"&gt;-f&lt;/span&gt; deployment.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents can accidentally perform deletions. Multiple layers of confirmation are necessary for destructive operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 5: Observable &amp;amp; Debuggable
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Principle&lt;/strong&gt;: The execution status can be observed, and recovery procedures can be determined in case of errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design Checks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Options for &lt;code&gt;--verbose&lt;/code&gt; / &lt;code&gt;--debug&lt;/code&gt; / &lt;code&gt;--log-format json&lt;/code&gt; are available.&lt;/li&gt;
&lt;li&gt;Ability to pass correlation IDs with &lt;code&gt;--trace-id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Classify exit codes to facilitate automatic recovery:

&lt;ul&gt;
&lt;li&gt;Example: 0=success / 2=argument error / 3=authentication error / 4=retry recommended.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:&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;# Output detailed logs&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; deployment.yaml &lt;span class="nt"&gt;--v&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9

&lt;span class="c"&gt;# Determine based on exit code&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 4 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Retryable error, waiting..."&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;5
  retry_command
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents will determine the "next step" from error messages. Structured exit codes and errors are crucial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended Exit Code Classification&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt;: Success&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2&lt;/strong&gt;: Argument error / usage error&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt;: Authentication / permission error&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4&lt;/strong&gt;: Retry recommended (rate limit / transient)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 6: Context-efficient
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Principle&lt;/strong&gt;: Do not waste the context window of LLMs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design Checks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;--fields/--select&lt;/code&gt; (projection) to retrieve only necessary fields.&lt;/li&gt;
&lt;li&gt;Handle large data with &lt;code&gt;--output ndjson&lt;/code&gt; (streaming format with one JSON per line).&lt;/li&gt;
&lt;li&gt;Default to summaries, with details provided via &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;--include-*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Implement server-side filtering (since/until/query/type…).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:&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;# Retrieve only necessary fields&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;-o&lt;/span&gt; custom-columns&lt;span class="o"&gt;=&lt;/span&gt;NAME:.metadata.name,STATUS:.status.phase

&lt;span class="c"&gt;# Handle large data with paging&lt;/span&gt;
aws s3api list-objects-v2 &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-bucket &lt;span class="nt"&gt;--max-items&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents can hit token limits if they cram too much data into the context window (the maximum input LLMs can reference at once). A design that retrieves only the minimum necessary data is required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 7: Introspectable
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Principle&lt;/strong&gt;: The CLI itself should output specifications in a machine-readable format, allowing agents to self-discover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design Checks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provide &lt;code&gt;commands --json&lt;/code&gt; (command list and argument list).&lt;/li&gt;
&lt;li&gt;Provide &lt;code&gt;schema --command ... --output json-schema&lt;/code&gt; (command-level &lt;a href="https://json-schema.org/" rel="noopener noreferrer"&gt;JSON Schema&lt;/a&gt;, defining the structure of JSON).&lt;/li&gt;
&lt;li&gt;Provide &lt;code&gt;--help --json&lt;/code&gt; (examples, exit codes, error vocabulary).&lt;/li&gt;
&lt;li&gt;Example of top-level fixed fields for &lt;code&gt;--output json&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;schemaVersion&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;ok&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:&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;# This is an example of desirable self-describing design&lt;/span&gt;
tool commands &lt;span class="nt"&gt;--output&lt;/span&gt; json
tool schema &lt;span class="nt"&gt;--command&lt;/span&gt; items.list &lt;span class="nt"&gt;--output&lt;/span&gt; json-schema
tool &lt;span class="nb"&gt;help &lt;/span&gt;items.list &lt;span class="nt"&gt;--output&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; (MCP) can derive schemas from tool definitions, but CLIs often become black boxes. By having the CLI output specifications in a machine-readable format, agents can achieve self-discovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended Set of Introspection Commands&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tool commands --json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tool schema --command &amp;lt;subcommand...&amp;gt; --output json-schema&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tool help --json&lt;/code&gt; (or &lt;code&gt;--help --json&lt;/code&gt; for each command)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Anti-patterns
&lt;/h2&gt;

&lt;p&gt;The following are examples of "commonly broken" aspects from the perspective of Agentic CLI Design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logs/Progress Mixed with stdout
&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;# ❌ Bad Example&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Processing..."&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"result": "success"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents will fail when trying to parse JSON. Please output logs to stderr.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON Structure Changes Based on Conditions
&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;# ❌ Bad Example&lt;/span&gt;
&lt;span class="c"&gt;# On success: {"data": {...}}&lt;/span&gt;
&lt;span class="c"&gt;# On failure: {"error": "..."}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please clarify success/failure with an &lt;code&gt;ok&lt;/code&gt; field and unify the structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Default Interaction Causes Blockage in Headless Environments
&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;# ❌ Bad Example&lt;/span&gt;
&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Continue? (y/n): "&lt;/span&gt; answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will cause blockage in CI environments or job runners. Please provide a &lt;code&gt;--yes&lt;/code&gt; option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Destructive Commands Can Be Executed by Default
&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;# ❌ Bad Example&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /data/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please provide two-step confirmations with &lt;code&gt;--dry-run&lt;/code&gt; and &lt;code&gt;--confirm&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--all&lt;/code&gt; Results in Huge JSON Output
&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;# ❌ Bad Example&lt;/span&gt;
curl https://api.example.com/items?all&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will explode the context window. Please implement paging (&lt;code&gt;--limit&lt;/code&gt; / &lt;code&gt;--cursor&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication Requires a Browser, Failing in Remote/Container Environments
&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;# ❌ Bad Example&lt;/span&gt;
open https://auth.example.com/login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please prioritize &lt;a href="https://datatracker.ietf.org/doc/html/rfc8628" rel="noopener noreferrer"&gt;Device Authorization Grant&lt;/a&gt; (RFC 8628).&lt;/p&gt;

&lt;h2&gt;
  
  
  Scorecard (Review Checklist)
&lt;/h2&gt;

&lt;p&gt;The following is a checklist that can score the seven principles of Agentic CLI Design on a scale of 0/1/2 points. It is structured with specific items to facilitate easy adaptation to other projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Principle 1: Machine-readable
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;--output json&lt;/code&gt; is available.&lt;/li&gt;
&lt;li&gt;[ ] stdout = results / stderr = logs is adhered to.&lt;/li&gt;
&lt;li&gt;[ ] Errors are structured (JSON recommended).&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;schemaVersion&lt;/code&gt; is present / compatibility policy is documented.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 2: Non-interactive
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;--non-interactive&lt;/code&gt; is available (can be auto ON without TTY).&lt;/li&gt;
&lt;li&gt;[ ] All operations requiring interaction are opt-in (i.e., default is non-interactive).&lt;/li&gt;
&lt;li&gt;[ ] Vocabulary for &lt;code&gt;--yes/--force/--no-confirm&lt;/code&gt; is unified.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 3: Idempotent &amp;amp; Replayable
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Writing commands have &lt;code&gt;--client-request-id&lt;/code&gt; / &lt;code&gt;--dedupe-key&lt;/code&gt; equivalents.&lt;/li&gt;
&lt;li&gt;[ ] There is a policy for &lt;code&gt;--if-exists&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;--cursor/--limit/--all&lt;/code&gt; are available (with &lt;code&gt;--all&lt;/code&gt; implementing internal paging).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 4: Safe-by-default
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Destructive operations can use &lt;code&gt;--dry-run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Actual execution requires additional guards like &lt;code&gt;--confirm &amp;lt;id&amp;gt;&lt;/code&gt; / &lt;code&gt;--force&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 5: Observable &amp;amp; Debuggable
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;--debug&lt;/code&gt; is available (logs to stderr).&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;--log-format json&lt;/code&gt; is available.&lt;/li&gt;
&lt;li&gt;[ ] Accepts &lt;code&gt;--trace-id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Exit code classification is present (2/3/4, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 6: Context-efficient
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;--fields/--select&lt;/code&gt; is available.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;--output ndjson&lt;/code&gt; is available.&lt;/li&gt;
&lt;li&gt;[ ] Heavy fields are opt-in via &lt;code&gt;--include-*&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Principle 7: Introspectable
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;commands --json&lt;/code&gt; is available.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;schema --command ... --output json-schema&lt;/code&gt; is available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This Scorecard can be used for reviewing CLIs or as acceptance criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Released AgentSkill
&lt;/h2&gt;

&lt;p&gt;The content written in this article consists of "principles," which can lead to confusion when trying to implement them. Therefore, I have released an AgentSkill (a manual for agents) that supports Agentic CLI Design.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tumf/skills/tree/main/agentic-cli-design" rel="noopener noreferrer"&gt;tumf/skills: agentic-cli-design&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it includes (having these elements generally stabilizes agent operations):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recipes by task (shortest command sequences).&lt;/li&gt;
&lt;li&gt;Guardrails (flow like &lt;code&gt;--dry-run&lt;/code&gt; → confirmation → &lt;code&gt;--confirm&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Recommended defaults (&lt;code&gt;--output json&lt;/code&gt;, &lt;code&gt;--non-interactive&lt;/code&gt;, paging, etc.).&lt;/li&gt;
&lt;li&gt;Typical success/failure output examples (JSON).&lt;/li&gt;
&lt;li&gt;Recovery procedures for errors (retries, authentication, insufficient permissions, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using this AgentSkill as a foundation, I believe it is the fastest way to solidify "procedures and vocabulary for safe usage" for your CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI vs MCP: Considerations for Differentiation
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP) is a standard protocol for connecting AI models with external tools. MCP and CLI are not competing; they can be differentiated as follows:&lt;/p&gt;

&lt;h3&gt;
  
  
  Cases Suitable for CLI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Existing CLI tools are available&lt;/strong&gt;: &lt;a href="https://cli.github.com/" rel="noopener noreferrer"&gt;GitHub CLI&lt;/a&gt; (gh), kubectl, aws cli, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless operations&lt;/strong&gt;: Operations that can be completed in a single command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration with Unix pipes&lt;/strong&gt;: Integration with existing shell scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cases Suitable for MCP
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No existing CLI tools&lt;/strong&gt;: Custom services or APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateful operations&lt;/strong&gt;: Operations that require maintaining state across multiple calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time streaming&lt;/strong&gt;: MCP supports streaming responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom business logic&lt;/strong&gt;: Applying unique rules to tool access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agentic CLI Design is a set of design principles for optimizing existing CLI tools for agents. When creating new tools, consider both MCP and CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Agentic CLI Design is a design concept that redefines CLI from "a UI operated by humans" to "a protocol invoked by agents."&lt;/p&gt;

&lt;p&gt;By being mindful of the seven principles (Principle 1 to Principle 7), you can design CLIs that allow agents to operate "without confusion, without destruction, without blockage, repeatedly, and while self-repairing."&lt;/p&gt;

&lt;p&gt;Using this Scorecard when reviewing existing CLI tools (gh, kubectl, aws cli, etc.) can help identify areas for improvement for agents.&lt;/p&gt;

&lt;p&gt;If you are interested, please take the time to score your CLI tool using the Scorecard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clig.dev/" rel="noopener noreferrer"&gt;Command Line Interface Guidelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc8628" rel="noopener noreferrer"&gt;RFC 8628: OAuth 2.0 Device Authorization Grant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/reference/kubectl/" rel="noopener noreferrer"&gt;kubectl Reference Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform" rel="noopener noreferrer"&gt;Terraform Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/" rel="noopener noreferrer"&gt;AWS CLI Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cli.github.com/" rel="noopener noreferrer"&gt;GitHub CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ndjson.org/" rel="noopener noreferrer"&gt;NDJSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://json-schema.org/" rel="noopener noreferrer"&gt;JSON Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tumf/skills/tree/main/agentic-cli-design" rel="noopener noreferrer"&gt;tumf/skills: agentic-cli-design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-02-03-cli-is-the-new-mcp/view" rel="noopener noreferrer"&gt;Why CLI is the New MCP for AI Agents - OneUptime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>devops</category>
    </item>
    <item>
      <title>jj workspace: Parallel Development with Vibe Coding Without Getting Stopped by Conflicts</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Thu, 05 Feb 2026 12:43:12 +0000</pubDate>
      <link>https://forem.com/tumf/jj-workspace-parallel-development-with-vibe-coding-without-getting-stopped-by-conflicts-3m9k</link>
      <guid>https://forem.com/tumf/jj-workspace-parallel-development-with-vibe-coding-without-getting-stopped-by-conflicts-3m9k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-01-06&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/1/6/jj-workspace-vibe-coding-parallel-development/" rel="noopener noreferrer"&gt;jj workspace: コンフリクトで止まらないvibe coding並列開発&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recently, while developing with &lt;a href="https://www.anthropic.com/news/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; running in parallel across four instances, I encountered a bit of a problem.&lt;/p&gt;

&lt;p&gt;I created four directories using &lt;code&gt;git worktree&lt;/code&gt; and assigned the AI to implement features in each one—up to this point, everything was smooth. However, when it came time to merge, I was hit with a storm of "CONFLICT" messages. If one worktree gets blocked by a conflict, all the work derived from it gets blocked as well, ultimately resulting in the AI just sitting idle.&lt;/p&gt;

&lt;p&gt;"I don’t want to stop working just because a conflict occurred..." I thought, and while researching, I discovered a version control system called &lt;a href="https://github.com/jj-vcs/jj" rel="noopener noreferrer"&gt;Jujutsu&lt;/a&gt; (jj), developed primarily by Google engineers. This tool turned out to be quite effective and fit my vibe coding style perfectly, so I wanted to share it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Jujutsu (jj)?
&lt;/h2&gt;

&lt;p&gt;Jujutsu is an open-source VCS that is compatible with Git, initiated by engineers at Google. Although it is not an official Google product, it is still actively developed by Googlers. The key point is its "Git compatibility," allowing it to be integrated directly into existing Git repositories.&lt;/p&gt;

&lt;p&gt;So, what are the benefits?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Work does not stop even if conflicts occur&lt;/strong&gt;: This is the biggest advantage. Conflicts are recorded in commits, allowing other work to continue normally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All operations are logged&lt;/strong&gt;: You can view the entire history with &lt;code&gt;jj op log&lt;/code&gt;, and you can revert to any point. This is subtly helpful when the AI's output is not quite right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic commits&lt;/strong&gt;: Files are committed as soon as they are saved. No need for &lt;code&gt;git add&lt;/code&gt; or similar commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built with Rust and fast&lt;/strong&gt;: This is just a bonus.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;If you're on macOS, you can install it with &lt;code&gt;brew install jj&lt;/code&gt;.&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;# macOS (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;jj

&lt;span class="c"&gt;# Linux (cargo)&lt;/span&gt;
cargo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--git&lt;/span&gt; https://github.com/jj-vcs/jj jj-cli

&lt;span class="c"&gt;# Check version&lt;/span&gt;
jj &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conceptual Differences from Git
&lt;/h3&gt;

&lt;p&gt;Initially, I was a bit confused by the subtle conceptual differences from Git. Once you get used to it, it becomes easier, but at first, I was like, "Wait, there are no branches?"&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Git&lt;/th&gt;
&lt;th&gt;Jujutsu (jj)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Working Copy&lt;/td&gt;
&lt;td&gt;Managed in the staging area (index)&lt;/td&gt;
&lt;td&gt;Always one commit (&lt;code&gt;@&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branches&lt;/td&gt;
&lt;td&gt;Required (detached HEAD is a special state)&lt;/td&gt;
&lt;td&gt;Not needed (can work anonymously)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflicts&lt;/td&gt;
&lt;td&gt;Treated as errors (blocks work)&lt;/td&gt;
&lt;td&gt;Recorded in commits (work can continue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History Manipulation&lt;/td&gt;
&lt;td&gt;rebase (risky)&lt;/td&gt;
&lt;td&gt;Automatic rebase (safe)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The especially "no need for branches" aspect felt odd at first, but in vibe coding, it allows you to "start working and decide on a name later," which turned out to be quite convenient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Operations in jj
&lt;/h2&gt;

&lt;p&gt;Trying out jj in an existing Git repository is straightforward. It can be used alongside Git, so if you don’t like it, you can revert back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initializing a Repository
&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;# Initialize jj in an existing Git repository&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-git-repo
jj git init &lt;span class="nt"&gt;--git-repo&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Check the current state&lt;/span&gt;
jj log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output of &lt;code&gt;jj log&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```text {hl_lines="1"}&lt;br&gt;
@  qpvuntsm &lt;a href="mailto:tumf@example.com"&gt;tumf@example.com&lt;/a&gt; 2026-01-05 14:30:00 &lt;br&gt;
│  (empty) (no description set)&lt;br&gt;
◉  rlvkpntz &lt;a href="mailto:tumf@example.com"&gt;tumf@example.com&lt;/a&gt; 2026-01-05 14:25:00 main&lt;br&gt;
│  Add README&lt;br&gt;
~&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`@` represents the current working copy commit. It is similar to Git's `HEAD`, but it always treats uncommitted changes as part of a commit.

### Basic Change Flow

If you're used to Git, you might initially think, "Wait, what?" since you don’t need `git add` or `git commit`.



```bash
# 1. Start a new change
jj new main -m "Add feature A"

# 2. Edit files
vim src/feature.js

# 3. Check the state (already committed!)
jj log

# 4. Add/modify description
jj describe -m "Add authentication feature"

# 5. Start the next change
jj new -m "Add tests for feature A"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;As soon as you save the file, it’s already committed, eliminating the need for the &lt;code&gt;git add&lt;/code&gt; → &lt;code&gt;git commit&lt;/code&gt; flow. Honestly, this alone makes it much more comfortable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Editing Commits
&lt;/h3&gt;

&lt;p&gt;Editing past commits is also easy. In Git, I would nervously use &lt;code&gt;git rebase -i&lt;/code&gt;, but with jj, it’s much more relaxed.&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 a past commit&lt;/span&gt;
jj edit &amp;lt;change-id&amp;gt;
vim src/feature.js
jj describe &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Updated feature"&lt;/span&gt;

&lt;span class="c"&gt;# Return to the original position&lt;/span&gt;
jj edit @
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moreover, descendant commits are automatically rebased, so you don’t have to worry about dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  git worktree vs jj workspace
&lt;/h2&gt;

&lt;p&gt;Now, onto the main topic. To engage in vibe coding with parallel development, you need "multiple working directories."&lt;/p&gt;

&lt;p&gt;Git has &lt;code&gt;git worktree&lt;/code&gt;, while jj has &lt;code&gt;jj workspace&lt;/code&gt;. While they may seem similar at first glance, there are several critical differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Mechanism is Similar
&lt;/h3&gt;

&lt;p&gt;The directory structure looks similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo/               # Main directory
├── .git/          # Git data (shared)
├── .jj/           # jj data (shared)
└── src/

workspace-1/        # Parallel working directory 1
└── src/

workspace-2/        # Parallel working directory 2
└── src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will still need to run &lt;code&gt;npm install&lt;/code&gt; in each directory for both, so there's no escaping that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Key Differences
&lt;/h3&gt;

&lt;p&gt;Now, here are the important differences that made me realize, "Oh, these are completely different."&lt;/p&gt;

&lt;h4&gt;
  
  
  Difference 1: Behavior During Conflicts (This is the Biggest One)
&lt;/h4&gt;

&lt;p&gt;With git worktree, it goes like this:&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;# Merge feature-A in worktree-1&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../worktree-1
git merge feature-A  &lt;span class="c"&gt;# ✅ Success&lt;/span&gt;

&lt;span class="c"&gt;# Merge feature-B in worktree-2&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../worktree-2
git merge feature-B  &lt;span class="c"&gt;# ❌ CONFLICT!&lt;/span&gt;
&lt;span class="c"&gt;# At this point, worktree-2 is blocked&lt;/span&gt;
&lt;span class="c"&gt;# Even if you create a new branch in another worktree, it branches from the main before the conflict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When worktree-2 gets blocked, all work derived from it also stops. This was painful.&lt;/p&gt;

&lt;p&gt;On the other hand, with jj workspace:&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;# Rebase feature-A in workspace-1&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-1
jj rebase &lt;span class="nt"&gt;-s&lt;/span&gt; feature-A &lt;span class="nt"&gt;-d&lt;/span&gt; main  &lt;span class="c"&gt;# ✅ Success&lt;/span&gt;

&lt;span class="c"&gt;# Rebase feature-B in workspace-2&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-2
jj rebase &lt;span class="nt"&gt;-s&lt;/span&gt; feature-B &lt;span class="nt"&gt;-d&lt;/span&gt; feature-A  &lt;span class="c"&gt;# ⚠️ Conflict occurs&lt;/span&gt;

&lt;span class="c"&gt;# But work can continue!&lt;/span&gt;
jj log
&lt;span class="c"&gt;# @  feature-B (conflict) ← Conflict state is recorded&lt;/span&gt;
&lt;span class="c"&gt;# ◉  feature-A&lt;/span&gt;
&lt;span class="c"&gt;# ◉  main&lt;/span&gt;

&lt;span class="c"&gt;# Start a different task in workspace-3&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-3
jj new main &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feature-C"&lt;/span&gt;  &lt;span class="c"&gt;# ✅ Work starts without issues&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conflicts are "recorded" only, and other work is not blocked. This is incredibly helpful for vibe coding.&lt;/p&gt;

&lt;p&gt;{{&amp;lt; figure-desc src="/images/jj-workspace-vibe-coding-parallel-development/conflict-comparison-diagram.png" alt="Comparison diagram of conflict handling between git worktree and jj workspace" caption="git worktree blocks on conflict, while jj records it and allows continuation" &amp;gt;}}&lt;/p&gt;

&lt;h4&gt;
  
  
  Difference 2: State Sharing and Visibility
&lt;/h4&gt;

&lt;p&gt;In git worktree, uncommitted changes in each worktree are not visible from others:&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;# Changes in worktree-1 (uncommitted)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../worktree-1
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; newfile.txt

&lt;span class="c"&gt;# Not visible from worktree-2&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../worktree-2
&lt;span class="nb"&gt;ls &lt;/span&gt;newfile.txt  &lt;span class="c"&gt;# ❌ Does not exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In jj, changes are automatically committed, so they are visible from all workspaces:&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;# Changes in workspace-1 (auto-committed)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-1
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; newfile.txt

&lt;span class="c"&gt;# Visible from workspace-2&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-2
jj log  &lt;span class="c"&gt;# Changes from workspace-1 are displayed with the @ mark&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces the instances of "Wait, where was I working and what was I doing?"&lt;/p&gt;

&lt;h4&gt;
  
  
  Difference 3: Automatic Rebase for Dependent Branches
&lt;/h4&gt;

&lt;p&gt;This is also subtly convenient. When there are dependencies like feature-A → feature-B → feature-C.&lt;/p&gt;

&lt;p&gt;In git worktree, if you fix feature-A, you need to manually rebase B and C:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../worktree-A
git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Fix feature A"&lt;/span&gt;

&lt;span class="c"&gt;# B and C need manual rebase&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../worktree-B
git rebase feature-A  &lt;span class="c"&gt;# Manual&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; ../worktree-C
git rebase feature-B  &lt;span class="c"&gt;# Manual&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With jj, it does it automatically for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-A
jj describe &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Fix feature A"&lt;/span&gt;

&lt;span class="c"&gt;# B and C are automatically rebased&lt;/span&gt;
jj log  &lt;span class="c"&gt;# All updated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since I often fine-tune code generated by AI later, this is a nice feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Workflow for Vibe Coding
&lt;/h2&gt;

&lt;p&gt;Here’s how I actually run my workflow with Claude Code in four parallel instances.&lt;/p&gt;

&lt;p&gt;{{&amp;lt; figure-desc src="/images/jj-workspace-vibe-coding-parallel-development/workspace-architecture-diagram.png" alt="Architecture diagram of parallel development with jj workspace" caption="Four workspaces operate in parallel around a shared repository hub" &amp;gt;}}&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;The initial setup looks like this. It’s a bit tedious to run &lt;code&gt;npm install&lt;/code&gt; four times, but it’s just the first time, so it’s manageable.&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;# Initialize in the project directory&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;my-project
jj git init &lt;span class="nt"&gt;--git-repo&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Create four workspaces&lt;/span&gt;
jj workspace add ../workspace-1
jj workspace add ../workspace-2
jj workspace add ../workspace-3
jj workspace add ../workspace-4

&lt;span class="c"&gt;# Install dependencies in each workspace (this is unavoidable)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..4&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-&lt;span class="nv"&gt;$i&lt;/span&gt;
  npm &lt;span class="nb"&gt;install
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Starting Parallel Tasks
&lt;/h3&gt;

&lt;p&gt;I throw different tasks at the AI in each workspace. I open four terminal windows and run Claude Code in each.&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;# workspace-1: Authentication feature&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-1
jj new main &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add authentication"&lt;/span&gt;
&lt;span class="c"&gt;# Ask Claude Code to "Implement JWT authentication"&lt;/span&gt;

&lt;span class="c"&gt;# workspace-2: Database optimization&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-2
jj new main &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Optimize database queries"&lt;/span&gt;
&lt;span class="c"&gt;# Ask Claude Code to "Fix N+1 queries"&lt;/span&gt;

&lt;span class="c"&gt;# workspace-3: Adding UI components&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-3
jj new main &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add user profile component"&lt;/span&gt;
&lt;span class="c"&gt;# Ask Claude Code to "Create a profile screen"&lt;/span&gt;

&lt;span class="c"&gt;# workspace-4: Adding tests&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-4
jj new main &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add integration tests"&lt;/span&gt;
&lt;span class="c"&gt;# Ask Claude Code to "Write E2E tests"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the AI is working, I switch to another workspace to review or think about the next task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling Conflicts
&lt;/h3&gt;

&lt;p&gt;When a conflict actually occurs, for example, if workspace-1 and workspace-2 edit the same file:&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;# Integrate changes from workspace-1 into main&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-1
jj rebase &lt;span class="nt"&gt;-s&lt;/span&gt; @ &lt;span class="nt"&gt;-d&lt;/span&gt; main  &lt;span class="c"&gt;# ✅ Success&lt;/span&gt;

&lt;span class="c"&gt;# When trying to integrate changes from workspace-2...&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-2
jj rebase &lt;span class="nt"&gt;-s&lt;/span&gt; @ &lt;span class="nt"&gt;-d&lt;/span&gt; main  &lt;span class="c"&gt;# ⚠️ Conflict occurs&lt;/span&gt;

&lt;span class="c"&gt;# But work can continue&lt;/span&gt;
jj log
&lt;span class="c"&gt;# @  workspace-2-change (conflict)&lt;/span&gt;
&lt;span class="c"&gt;# ◉  workspace-1-change&lt;/span&gt;
&lt;span class="c"&gt;# ◉  main&lt;/span&gt;

&lt;span class="c"&gt;# Work in workspaces 3 and 4 is unaffected!&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-3
jj new main &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Continue other work"&lt;/span&gt;  &lt;span class="c"&gt;# ✅ No issues&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point is that "conflict resolution can be done later in bulk." This prevents the AI from sitting idle.&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;# After all tasks are complete, resolve conflicts in bulk&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../workspace-2
jj edit @  &lt;span class="c"&gt;# Move to the commit with conflicts&lt;/span&gt;
jj resolve  &lt;span class="c"&gt;# Resolve interactively&lt;/span&gt;
jj describe &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Optimize queries (resolved conflict)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Final Merge
&lt;/h3&gt;

&lt;p&gt;Once everything is done, I push everything at once. Pushing to GitHub is done in the usual way.&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 changes in all workspaces&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;my-project
jj log &lt;span class="nt"&gt;--all&lt;/span&gt;

&lt;span class="c"&gt;# Organize with squash if necessary&lt;/span&gt;
jj squash &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;change-id&amp;gt; &lt;span class="nt"&gt;-d&lt;/span&gt; main

&lt;span class="c"&gt;# Push as a Git repository&lt;/span&gt;
jj git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tracking Prompt History with Operation Log
&lt;/h2&gt;

&lt;p&gt;This is a side benefit I discovered, but the operation log is handy for tracking "what was generated with which prompt."&lt;/p&gt;

&lt;p&gt;{{&amp;lt; figure-desc src="/images/jj-workspace-vibe-coding-parallel-development/operation-log-diagram.png" alt="Timeline diagram of the operation log" caption="All operations are recorded, allowing restoration to any point" &amp;gt;}}&lt;/p&gt;

&lt;h3&gt;
  
  
  Record of All Operations
&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;# Display all operations&lt;/span&gt;
jj op log

&lt;span class="c"&gt;# Example output&lt;/span&gt;
@  qpvuntsm agent-1@example.com 2026-01-05 15:30:00 op_abc123
│  describe &lt;span class="s2"&gt;"Implement authentication"&lt;/span&gt;
◉  sqpuoqvx agent-2@example.com 2026-01-05 15:29:45 op_def456
│  edit src/auth.js
◉  rqxostpw agent-1@example.com 2026-01-05 15:29:30 op_ghi789
   new &lt;span class="nt"&gt;--after&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Record Prompts in Commit Messages
&lt;/h3&gt;

&lt;p&gt;I tend to include the prompts I give to the AI directly in the commit messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jj describe &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Prompt: Add user authentication with JWT.
Implementation includes:
- Login endpoint with email/password
- Token generation and validation
- Middleware for protected routes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, if I later wonder, "Why is this code like this?" I can refer back to the prompt, which is very convenient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restoring to Specific Operations
&lt;/h3&gt;

&lt;p&gt;If I want to roll back because "the AI's output was not quite right," I can easily revert:&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;# Return to a past operational state&lt;/span&gt;
jj op restore op_abc123

&lt;span class="c"&gt;# Return to the current state&lt;/span&gt;
jj op restore @
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Git, you would struggle with &lt;code&gt;git reflog&lt;/code&gt;, but jj is more intuitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming Branches Later with Bookmarks
&lt;/h2&gt;

&lt;p&gt;This feature also suits vibe coding well. You can "start working and decide on a name later."&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;# Start working anonymously (no branch name needed)&lt;/span&gt;
jj new main
&lt;span class="c"&gt;# Implement with Claude Code...&lt;/span&gt;

&lt;span class="c"&gt;# Name it after completion&lt;/span&gt;
jj bookmark create user-auth-feature

&lt;span class="c"&gt;# Push for GitHub PR&lt;/span&gt;
jj git push &lt;span class="nt"&gt;--bookmark&lt;/span&gt; user-auth-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Git, you have to decide on a branch name upfront with &lt;code&gt;git checkout -b feature-xxx&lt;/code&gt;, but when having the AI implement, you often don’t know what you’ll end up with until the end. jj eliminates that issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Continue Using jj?
&lt;/h2&gt;

&lt;p&gt;Honestly, it’s a bit tricky to recommend it to everyone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who jj is Suitable For
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Developers using AI agents in parallel&lt;/li&gt;
&lt;li&gt;Environments where conflicts frequently occur (often editing the same files)&lt;/li&gt;
&lt;li&gt;Exploratory development involving trial and error&lt;/li&gt;
&lt;li&gt;Frequent history shaping and editing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Who Should Stick with git worktree
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Teams where everyone is familiar with Git (to avoid learning costs)&lt;/li&gt;
&lt;li&gt;Those who prioritize IDE Git integration (jj's IDE support is still developing)&lt;/li&gt;
&lt;li&gt;Environments where conflicts rarely occur&lt;/li&gt;
&lt;li&gt;Those who only need 1-2 parallel instances&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I was stuck with the "everything stops due to conflicts" issue while doing parallel development with git worktree, and switching to jj resolved that.&lt;/p&gt;

&lt;p&gt;To summarize the advantages of jj:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Conflicts do not block work&lt;/strong&gt;: They are recorded, allowing work to continue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility of state&lt;/strong&gt;: All changes are visible from all workspaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic rebase&lt;/strong&gt;: Changes in dependencies are automatically propagated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a learning curve, but for those who want to avoid letting the AI sit idle during vibe coding, it’s worth trying. Since it’s Git-compatible, you can always revert if you don’t like it.&lt;/p&gt;

&lt;p&gt;Start by trying &lt;code&gt;jj git init --git-repo .&lt;/code&gt; in an existing project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jj-vcs.dev/" rel="noopener noreferrer"&gt;Jujutsu Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jj-vcs/jj" rel="noopener noreferrer"&gt;Jujutsu GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.jj-vcs.dev/latest/git-comparison/" rel="noopener noreferrer"&gt;Comparison of Jujutsu vs Git&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://v5.chriskrycho.com/essays/jj-init/" rel="noopener noreferrer"&gt;jj init - Chris Krycho&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
    </item>
    <item>
      <title>Quantifying the "Vague Anxiety" of Tailscale: tailsnitch Exposes 50 Configuration Mistakes</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Thu, 05 Feb 2026 12:41:22 +0000</pubDate>
      <link>https://forem.com/tumf/quantifying-the-vague-anxiety-of-tailscale-tailsnitch-exposes-50-configuration-mistakes-1cm9</link>
      <guid>https://forem.com/tumf/quantifying-the-vague-anxiety-of-tailscale-tailsnitch-exposes-50-configuration-mistakes-1cm9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-01-07&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/1/7/tailsnitch-tailscale-security-audit/" rel="noopener noreferrer"&gt;Tailscaleの『なんとなく不安』を数値化する：tailsnitchが暴く50の設定ミス&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In September 2025, Asahi Group Holdings suffered a ransomware attack. The entry point was a vulnerability in their VPN device. In October, Askul was breached through a VPN account of a contracted service provider, leading to the shutdown of their e-commerce site.&lt;/p&gt;

&lt;p&gt;Both incidents were the result of the misconception that "as long as we have a VPN, we are safe."&lt;/p&gt;

&lt;p&gt;If you have implemented Tailscale, you might be thinking, "It should be safer than traditional VPNs, so I'm fine."&lt;/p&gt;

&lt;p&gt;Indeed, Tailscale has advantages over traditional on-premises VPN gateways. With lightweight and audited encryption via WireGuard, device-level zero-trust authentication, and a SaaS architecture that eliminates the risk of exploiting VPN device vulnerabilities—these designs address the weaknesses of traditional VPNs.&lt;/p&gt;

&lt;p&gt;However, Tailscale can also be dangerous if misconfigured. Leaving default ACLs unchecked can allow unrestricted access to all devices, and if reusable authentication keys are leaked, attackers can add unauthorized devices. &lt;a href="https://github.com/Adversis/tailsnitch" rel="noopener noreferrer"&gt;tailsnitch&lt;/a&gt; is a security auditing tool that quantifies such "vague anxiety" with over 50 checks, evaluating them on a scale of Critical/High/Medium/Low/Info.&lt;/p&gt;

&lt;p&gt;In this article, we will explain how to use tailsnitch and the dangerous configuration mistakes it can detect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is tailsnitch?
&lt;/h2&gt;

&lt;p&gt;tailsnitch is an open-source tool that automatically audits the configuration of a &lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; network (tailnet). Released on December 24, 2025, it garnered over 430 GitHub Stars in just two weeks.&lt;/p&gt;

&lt;p&gt;Key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Over 50 security checks&lt;/strong&gt;: 7 categories including ACLs, authentication keys, devices, network exposure, SSH, logs, and DNS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Severity rating in 5 levels&lt;/strong&gt;: Critical → High → Medium → Low → Info&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOC 2 audit trail output&lt;/strong&gt;: CSV/JSON output mapped to controls like CC6.1, CC6.2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD integration&lt;/strong&gt;: Automatic checks during PRs with GitHub Actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive remediation mode&lt;/strong&gt;: Fix configuration mistakes with the &lt;code&gt;--fix&lt;/code&gt; flag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developed by the security company &lt;a href="https://www.adversis.io/" rel="noopener noreferrer"&gt;Adversis&lt;/a&gt;, a &lt;a href="https://github.com/Adversis/tailsnitch/blob/main/HARDENING_TAILSCALE.md" rel="noopener noreferrer"&gt;Tailscale Hardening Guide&lt;/a&gt; is also available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;You can install tailsnitch using one of the following methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prebuilt Binary (Recommended)
&lt;/h3&gt;

&lt;p&gt;Download the latest version from &lt;a href="https://github.com/Adversis/tailsnitch/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;:&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;# Remove quarantine attribute for macOS&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;xattr &lt;span class="nt"&gt;-rd&lt;/span&gt; com.apple.quarantine tailsnitch
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x tailsnitch
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;tailsnitch /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install with Go
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/Adversis/tailsnitch@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build from Source
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Adversis/tailsnitch.git
&lt;span class="nb"&gt;cd &lt;/span&gt;tailsnitch
go build &lt;span class="nt"&gt;-o&lt;/span&gt; tailsnitch &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Authentication Setup
&lt;/h2&gt;

&lt;p&gt;tailsnitch uses the Tailscale API, so you need authentication credentials. You can use either an OAuth Client (recommended) or an API Key.&lt;/p&gt;

&lt;h3&gt;
  
  
  OAuth Client (Recommended)
&lt;/h3&gt;

&lt;p&gt;The OAuth Client allows you to restrict permissions with scopes and is logged in the audit logs. There is no risk of API keys being invalidated when employees leave.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an OAuth Client at &lt;a href="https://login.tailscale.com/admin/settings/oauth" rel="noopener noreferrer"&gt;https://login.tailscale.com/admin/settings/oauth&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Grant the following scopes for read-only auditing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;all:read&lt;/code&gt; (the easiest)&lt;/li&gt;
&lt;li&gt;Or individually: &lt;code&gt;policy_file:read&lt;/code&gt;, &lt;code&gt;devices:core:read&lt;/code&gt;, &lt;code&gt;dns:read&lt;/code&gt;, &lt;code&gt;auth_keys:read&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set the environment variables:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TS_OAUTH_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TS_OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tskey-client-..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  API Key
&lt;/h3&gt;

&lt;p&gt;The API Key inherits the permissions of the user who created it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an API Key at &lt;a href="https://login.tailscale.com/admin/settings/keys" rel="noopener noreferrer"&gt;https://login.tailscale.com/admin/settings/keys&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Set the environment variable:
&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;export &lt;/span&gt;&lt;span class="nv"&gt;TSKEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tskey-api-..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Basic Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Audit All Items
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;Example of the first run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+=====================================================================+
|                    TAILSNITCH SECURITY AUDIT                        |
|            Tailnet: example.com                                     |
|            Version: 1.4.0 (build: d717661)                          |
+=====================================================================+

=== ACCESS CONTROLS ===================================================

[CRITICAL] ACL-001: Default 'allow all' policy active
  Your ACL policy omits the 'acls' field. Tailscale applies a
  default 'allow all' policy, granting all devices full access.

  Remediation:
  Define explicit ACL rules following least privilege principle.

  Source: https://tailscale.com/kb/1192/acl-samples
----------------------------------------------------------------------

[HIGH] AUTH-001: Reusable auth keys exist
  Found 2 reusable auth key(s). These can be reused to add
  multiple devices if compromised.

  Details:
    - Key tskey-auth-xxx (expires in 45 days)
    - Key tskey-auth-yyy (expires in 89 days)

  Remediation:
  Store reusable keys in a secrets manager. Prefer one-off keys.
----------------------------------------------------------------------

SUMMARY
======================================================================
  Critical: 1  High: 3  Medium: 5  Low: 2  Info: 8
  Total findings: 19  |  Passed: 33
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Filter by Severity
&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;# Show only Critical/High&lt;/span&gt;
tailsnitch &lt;span class="nt"&gt;--severity&lt;/span&gt; high

&lt;span class="c"&gt;# Specific categories only&lt;/span&gt;
tailsnitch &lt;span class="nt"&gt;--category&lt;/span&gt; access   &lt;span class="c"&gt;# ACL issues&lt;/span&gt;
tailsnitch &lt;span class="nt"&gt;--category&lt;/span&gt; auth     &lt;span class="c"&gt;# Authentication key issues&lt;/span&gt;
tailsnitch &lt;span class="nt"&gt;--category&lt;/span&gt; device   &lt;span class="c"&gt;# Device security&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JSON Output and Aggregation with jq
&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;# Output all results in JSON&lt;/span&gt;
tailsnitch &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; audit.json

&lt;span class="c"&gt;# Extract only failed checks&lt;/span&gt;
tailsnitch &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  .suggestions
  | map(select(.pass == false))
  | .[]
  | [.id, .title, .severity, .remediation]
  | @tsv
'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; findings.tsv

&lt;span class="c"&gt;# Aggregate by severity&lt;/span&gt;
tailsnitch &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'
  .suggestions
  | map(select(.pass == false))
  | group_by(.severity)
  | map({severity: .[0].severity, count: length})
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRITICAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MEDIUM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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;h2&gt;
  
  
  Dangerous Configuration Mistakes Detected
&lt;/h2&gt;

&lt;p&gt;Here are some representative issues that tailsnitch can detect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Critical: Leaving Default ACLs Unchecked
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: If the ACL policy lacks the &lt;code&gt;acls&lt;/code&gt; field, Tailscale applies a default 'allow all' policy, granting all devices full access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[CRITICAL] ACL-001: Default 'allow all' policy active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Unlimited access from a developer's laptop to the production database&lt;/li&gt;
&lt;li&gt;A single compromised device puts the entire tailnet at risk&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Define minimal ACLs:&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:engineering"&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;"alice@company.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:devops"&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;"charlie@company.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:dev"&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;"autogroup:admin"&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:prod"&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;"autogroup: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;"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:engineering"&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;"tag:dev:443"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tag:dev:8080"&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:devops"&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;"tag:prod:22"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tag:prod:443"&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;h3&gt;
  
  
  High: Reusable Authentication Keys Exist
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: If reusable authentication keys are leaked, attackers can add devices without restriction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HIGH] AUTH-001: Reusable auth keys exist
  Found 2 reusable auth key(s):
    - Key tskey-auth-xxx (expires in 45 days)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Breach through an authentication key committed to a GitHub repository&lt;/li&gt;
&lt;li&gt;Unauthorized device addition with keys stolen from a CI/CD pipeline&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;Delete existing reusable keys&lt;/li&gt;
&lt;li&gt;Switch to ephemeral (temporary) keys:
&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;# Generate an ephemeral key (usable once)&lt;/span&gt;
tailscale up &lt;span class="nt"&gt;--authkey&lt;/span&gt; tskey-auth-xxx &lt;span class="nt"&gt;--ephemeral&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Use OAuth Client in CI/CD&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  High: Tailnet Lock Disabled
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: If Tailnet Lock is disabled, an attacker can add unauthorized devices if the Tailscale coordination server is compromised.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HIGH] DEV-010: Tailnet Lock disabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Trust in the control plane is required&lt;/li&gt;
&lt;li&gt;Risk of man-in-the-middle attacks by advanced attackers&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Enable Tailnet Lock (requires a signing node):&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;# Initialize lock on a trusted node&lt;/span&gt;
tailscale lock init tlpub:&amp;lt;SIGNING_NODE_KEY&amp;gt;

&lt;span class="c"&gt;# New devices will require signing&lt;/span&gt;
tailscale lock sign nodekey:&amp;lt;NEW_NODE_KEY&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Tailnet Lock can impose operational burdens, hence it is suited for defense industries or companies with strict compliance requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Medium: Outdated Clients Detected
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Older Tailscale clients may have known vulnerabilities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[MEDIUM] DEV-003: Outdated clients detected
  Found 3 devices running Tailscale &amp;lt; 1.50.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Enforce version checks with Device Posture:&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;"postures"&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;"posture:baseline"&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;"node:tsVersion &amp;gt;= '1.50.0'"&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;"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:devops"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"srcPosture"&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;"posture:baseline"&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;"tag:prod:22"&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;h3&gt;
  
  
  Medium: Stale Devices Detected
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Devices that have not been used for over 60 days pose a risk of being compromised if they belong to former employees.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[MEDIUM] DEV-004: Stale devices detected
  Found 5 devices not seen in 60+ days
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Interactively delete with &lt;code&gt;--fix&lt;/code&gt; mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailsnitch &lt;span class="nt"&gt;--fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or delete manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale &lt;span class="nb"&gt;logout&lt;/span&gt; &lt;span class="nt"&gt;--device&lt;/span&gt; &amp;lt;device-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interactive Remediation Mode (&lt;code&gt;--fix&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Using the &lt;code&gt;--fix&lt;/code&gt; flag allows you to interactively correct issues that can be fixed via the API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailsnitch &lt;span class="nt"&gt;--fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixable items include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Remediation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AUTH-001, AUTH-002, AUTH-003&lt;/td&gt;
&lt;td&gt;Delete authentication keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AUTH-004&lt;/td&gt;
&lt;td&gt;Replace with ephemeral keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEV-002&lt;/td&gt;
&lt;td&gt;Remove tags from user devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEV-004&lt;/td&gt;
&lt;td&gt;Delete stale devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DEV-005&lt;/td&gt;
&lt;td&gt;Approve unauthorized devices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For items requiring manual intervention, links to the management console will be displayed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dry Run&lt;/strong&gt; (preview changes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailsnitch &lt;span class="nt"&gt;--fix&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  SOC 2 Audit Trail Output
&lt;/h2&gt;

&lt;p&gt;tailsnitch can output the necessary audit trails for SOC 2 in CSV/JSON format.&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;# CSV format&lt;/span&gt;
tailsnitch &lt;span class="nt"&gt;--soc2&lt;/span&gt; csv &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; soc2-evidence.csv

&lt;span class="c"&gt;# JSON format&lt;/span&gt;
tailsnitch &lt;span class="nt"&gt;--soc2&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; soc2-evidence.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output (CSV):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource_type,resource_id,resource_name,check_id,check_title,cc_codes,status,details,tested_at
device,node123,prod-server,DEV-001,Tagged devices with key expiry disabled,CC6.1;CC6.3,PASS,Tags: [tag:server] key expiry enabled,2025-01-05T10:30:00Z
key,tskey-auth-xxx,tskey-auth-xxx,AUTH-001,Reusable auth keys exist,CC6.1;CC6.2;CC6.3,FAIL,Reusable key expires in 45 days,2025-01-05T10:30:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each check is mapped to the following SOC 2 controls (CC):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CC6.1&lt;/strong&gt;: Logical Access Controls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CC6.2&lt;/strong&gt;: Granting Access Rights&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CC6.3&lt;/strong&gt;: Removing Access Rights&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CC6.6&lt;/strong&gt;: Network Segmentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CC7.1&lt;/strong&gt;: Detection of Security Events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CC7.2&lt;/strong&gt;: Monitoring Security Incidents&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automated Auditing in CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;Example of running automatic checks during ACL changes with GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/tailscale-acl.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tailscale ACL CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;policy.hujson'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;policy.hujson'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test-acl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tailsnitch&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;TS_OAUTH_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TS_OAUTH_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;TS_OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TS_OAUTH_CLIENT_SECRET }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -L https://github.com/Adversis/tailsnitch/releases/latest/download/tailsnitch-linux-amd64 -o tailsnitch&lt;/span&gt;
          &lt;span class="s"&gt;chmod +x tailsnitch&lt;/span&gt;
          &lt;span class="s"&gt;./tailsnitch --severity high --json &amp;gt; audit.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Fail on critical issues&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if ./tailsnitch --severity high --json | jq -e '.summary.critical + .summary.high &amp;gt; 0' &amp;gt; /dev/null; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Critical or high severity issues found!"&lt;/span&gt;
            &lt;span class="s"&gt;./tailsnitch --severity high&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this configuration, security checks will automatically run on PRs for ACL changes, blocking merges if Critical/High issues are found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing Configuration Mistakes with ACL Tests
&lt;/h2&gt;

&lt;p&gt;While tailsnitch is a detection tool, adding a &lt;code&gt;tests&lt;/code&gt; field within your ACLs can help prevent configuration mistakes from occurring in the first place.&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;"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:engineering"&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;"tag:dev:443"&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;"tests"&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;"src"&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:engineering"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"deny"&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:prod:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tag:prod-db:5432"&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;"src"&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:devops"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"accept"&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:bastion:22"&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;If tests fail, ACL changes will be rejected. This helps prevent configuration mistakes before they are detected by tailsnitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Danger of &lt;code&gt;autogroup:member&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;autogroup:member&lt;/code&gt; includes all users participating in the tailnet. Since it also includes external users (Shared Nodes), it can unintentionally grant access rights.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad Example&lt;/strong&gt;:&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;"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;"autogroup:member"&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;"tag:staging:*"&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;strong&gt;Good Example&lt;/strong&gt;:&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;"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:engineering"&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;"tag:staging:443"&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;h3&gt;
  
  
  Overreliance on Subnet Routers
&lt;/h3&gt;

&lt;p&gt;While Subnet Routers are convenient, if compromised, they can provide access to a wide range of networks.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Enable Stateful Filtering:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale up &lt;span class="nt"&gt;--advertise-routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.0.0.0/24 &lt;span class="nt"&gt;--stateful-filtering&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Protect the Subnet Router itself with security groups or NACLs.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Trap of SSH &lt;code&gt;autogroup:nonroot&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;autogroup:nonroot&lt;/code&gt; allows SSH access for all users except &lt;code&gt;root&lt;/code&gt;, but it also includes users with &lt;code&gt;sudo&lt;/code&gt; privileges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad Example&lt;/strong&gt;:&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;"ssh"&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:engineering"&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;"tag:prod"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"users"&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;"autogroup:nonroot"&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;strong&gt;Good Example&lt;/strong&gt;:&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;"ssh"&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:devops"&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;"tag:prod"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"users"&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;"deploy"&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;h2&gt;
  
  
  Regular Audit Operations
&lt;/h2&gt;

&lt;p&gt;Here are operational guidelines for continuously utilizing tailsnitch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weekly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Run tailsnitch to check for Critical/High issues&lt;/li&gt;
&lt;li&gt;[ ] Review the device approval queue&lt;/li&gt;
&lt;li&gt;[ ] Remove unused authentication keys&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Monthly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Cross-check group memberships with the employee roster&lt;/li&gt;
&lt;li&gt;[ ] Ensure devices of former employees have been removed&lt;/li&gt;
&lt;li&gt;[ ] Review ACL change history&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quarterly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Audit all access rights (who can access what)&lt;/li&gt;
&lt;li&gt;[ ] Review third-party access&lt;/li&gt;
&lt;li&gt;[ ] Reassess Subnet Router configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Upon Employee Departure (Immediately)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Remove from Tailscale groups&lt;/li&gt;
&lt;li&gt;[ ] Delete user from tailnet&lt;/li&gt;
&lt;li&gt;[ ] Remove created authentication keys&lt;/li&gt;
&lt;li&gt;[ ] Delete devices&lt;/li&gt;
&lt;li&gt;[ ] Audit recent ACL changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While Tailscale addresses many weaknesses of traditional VPNs at the design level, risks from configuration mistakes still exist. By using tailsnitch, you can visualize "vague anxiety" as concrete issues and prioritize addressing them.&lt;/p&gt;

&lt;p&gt;Setup takes just five minutes, and integrating it into your CI/CD pipeline allows for automatic checks with every ACL change. You can also output audit trails for SOC 2 compliance.&lt;/p&gt;

&lt;p&gt;Personally, I believe that every organization using Tailscale should run tailsnitch at least once a month. Do not leave Critical/High issues unaddressed until they reach zero, and add ACL tests to prevent recurrence—by thoroughly implementing these two practices, you can significantly reduce the risk of large-scale incidents from VPN breaches.&lt;/p&gt;

&lt;p&gt;If you're interested, try running tailsnitch on your own tailnet. You might discover unexpected configuration mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Adversis/tailsnitch" rel="noopener noreferrer"&gt;tailsnitch GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/kb/1196/security-hardening" rel="noopener noreferrer"&gt;Tailscale Security Hardening Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/kb/1337/policy-syntax" rel="noopener noreferrer"&gt;Tailscale ACL Policy Syntax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Adversis/tailsnitch/blob/main/HARDENING_TAILSCALE.md" rel="noopener noreferrer"&gt;Tailscale Hardening Checklist (included with tailsnitch)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=46501137" rel="noopener noreferrer"&gt;Show HN: Tailsnitch – A security auditor for Tailscale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.asahigroup-holdings.com/newsroom/detail/20251127-0104.html" rel="noopener noreferrer"&gt;Asahi Group HD Official Investigation Results on Cyber Attacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.askul.co.jp/corp/security/" rel="noopener noreferrer"&gt;Askul Cybersecurity (Official)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>jj-desc: Release of the Rust-based jj Commit Message Generation Tool</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Thu, 05 Feb 2026 12:39:19 +0000</pubDate>
      <link>https://forem.com/tumf/jj-desc-release-of-the-rust-based-jj-commit-message-generation-tool-3mpf</link>
      <guid>https://forem.com/tumf/jj-desc-release-of-the-rust-based-jj-commit-message-generation-tool-3mpf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-01-08&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/1/8/jj-desc-release/" rel="noopener noreferrer"&gt;jj-desc: Rust製のjjコミットメッセージ自動生成ツールをリリース&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We have released a CLI tool called &lt;a href="https://github.com/tumf/jj-desc" rel="noopener noreferrer"&gt;jj-desc&lt;/a&gt; that automatically generates commit messages for Jujutsu (jj) using LLMs.&lt;/p&gt;

&lt;p&gt;jj is a Git-compatible version control tool developed by Google, known for its powerful undo functionality and flexible commit operations through revset (revision set). The generated commit messages adhere to the &lt;a href="https://www.conventionalcommits.org/" rel="noopener noreferrer"&gt;Conventional Commits&lt;/a&gt; format. A significant feature of jj-desc is its ability to generate multiple commit messages in bulk, leveraging the unique characteristics of jj.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main Features of jj-desc
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Support for Multiple LLM Providers
&lt;/h3&gt;

&lt;p&gt;jj-desc supports the following LLM providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://openrouter.ai/" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; (default)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://console.anthropic.com/" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;Google Gemini&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Custom endpoints (Azure OpenAI, &lt;a href="https://ollama.ai/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can easily switch between them using environment variables or CLI options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LLM_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;anthropic
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-ant-..."&lt;/span&gt;
jj-desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Backfill Functionality Utilizing revset
&lt;/h3&gt;

&lt;p&gt;The greatest strength of jj-desc is its &lt;strong&gt;backfill&lt;/strong&gt; functionality, which allows for bulk generation of commit messages by utilizing &lt;strong&gt;revset&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Revset is jj's unique query language that allows for flexible specification of a set of commits (revisions). It provides powerful features not found in Git, enabling you to freely select targets by combining set operations (&lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;~&lt;/code&gt;) and condition filters.&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;# Current commit&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-r&lt;/span&gt; @

&lt;span class="c"&gt;# Process all of my commits in bulk&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"mine()"&lt;/span&gt;

&lt;span class="c"&gt;# Range from the main branch to HEAD&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"@..main"&lt;/span&gt;

&lt;span class="c"&gt;# Mutable commits without descriptions (default)&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"::@ &amp;amp; mutable()"&lt;/span&gt;

&lt;span class="c"&gt;# Process 5 commits without descriptions in bulk&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"::@ &amp;amp; mutable()"&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why is backfill important?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During development, it's common to "commit for now" and want to organize messages later. In Git, rewriting commit messages with &lt;code&gt;git rebase -i&lt;/code&gt; can be cumbersome. Tools like aicommits primarily generate messages for "the content being committed now."&lt;/p&gt;

&lt;p&gt;jj-desc is different. By combining jj's editable commit history with revset, it allows for &lt;strong&gt;bulk message generation for past commits&lt;/strong&gt;. You can focus on your work, accumulate commits, and then simply run &lt;code&gt;jj-desc&lt;/code&gt; when you’re ready. This is the true value of jj-desc.&lt;/p&gt;

&lt;p&gt;The following demo shows &lt;code&gt;jj-desc&lt;/code&gt; being executed on multiple commits without descriptions, generating messages in bulk:&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%2Fiayfud22dfd0hxz83rq9.gif" 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%2Fiayfud22dfd0hxz83rq9.gif" alt="jj-desc demo - backfill multiple commits" width="760" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Immediate Application Without Confirmation Prompts
&lt;/h3&gt;

&lt;p&gt;jj-desc applies the generated commit messages &lt;strong&gt;immediately without confirmation&lt;/strong&gt;. While this may seem bold, it is designed with jj's powerful undo functionality (&lt;code&gt;jj undo&lt;/code&gt;, &lt;code&gt;jj op log&lt;/code&gt;) in mind.&lt;/p&gt;

&lt;p&gt;In Git tools, it's common to ask, "Do you really want to apply this?" However, in the jj ecosystem, the natural workflow is "try it, and if it doesn't work, undo." All operations are recorded in history and can be easily reverted, making confirmation prompts an unnecessary friction.&lt;/p&gt;

&lt;p&gt;Of course, if you prefer to confirm carefully, options like &lt;code&gt;--dry-run&lt;/code&gt; (preview) and &lt;code&gt;-i&lt;/code&gt; (interactive mode) are also available:&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;# Preview only (do not apply)&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# Apply while confirming one by one&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"mine()"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Diff Optimization and Token Savings
&lt;/h3&gt;

&lt;p&gt;Before sending diffs to the LLM, the following optimizations are automatically performed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic exclusion of lock files (&lt;code&gt;Cargo.lock&lt;/code&gt;, &lt;code&gt;package-lock.json&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Simplification of binary files (&lt;code&gt;Binary file {path} changed&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;User-specified exclusion patterns (using the &lt;code&gt;--exclude&lt;/code&gt; option)&lt;/li&gt;
&lt;li&gt;Warning for diffs exceeding 50KB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This balances cost reduction for LLM API calls and avoidance of context limits.&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;# Exclude specific files&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s2"&gt;"*.json"&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s2"&gt;"*.yaml"&lt;/span&gt;

&lt;span class="c"&gt;# Shortened form&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"docs/*"&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"*.lock"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Automatic Detection of Merge Commits
&lt;/h3&gt;

&lt;p&gt;In jj, many merge commits are treated as "empty" (&lt;a href="https://docs.jj-vcs.dev/latest/FAQ/#why-are-most-merge-commits-marked-as-empty" rel="noopener noreferrer"&gt;see jj FAQ&lt;/a&gt;). jj-desc automatically detects merge commits and sets appropriate descriptions ("Merge commit") without needing an LLM API call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Homebrew (Recommended)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;tumf/tap/jj-desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prebuilt Binaries
&lt;/h3&gt;

&lt;p&gt;You can obtain binaries for each platform from the &lt;a href="https://github.com/tumf/jj-desc/releases/latest" rel="noopener noreferrer"&gt;release page&lt;/a&gt;:&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;# macOS (Apple Silicon)&lt;/span&gt;
curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-LsSf&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/tumf/jj-desc/releases/latest/download/jj-desc-installer.sh | sh

&lt;span class="c"&gt;# Linux (x86_64)&lt;/span&gt;
curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 &lt;span class="nt"&gt;-LsSf&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/tumf/jj-desc/releases/latest/download/jj-desc-installer.sh | sh

&lt;span class="c"&gt;# Windows (PowerShell)&lt;/span&gt;
powershell &lt;span class="nt"&gt;-ExecutionPolicy&lt;/span&gt; Bypass &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"irm https://github.com/tumf/jj-desc/releases/latest/download/jj-desc-installer.ps1 | iex"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build from Cargo
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--git&lt;/span&gt; https://github.com/tumf/jj-desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Basic Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Setting Up LLM Provider
&lt;/h3&gt;

&lt;p&gt;First, set the API key for the LLM provider you wish to use:&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;# OpenRouter (default)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-api-key"&lt;/span&gt;

&lt;span class="c"&gt;# Or OpenAI&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LLM_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openai
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-..."&lt;/span&gt;

&lt;span class="c"&gt;# Or Anthropic&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LLM_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;anthropic
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-ant-..."&lt;/span&gt;

&lt;span class="c"&gt;# Ollama (local LLM)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LLM_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openai
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dummy"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:11434/v1"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"llama2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generating Commit Messages
&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;# Default: Process all mutable commits without descriptions&lt;/span&gt;
jj-desc

&lt;span class="c"&gt;# Only the current commit&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-r&lt;/span&gt; @

&lt;span class="c"&gt;# All of my commits&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"mine()"&lt;/span&gt;

&lt;span class="c"&gt;# Preview&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# Interactive mode&lt;/span&gt;
jj-desc &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Differences from aicommit2
&lt;/h2&gt;

&lt;p&gt;As an AI commit message generation tool for jj, &lt;a href="https://github.com/tak-bro/aicommit2" rel="noopener noreferrer"&gt;aicommit2&lt;/a&gt; has been around longer. aicommit2 is a TypeScript-based general tool that supports Git, YADM, and jj.&lt;/p&gt;

&lt;p&gt;The main difference between the two is the presence of the &lt;strong&gt;backfill functionality&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;Feature&lt;/th&gt;
&lt;th&gt;jj-desc&lt;/th&gt;
&lt;th&gt;aicommit2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Backfill&lt;/strong&gt; (bulk processing of past commits)&lt;/td&gt;
&lt;td&gt;✅ Freely specified with revset&lt;/td&gt;
&lt;td&gt;❌ Current commits only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supported VCS&lt;/td&gt;
&lt;td&gt;jj only&lt;/td&gt;
&lt;td&gt;Git, YADM, jj&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implementation Language&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirmation Prompt&lt;/td&gt;
&lt;td&gt;None (based on undo)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diff Optimization&lt;/td&gt;
&lt;td&gt;✅ Automatic filtering&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;aicommit2 is geared towards a "committing now" workflow. jj-desc is designed for a workflow where you "commit in bulk and organize messages later."&lt;/p&gt;

&lt;p&gt;If you want to leverage jj's features to the fullest, use jj-desc; if you want to integrate with existing Git workflows, aicommit2 is recommended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;jj-desc's standout feature is its &lt;strong&gt;backfill functionality&lt;/strong&gt; utilizing jj's revset. Focus on your work, accumulate commits, and generate messages later—this workflow is unique to jj and cannot be achieved with Git tools.&lt;/p&gt;

&lt;p&gt;If you're a jj user, be sure to give it a try. We welcome feedback and feature suggestions on our &lt;a href="https://github.com/tumf/jj-desc" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tumf/jj-desc" rel="noopener noreferrer"&gt;jj-desc GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://martinvonz.github.io/jj/" rel="noopener noreferrer"&gt;Jujutsu (jj) Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tak-bro/aicommit2" rel="noopener noreferrer"&gt;aicommit2&lt;/a&gt; - AI commit tool supporting Git/YADM/jj&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openrouter.ai/" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://console.anthropic.com/" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>cron, anacron, and systemd timer: Guidelines for Choosing Between Three Linux Task Schedulers</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Thu, 05 Feb 2026 12:38:09 +0000</pubDate>
      <link>https://forem.com/tumf/cron-anacron-and-systemd-timer-guidelines-for-choosing-between-three-linux-task-schedulers-g7o</link>
      <guid>https://forem.com/tumf/cron-anacron-and-systemd-timer-guidelines-for-choosing-between-three-linux-task-schedulers-g7o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-02-04&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/2/4/linux-task-scheduler-comparison/" rel="noopener noreferrer"&gt;cron・anacron・systemd timer: Linuxタスクスケジューラ3種の使い分け基準&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"I want to run backups regularly" or "I want to rotate logs at midnight"—there are three methods to automate tasks in Linux: &lt;a href="https://man7.org/linux/man-pages/man8/cron.8.html" rel="noopener noreferrer"&gt;cron&lt;/a&gt;, &lt;a href="https://man7.org/linux/man-pages/man8/anacron.8.html" rel="noopener noreferrer"&gt;anacron&lt;/a&gt;, and &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html" rel="noopener noreferrer"&gt;systemd timer&lt;/a&gt;. However, many people may wonder, "Which one should I use?"&lt;/p&gt;

&lt;p&gt;In this article, we will clarify the characteristics of these three schedulers and provide criteria for deciding which one to choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Characteristics of the Three Schedulers
&lt;/h2&gt;

&lt;p&gt;First, let's outline the basic features of each.&lt;/p&gt;

&lt;h3&gt;
  
  
  cron: A Scheduler with Strict Time Specifications
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://man7.org/linux/man-pages/man8/cron.8.html" rel="noopener noreferrer"&gt;cron&lt;/a&gt; is the oldest task scheduler used in UNIX-like operating systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict Time Specification&lt;/strong&gt;: Executes jobs at specific times, such as "every day at 2 AM" or "every Monday at 9 AM."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minute-Level Precision&lt;/strong&gt;: The smallest unit is 1 minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Designed for Continuous Operation&lt;/strong&gt;: Assumes the system is running 24/7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple Configuration&lt;/strong&gt;: Can be intuitively set up in crontab format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configuration Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Backup every day at 2 AM
0 2 * * * /usr/local/bin/backup.sh

# Generate report every Monday at 9 AM
0 9 * * 1 /usr/local/bin/generate-report.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Suitable Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Machines that are always running, such as servers.&lt;/li&gt;
&lt;li&gt;Jobs that require strict time specifications (e.g., maintenance outside of business hours).&lt;/li&gt;
&lt;li&gt;Detailed schedules at minute intervals (e.g., monitoring every 5 minutes).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Jobs during system downtime will not be executed.&lt;/li&gt;
&lt;li&gt;The next job can start even if the previous one has not finished (risk of overlapping execution).&lt;/li&gt;
&lt;li&gt;Checking logs can be cumbersome (need to look in &lt;code&gt;/var/log/syslog&lt;/code&gt; or &lt;code&gt;/var/log/cron&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  anacron: A Daily Scheduler That Doesn't Miss Jobs When Powered Off
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://man7.org/linux/man-pages/man8/anacron.8.html" rel="noopener noreferrer"&gt;anacron&lt;/a&gt; is designed for machines that are not always running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frequency-Based Execution&lt;/strong&gt;: Specifies execution based on frequency, such as "once a day" or "once every 7 days."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compensation for Missed Jobs&lt;/strong&gt;: Automatically executes jobs that have not run during system startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum Unit is a Day&lt;/strong&gt;: Minute-level specifications are not possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a Daemon&lt;/strong&gt;: Typically operates by being called periodically from cron or systemd.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configuration Example (&lt;code&gt;/etc/anacrontab&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# period  delay  job-id  command
1         5      cron.daily    run-parts /etc/cron.daily
7         10     cron.weekly   run-parts /etc/cron.weekly
@monthly  15     cron.monthly  run-parts /etc/cron.monthly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How It Works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;anacron records the last execution time of jobs in &lt;code&gt;/var/spool/anacron/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;At system startup (or periodically from cron), it checks for missed jobs.&lt;/li&gt;
&lt;li&gt;Executes the jobs after the specified delay time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Suitable Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Laptops or desktops that have periods of being powered off.&lt;/li&gt;
&lt;li&gt;Daily, weekly, or monthly maintenance tasks.&lt;/li&gt;
&lt;li&gt;Jobs that do not require strict time specifications (e.g., backups, package updates).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Minute-level detailed scheduling is not possible.&lt;/li&gt;
&lt;li&gt;Cannot specify strict times (only delay time after startup can be specified).&lt;/li&gt;
&lt;li&gt;Standard settings often require root operation (when using &lt;code&gt;/etc/anacrontab&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  systemd timer: A Modern Integrated Scheduler
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html" rel="noopener noreferrer"&gt;systemd timer&lt;/a&gt; is a task scheduler provided as part of systemd.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flexible Schedule Specification&lt;/strong&gt;: Allows for various specifications, including strict times, relative times, and elapsed time after startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compensation for Missed Jobs&lt;/strong&gt;: Can operate similarly to anacron with &lt;code&gt;Persistent=true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration with systemd&lt;/strong&gt;: Unified handling of service management, logging, and dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random Delay&lt;/strong&gt;: Can randomize execution times with &lt;code&gt;RandomizedDelaySec&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configuration Example:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Timer file (&lt;code&gt;backup.timer&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Daily backup timer&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;daily&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1h&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Service file (&lt;code&gt;backup.service&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Backup service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/backup.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Suitable Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Systems that adopt systemd.&lt;/li&gt;
&lt;li&gt;Complex scheduling requirements (e.g., executing 24 hours after the last run, or 15 minutes after startup).&lt;/li&gt;
&lt;li&gt;When there are dependencies between services.&lt;/li&gt;
&lt;li&gt;When centralized logging is necessary.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Not usable on systems that do not use systemd.&lt;/li&gt;
&lt;li&gt;Requires two configuration files (&lt;code&gt;.timer&lt;/code&gt; and &lt;code&gt;.service&lt;/code&gt;), resulting in more verbosity compared to cron.&lt;/li&gt;
&lt;li&gt;Slightly higher learning curve.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparison Table of the Three Schedulers
&lt;/h2&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;cron&lt;/th&gt;
&lt;th&gt;anacron&lt;/th&gt;
&lt;th&gt;systemd timer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Minimum Unit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 minute&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;Less than a second (default AccuracySec is 1 minute)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time Specification&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Strict&lt;/td&gt;
&lt;td&gt;❌ Not possible&lt;/td&gt;
&lt;td&gt;✅ Strict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compensation for Missed Jobs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes (Persistent=true)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prevention of Overlapping Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ None&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;&lt;strong&gt;Log Checking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;syslog&lt;/td&gt;
&lt;td&gt;syslog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;journalctl -u &amp;lt;unit&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;General User&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Possible&lt;/td&gt;
&lt;td&gt;△ Possible (-S spooldir)&lt;/td&gt;
&lt;td&gt;✅ Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simplicity of Configuration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ 1 file&lt;/td&gt;
&lt;td&gt;✅ 1 file&lt;/td&gt;
&lt;td&gt;❌ 2 files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependency Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Random Delay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;△ delay (not random)&lt;/td&gt;
&lt;td&gt;✅ Yes (RandomizedDelaySec)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Guidelines for Choosing Between Them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Case 1: Strict Time Specification Needed on a Server
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: Use cron&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintenance outside of business hours (e.g., every day at 2 AM).&lt;/li&gt;
&lt;li&gt;Regular report generation (e.g., every Monday at 9 AM).&lt;/li&gt;
&lt;li&gt;High-frequency monitoring (e.g., every 5 minutes).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Simple and low learning cost.&lt;/li&gt;
&lt;li&gt;Allows for strict time specifications.&lt;/li&gt;
&lt;li&gt;Supports detailed schedules at minute intervals.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Case 2: Daily Backup on a Laptop
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: Use anacron or systemd timer&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daily tasks on machines that have powered-off periods.&lt;/li&gt;
&lt;li&gt;Maintenance that does not require strict time specifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose anacron if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to keep the configuration simple.&lt;/li&gt;
&lt;li&gt;You want to leverage existing &lt;code&gt;/etc/cron.daily&lt;/code&gt; setups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose systemd timer if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want centralized logging with &lt;code&gt;journalctl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;There are dependencies between services.&lt;/li&gt;
&lt;li&gt;You want to distribute load with random delays.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Case 3: Complex Scheduling Requirements
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: Use systemd timer&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Execute 24 hours after the last run.&lt;/li&gt;
&lt;li&gt;Execute 15 minutes after system startup.&lt;/li&gt;
&lt;li&gt;Execute after a specific service has started.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Supports various triggers like &lt;code&gt;OnBootSec&lt;/code&gt;, &lt;code&gt;OnUnitActiveSec&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Manages service dependencies with &lt;code&gt;After=&lt;/code&gt;, &lt;code&gt;Requires=&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For details on the format of &lt;code&gt;OnCalendar&lt;/code&gt; and timer precision (&lt;code&gt;AccuracySec&lt;/code&gt;), refer to &lt;a href="https://man7.org/linux/man-pages/man7/systemd.time.7.html" rel="noopener noreferrer"&gt;systemd.time(7)&lt;/a&gt; and &lt;a href="https://man7.org/linux/man-pages/man5/systemd.timer.5.html" rel="noopener noreferrer"&gt;systemd.timer(5)&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 4: Improving Existing cron Jobs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: Migrate to systemd timer&lt;/strong&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Logs can be easily checked with &lt;code&gt;journalctl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Prevents overlapping execution if the previous job has not finished.&lt;/li&gt;
&lt;li&gt;Allows for load distribution with random delays.&lt;/li&gt;
&lt;li&gt;Manual execution is straightforward (&lt;code&gt;systemctl start service.service&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example Migration Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check crontab entries.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# 0 2 * * * /usr/local/bin/backup.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create systemd timer and service.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.config/systemd/user/backup.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Daily backup timer&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;02:00&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.config/systemd/user/backup.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Backup service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/backup.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Enable the timer.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; backup.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Confirm operation.
&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;# Check the list of timers&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-timers

&lt;span class="c"&gt;# Check logs&lt;/span&gt;
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; backup.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical Example: Automating Backups on a Laptop
&lt;/h2&gt;

&lt;p&gt;Here, we will introduce an example of executing daily backups on a laptop that has powered-off periods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using anacron
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a backup script.
&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/backup.sh&lt;/span&gt;

&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/mnt/backup"&lt;/span&gt;
&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;

rsync &lt;span class="nt"&gt;-av&lt;/span&gt; &lt;span class="nt"&gt;--delete&lt;/span&gt; /home/user/Documents/ &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DATE&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Place it in &lt;code&gt;/etc/cron.daily/&lt;/code&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 cp&lt;/span&gt; /usr/local/bin/backup.sh /etc/cron.daily/backup
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /etc/cron.daily/backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;anacron will execute it automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On Ubuntu/Debian, anacron is set up by default to run &lt;code&gt;/etc/cron.daily/&lt;/code&gt; daily. Even if the machine is powered off, missed jobs will be compensated during the next startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using systemd timer
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create the backup script (same as above).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create the service file.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.config/systemd/user/backup.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Daily backup&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/backup.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create the timer file.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.config/systemd/user/backup.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Daily backup timer&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;daily&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30min&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Enable the timer.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; backup.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Confirm operation.
&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;# Check the next execution time&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; list-timers backup.timer

&lt;span class="c"&gt;# Manually execute for testing&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start backup.service

&lt;span class="c"&gt;# Check logs&lt;/span&gt;
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; backup.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advantages of systemd timer:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logs can be easily checked with &lt;code&gt;journalctl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Execution times can be randomized with &lt;code&gt;RandomizedDelaySec&lt;/code&gt; for load distribution.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Persistent=true&lt;/code&gt; compensates for missed jobs.&lt;/li&gt;
&lt;li&gt;Manual execution is straightforward (&lt;code&gt;systemctl start&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Q1: Is anacron still in use?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; Yes, it is installed by default in desktop environments like Ubuntu/Debian and is used to execute &lt;code&gt;/etc/cron.daily&lt;/code&gt; and similar tasks. However, there is a trend toward migrating to systemd timer, and new projects are increasingly opting for systemd timer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q2: Should I migrate from cron to systemd timer?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; It is worth considering migration if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You find checking logs cumbersome.&lt;/li&gt;
&lt;li&gt;The next job starts even if the previous one has not finished.&lt;/li&gt;
&lt;li&gt;You want to manage dependencies between services.&lt;/li&gt;
&lt;li&gt;You frequently perform manual executions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other hand, if your jobs are simple and sufficient with cron, there is no need to force a migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q3: Is the learning curve for systemd timer high?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; Compared to cron, it requires two configuration files, resulting in more verbosity. However, you can easily check the list of timers and logs with the &lt;code&gt;systemctl&lt;/code&gt; command, making operational management easier in many cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q4: Can general users use systemd timer?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; Yes, you can place files in &lt;code&gt;~/.config/systemd/user/&lt;/code&gt; and manage them with the &lt;code&gt;systemctl --user&lt;/code&gt; command. However, to ensure that the timer continues to run after logging out, you need to execute &lt;code&gt;loginctl enable-linger username&lt;/code&gt; (&lt;a href="https://man7.org/linux/man-pages/man1/loginctl.1.html" rel="noopener noreferrer"&gt;loginctl(1)&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It is important to choose the appropriate Linux task scheduler based on your needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guidelines for Selection:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict time specification on servers&lt;/strong&gt;: cron&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily tasks on laptops&lt;/strong&gt;: anacron or systemd timer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex schedules&lt;/strong&gt;: systemd timer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improving existing cron jobs&lt;/strong&gt;: migrate to systemd timer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Personally, I believe that on systems using systemd, new jobs should be created with systemd timer, and existing cron jobs should gradually be migrated. The centralized management of logs and prevention of overlapping execution can significantly reduce operational overhead.&lt;/p&gt;

&lt;p&gt;If you're interested, start by trying out systemd timer with a simple job. &lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/cron.8.html" rel="noopener noreferrer"&gt;cron(8) - Linux manual page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/anacron.8.html" rel="noopener noreferrer"&gt;anacron(8) - Linux manual page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html" rel="noopener noreferrer"&gt;systemd.timer(5) - systemd manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.timer.5.html" rel="noopener noreferrer"&gt;systemd.timer(5) - Linux manual page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/systemd.time.7.html" rel="noopener noreferrer"&gt;systemd.time(7) - Linux manual page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man1/loginctl.1.html" rel="noopener noreferrer"&gt;loginctl(1) - Linux manual page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://anteru.net/blog/2024/replacing-cron-with-systemd-timers/" rel="noopener noreferrer"&gt;Replacing cron with systemd-timers - Anteru's Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tecmint.com/cron-vs-anacron-schedule-jobs-using-anacron-on-linux/" rel="noopener noreferrer"&gt;Cron Vs Anacron: How to Schedule Jobs Using Anacron on Linux - TecMint&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>devops</category>
    </item>
    <item>
      <title>Git Hooks Completed with a Single Binary: Migration Notes from pre-commit to prek</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Thu, 05 Feb 2026 12:36:34 +0000</pubDate>
      <link>https://forem.com/tumf/git-hooks-completed-with-a-single-binary-migration-notes-from-pre-commit-to-prek-59b1</link>
      <guid>https://forem.com/tumf/git-hooks-completed-with-a-single-binary-migration-notes-from-pre-commit-to-prek-59b1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-02-05&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/2/5/prek-portable-git-hooks-manager/" rel="noopener noreferrer"&gt;シングルバイナリで完結するGit hooks: pre-commitからprekへの移行メモ&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Have you ever thought, "Why do I need Python when I'm not working on a Python project?" while using &lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;pre-commit&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prek.j178.dev/" rel="noopener noreferrer"&gt;prek&lt;/a&gt; is a Git hooks management tool that reimplements pre-commit in Rust. It allows you to use your existing &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; without needing a Python runtime, and it operates as a single binary. In this article, I will discuss the features of prek, the migration process, and the importance of "portability" over "speed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Fundamental Issues with pre-commit
&lt;/h2&gt;

&lt;p&gt;While pre-commit is an excellent tool, it has several challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python Required&lt;/strong&gt; - Even for Go/Rust/TypeScript projects, a Python environment is necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtualenv Management&lt;/strong&gt; - There is a potential conflict with the project's Python version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity in CI/CD&lt;/strong&gt; - Steps like &lt;code&gt;setup-python&lt;/code&gt; + &lt;code&gt;pip install pre-commit&lt;/code&gt; are required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding Barriers&lt;/strong&gt; - New members must start by setting up a Python environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This situation often arises in non-Python projects, where you end up needing Python just for Git hooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features of prek
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/j178/prek" rel="noopener noreferrer"&gt;prek&lt;/a&gt; (MIT License) addresses these challenges:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Single Binary, No Dependencies
&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;# macOS/Linux&lt;/span&gt;
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://github.com/j178/prek/releases/latest/download/prek-installer.sh | sh

&lt;span class="c"&gt;# Windows&lt;/span&gt;
powershell &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"irm https://github.com/j178/prek/releases/latest/download/prek-installer.ps1 | iex"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Python runtime is required. It operates with just one binary.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Fully Compatible with pre-commit
&lt;/h3&gt;

&lt;p&gt;You can use your existing &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; as is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pre-commit/pre-commit-hooks&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v6.0.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trailing-whitespace&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;end-of-file-fixer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migration is as simple as &lt;code&gt;pre-commit uninstall &amp;amp;&amp;amp; prek install&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Speed Improvements
&lt;/h3&gt;

&lt;p&gt;Official benchmarks (Apache Airflow):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;pre-commit&lt;/th&gt;
&lt;th&gt;prek&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial Installation&lt;/td&gt;
&lt;td&gt;187 seconds&lt;/td&gt;
&lt;td&gt;18 seconds&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.2x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hook Execution&lt;/td&gt;
&lt;td&gt;352 ms&lt;/td&gt;
&lt;td&gt;77 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.6x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk Usage&lt;/td&gt;
&lt;td&gt;1.6 GB&lt;/td&gt;
&lt;td&gt;810 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Half&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;However, this is a "bonus." The essential value will be explained in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Value of Portability
&lt;/h2&gt;

&lt;p&gt;The true value of Rust tools lies not in "speed" but in "portability."&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplified CI/CD
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For pre-commit:&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="c1"&gt;# .github/workflows/lint.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install pre-commit&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pre-commit run --all-files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For prek:&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="c1"&gt;# .github/workflows/lint.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;j178/prek-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The setup steps become unnecessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reduced Dependencies in Development Environments
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Dependencies for pre-commit:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project
├── Python 3.11 (for the app)
└── Python 3.9 (for pre-commit, virtualenv)
    └── pre-commit
        └── Dependencies for each hook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dependencies for prek:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project
└── prek (single binary)
    └── Dependencies for each hook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The risk of Python version conflicts is eliminated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-Platform Compatibility
&lt;/h3&gt;

&lt;p&gt;The same binary works on Windows/macOS/Linux:&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;# Same command on any OS&lt;/span&gt;
prek &lt;span class="nb"&gt;install
&lt;/span&gt;prek run &lt;span class="nt"&gt;--all-files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This absorbs differences in development environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install prek
&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;# Homebrew&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;prek

&lt;span class="c"&gt;# uv&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install &lt;/span&gt;prek

&lt;span class="c"&gt;# cargo&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;prek
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Uninstall Existing pre-commit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pre-commit uninstall
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Install prek
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;prek &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use your &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; as is.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Verify Functionality
&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;# Display list of hooks&lt;/span&gt;
prek list

&lt;span class="c"&gt;# Run on all files&lt;/span&gt;
prek run &lt;span class="nt"&gt;--all-files&lt;/span&gt;

&lt;span class="c"&gt;# Run specific hooks only&lt;/span&gt;
prek run trailing-whitespace end-of-file-fixer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Update GitHub Actions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pre-commit/action@v3.0.1&lt;/span&gt;

&lt;span class="c1"&gt;# After&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;j178/prek-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Additional Features of prek
&lt;/h2&gt;

&lt;p&gt;Convenient features not available in pre-commit:&lt;/p&gt;

&lt;h3&gt;
  
  
  List Hooks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;prek list
.:trailing-whitespace
.:end-of-file-fixer
.:check-yaml
.:check-toml
.:ruff
.:ruff-format
.:mypy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run with Directory Specification
&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;# Only in the src directory&lt;/span&gt;
prek run &lt;span class="nt"&gt;--directory&lt;/span&gt; src

&lt;span class="c"&gt;# Multiple directories&lt;/span&gt;
prek run &lt;span class="nt"&gt;--directory&lt;/span&gt; src &lt;span class="nt"&gt;--directory&lt;/span&gt; tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run Only on Last Commit Targets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;prek run &lt;span class="nt"&gt;--last-commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Monorepo Support
&lt;/h3&gt;

&lt;p&gt;You can place a &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; for each subproject:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monorepo/
├── .pre-commit-config.yaml  # Root
├── frontend/
│   └── .pre-commit-config.yaml
└── backend/
    └── .pre-commit-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run for all projects&lt;/span&gt;
prek run

&lt;span class="c"&gt;# Run for specific projects only&lt;/span&gt;
prek run frontend backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Points to Note
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Tool in Development
&lt;/h3&gt;

&lt;p&gt;Currently at v0.3.1. Some languages and subcommands are not yet implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://prek.j178.dev/languages/" rel="noopener noreferrer"&gt;Language Support Status&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prek.j178.dev/todo/" rel="noopener noreferrer"&gt;TODO&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is recommended to verify the functionality of the hooks you plan to use before production deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Unique Features of pre-commit
&lt;/h3&gt;

&lt;p&gt;Some unique features of pre-commit may not be supported. Please check the contents of your &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; before migrating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison with Other Tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;pre-commit Compatible&lt;/th&gt;
&lt;th&gt;Dependencies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://prek.j178.dev/" rel="noopener noreferrer"&gt;prek&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;✅ Fully&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/evilmartians/lefthook" rel="noopener noreferrer"&gt;lefthook&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;❌ Custom Settings&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/jdx/hk" rel="noopener noreferrer"&gt;hk&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;❌ Custom Settings&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://typicode.github.io/husky/" rel="noopener noreferrer"&gt;Husky&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;❌ Custom Settings&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;pre-commit&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Importance of Configuration Compatibility:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While lefthook and hk are also fast, they require rewriting the existing &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt;. Since prek can use the configuration file as is, the migration cost is zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adoption Records
&lt;/h2&gt;

&lt;p&gt;Notable OSS projects that have adopted it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/python/cpython" rel="noopener noreferrer"&gt;CPython&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/airflow" rel="noopener noreferrer"&gt;Apache Airflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fastapi/fastapi" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/astral-sh/ruff" rel="noopener noreferrer"&gt;Ruff&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/home-assistant/core" rel="noopener noreferrer"&gt;Home Assistant&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The intrinsic value of prek lies not in "speed" but in "portability":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduction of Dependencies&lt;/strong&gt; - No Python runtime required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplification of CI/CD&lt;/strong&gt; - Fewer setup steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unification of Development Environments&lt;/strong&gt; - Cross-platform support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Migration Cost&lt;/strong&gt; - Existing configurations can be used as is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is particularly recommended for non-Python projects or for teams looking to simplify their development environments.&lt;/p&gt;

&lt;p&gt;If you're interested, please give it a try.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://prek.j178.dev/" rel="noopener noreferrer"&gt;Official prek Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/j178/prek" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prek.j178.dev/benchmark/" rel="noopener noreferrer"&gt;Benchmark Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prek.j178.dev/languages/" rel="noopener noreferrer"&gt;Language Support Status&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;Official pre-commit Site&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>devops</category>
    </item>
    <item>
      <title>restic: Designing a "Restorable Development Environment" While Excluding node_modules and .git</title>
      <dc:creator>tumf</dc:creator>
      <pubDate>Tue, 03 Feb 2026 03:05:58 +0000</pubDate>
      <link>https://forem.com/tumf/restic-designing-a-restorable-development-environment-while-excluding-nodemodules-and-git-4ig</link>
      <guid>https://forem.com/tumf/restic-designing-a-restorable-development-environment-while-excluding-nodemodules-and-git-4ig</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on 2026-01-09&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Original article (Japanese): &lt;a href="https://blog.tumf.dev/posts/diary/2026/1/9/restic-developer-backup-strategy/" rel="noopener noreferrer"&gt;restic: node_modules と .git を除外しつつ「復元可能な開発環境」を維持する設計&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Are you properly backing up your development machine?&lt;/p&gt;

&lt;p&gt;I used to think, "I have Time Machine, so I'm fine," or "I can restore &lt;code&gt;node_modules&lt;/code&gt; with &lt;code&gt;git clone&lt;/code&gt;..." But backups that cannot be restored when truly needed are meaningless.&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://blog.tumf.dev/posts/diary/2025/12/28/engineer-year-end-cleanup-part2/" rel="noopener noreferrer"&gt;year-end cleanup article (Part 2)&lt;/a&gt;, I introduced restic + resticprofile, but I received questions like, "How exactly should I configure it?" In this article, I will delve into the practical design of development environment backups using restic, explaining the fundamentals. Let's build a balanced backup strategy that avoids wasting space while ensuring that necessary items can be reliably restored.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is restic?
&lt;/h2&gt;

&lt;p&gt;restic is a modern backup tool developed in the &lt;a href="https://go.dev/" rel="noopener noreferrer"&gt;Go&lt;/a&gt; programming language. Since its introduction in 2015, it has gained support for its simplicity and power.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encryption&lt;/strong&gt;: All data is encrypted with AES-256&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication&lt;/strong&gt;: Efficient differential management at the chunk level rather than the file level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental Backups&lt;/strong&gt;: Only changes are transferred after the initial backup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression Support&lt;/strong&gt;: Supports &lt;a href="https://facebook.github.io/zstd/" rel="noopener noreferrer"&gt;zstd&lt;/a&gt; compression since version 0.14 (2022)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diverse Backends&lt;/strong&gt;: Supports over 20 types of storage, including local disks, S3, SFTP, and B2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Platform&lt;/strong&gt;: Consistent user experience on &lt;a href="https://blog.tumf.dev/tags/macos/" rel="noopener noreferrer"&gt;macOS&lt;/a&gt;, &lt;a href="https://blog.tumf.dev/tags/linux/" rel="noopener noreferrer"&gt;Linux&lt;/a&gt;, and Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Differences from Time Machine and borg
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Compression&lt;/th&gt;
&lt;th&gt;Multithreaded&lt;/th&gt;
&lt;th&gt;Cloud Support&lt;/th&gt;
&lt;th&gt;Main Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time Machine&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;Local backups for macOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;borg&lt;/td&gt;
&lt;td&gt;○&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;For Linux servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;restic&lt;/td&gt;
&lt;td&gt;○&lt;/td&gt;
&lt;td&gt;○&lt;/td&gt;
&lt;td&gt;◎&lt;/td&gt;
&lt;td&gt;Cross-platform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While Time Machine is convenient because it is integrated into macOS, it requires an external disk and lacks flexibility. &lt;a href="https://www.borgbackup.org/" rel="noopener noreferrer"&gt;borg&lt;/a&gt; has excellent compression rates but operates in single-threaded mode, making it slower than restic on fast local disks.&lt;/p&gt;

&lt;p&gt;restic combines "multithreading + compression + cloud support," making it particularly suitable for developers in diverse environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  macOS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;restic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Linux (Ubuntu/Debian)
&lt;/h3&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;apt &lt;span class="nb"&gt;install &lt;/span&gt;restic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check Version
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic version
&lt;span class="c"&gt;# restic 0.18.1 compiled with go1.23.x on linux/amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Basic Concept: The 3-2-1 Backup Rule
&lt;/h2&gt;

&lt;p&gt;A fundamental principle of backup design is the "3-2-1 rule":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; copies (production data + 2 backups)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2&lt;/strong&gt; different media types (local + cloud, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1&lt;/strong&gt; offsite (physically separate location)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With restic, this rule can be relatively easily implemented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Initialize the Repository
&lt;/h2&gt;

&lt;p&gt;In restic, the location where backup data is stored is called a "repository." Let's create a repository on a local disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Local Repository
&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;# Create the backup destination directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /Volumes/Backup/restic-repo

&lt;span class="c"&gt;# Initialize the repository&lt;/span&gt;
restic init &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will be prompted for a password. This will be used to generate the encryption key, so &lt;strong&gt;make sure to store it securely&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enter password for new repository: 
enter password again: 
created restic repository 6a3e8a9b at /Volumes/Backup/restic-repo

Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is
irrecoverably lost.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Password Management Options
&lt;/h3&gt;

&lt;p&gt;Entering the password every time can be cumbersome. You can automate this using the following methods.&lt;/p&gt;

&lt;h4&gt;
  
  
  Method 1: Password File (Recommended)
&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 a password file (set strict permissions)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"your-secure-password"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.restic-password
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.restic-password

&lt;span class="c"&gt;# Backup using the password file&lt;/span&gt;
restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       backup ~/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Method 2: Environment Variables
&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;# Add to .zshrc or .bashrc&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/Volumes/Backup/restic-repo"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-secure-password"&lt;/span&gt;

&lt;span class="c"&gt;# You can now run with shorter commands&lt;/span&gt;
restic backup ~/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security Note&lt;/strong&gt;: Environment variables can be visible with commands like &lt;code&gt;ps&lt;/code&gt;, so the password file method is safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Initial Backup
&lt;/h2&gt;

&lt;p&gt;Let's back up the &lt;code&gt;~/work&lt;/code&gt; directory in its simplest form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       backup ~/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Execution result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repository 6a3e8a9b opened (version 2, compression level auto)
created new cache in /Users/tumf/.cache/restic

Files:       12543 new,     0 changed,     0 unmodified
Dirs:         1823 new,     0 changed,     0 unmodified
Added to the repository: 4.2 GiB (3.1 GiB stored)

processed 12543 files, 5.8 GiB in 2:15
snapshot 9a3d7f2e saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compression Effect&lt;/strong&gt;: 5.8 GiB of data compressed to 3.1 GiB (approximately 47% reduction)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing Speed&lt;/strong&gt;: Processed 12,543 files in 2 minutes and 15 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot ID&lt;/strong&gt;: &lt;code&gt;9a3d7f2e&lt;/code&gt; is the ID that identifies this backup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Directories to Exclude in Development Environments
&lt;/h2&gt;

&lt;p&gt;Now we get to the main topic. Development environments often contain a large number of temporary files and build artifacts, which can take up significant space if backed up as is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typical Directories to Exclude
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Node.js/JavaScript Projects
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules/       # Dependency packages (can be restored from package.json)
.next/              # Next.js build artifacts
dist/               # Build output
build/              # Build output
coverage/           # Test coverage
.turbo/             # Turbopack cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Python Projects
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;__pycache__/        # Python bytecode
.venv/              # Virtual environment (can be restored from requirements.txt)
venv/
.pytest_cache/      # pytest cache
.mypy_cache/        # mypy cache
.ruff_cache/        # ruff cache
*.egg-info/         # Package metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Rust Projects
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target/             # Build artifacts (can be restored from Cargo.toml)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Go Projects
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vendor/             # Dependency packages (can be restored from go.mod)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Git Repositories
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.git/objects/       # Git objects (can be restored from remote)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, you &lt;strong&gt;must not exclude&lt;/strong&gt; &lt;code&gt;.git/config&lt;/code&gt; or &lt;code&gt;.git/hooks&lt;/code&gt; as they contain local settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Criteria for Determining Restorability
&lt;/h3&gt;

&lt;p&gt;Criteria for determining whether something can be excluded:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Can it be fully restored from definition files?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;package.json&lt;/code&gt; → Restorable with &lt;code&gt;npm install&lt;/code&gt; ✅&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; → Restorable with &lt;code&gt;pip install -r&lt;/code&gt; ✅&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Cargo.toml&lt;/code&gt; → Restorable with &lt;code&gt;cargo build&lt;/code&gt; ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Can it be obtained from remote?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.git/objects/&lt;/code&gt; → Restorable with &lt;code&gt;git clone&lt;/code&gt; or &lt;code&gt;git fetch&lt;/code&gt; ✅&lt;/li&gt;
&lt;li&gt;Local uncommitted changes → Not restorable ❌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Does regeneration take too long?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build time is a few minutes → Can be excluded ✅&lt;/li&gt;
&lt;li&gt;Build time is several hours → Depends on the situation △&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 4: Designing Exclude Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create Exclude File
&lt;/h3&gt;

&lt;p&gt;Write the patterns in &lt;code&gt;~/.restic-excludes&lt;/code&gt;:&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;# ~/.restic-excludes&lt;/span&gt;

&lt;span class="c"&gt;# Node.js&lt;/span&gt;
&lt;span class="k"&gt;**&lt;/span&gt;/node_modules
&lt;span class="k"&gt;**&lt;/span&gt;/.next
&lt;span class="k"&gt;**&lt;/span&gt;/dist
&lt;span class="k"&gt;**&lt;/span&gt;/build
&lt;span class="k"&gt;**&lt;/span&gt;/coverage
&lt;span class="k"&gt;**&lt;/span&gt;/.turbo

&lt;span class="c"&gt;# Python&lt;/span&gt;
&lt;span class="k"&gt;**&lt;/span&gt;/__pycache__
&lt;span class="k"&gt;**&lt;/span&gt;/.venv
&lt;span class="k"&gt;**&lt;/span&gt;/venv
&lt;span class="k"&gt;**&lt;/span&gt;/.pytest_cache
&lt;span class="k"&gt;**&lt;/span&gt;/.mypy_cache
&lt;span class="k"&gt;**&lt;/span&gt;/.ruff_cache
&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.egg-info

&lt;span class="c"&gt;# Rust&lt;/span&gt;
&lt;span class="k"&gt;**&lt;/span&gt;/target

&lt;span class="c"&gt;# Go&lt;/span&gt;
&lt;span class="k"&gt;**&lt;/span&gt;/vendor

&lt;span class="c"&gt;# Git objects (keep config)&lt;/span&gt;
&lt;span class="k"&gt;**&lt;/span&gt;/.git/objects
&lt;span class="k"&gt;**&lt;/span&gt;/.git/logs
&lt;span class="k"&gt;**&lt;/span&gt;/.git/refs/remotes

&lt;span class="c"&gt;# Editors/IDEs&lt;/span&gt;
&lt;span class="k"&gt;**&lt;/span&gt;/.vscode/.history
&lt;span class="k"&gt;**&lt;/span&gt;/.idea/workspace.xml
&lt;span class="k"&gt;**&lt;/span&gt;/.idea/tasks.xml

&lt;span class="c"&gt;# macOS&lt;/span&gt;
&lt;span class="k"&gt;**&lt;/span&gt;/.DS_Store
&lt;span class="k"&gt;**&lt;/span&gt;/._&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Temporary files&lt;/span&gt;
&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.tmp
&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.log
&lt;span class="k"&gt;**&lt;/span&gt;/.cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to Write Patterns
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;**/&lt;/code&gt; matches any depth&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*.log&lt;/code&gt; matches by extension&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;!pattern&lt;/code&gt; specifies exceptions to the exclusion (to include specific files)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Backup with Exclude File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--exclude-file&lt;/span&gt; ~/.restic-excludes &lt;span class="se"&gt;\&lt;/span&gt;
       backup ~/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Execution result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Files:        8234 new,     0 changed,     0 unmodified
Dirs:          983 new,     0 changed,     0 unmodified
Added to the repository: 1.2 GiB (892 MiB stored)

processed 8234 files, 1.8 GiB in 0:45
snapshot b7e2c9a1 saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The size reduced from 5.8 GiB to 1.8 GiB after exclusions (approximately 69% reduction).&lt;/p&gt;

&lt;h3&gt;
  
  
  Utilizing exclude-caches
&lt;/h3&gt;

&lt;p&gt;Many build tools place a &lt;code&gt;CACHEDIR.TAG&lt;/code&gt; file in their cache directories. You can automatically detect and exclude these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--exclude-file&lt;/span&gt; ~/.restic-excludes &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--exclude-caches&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       backup ~/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--exclude-caches&lt;/code&gt; option excludes directories that contain files adhering to the &lt;a href="https://bford.info/cachedir/" rel="noopener noreferrer"&gt;Cache Directory Tagging Standard&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache directories for Docker and Homebrew will also be automatically excluded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Verify Backups and Restore
&lt;/h2&gt;

&lt;h3&gt;
  
  
  List Snapshots
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       snapshots
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repository 6a3e8a9b opened (version 2, compression level auto)
ID        Time                 Host        Tags        Paths
-------------------------------------------------------------------------
9a3d7f2e  2025-12-28 15:30:00  macbook                 /Users/tumf/work
b7e2c9a1  2025-12-28 16:45:00  macbook                 /Users/tumf/work
-------------------------------------------------------------------------
2 snapshots
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Search for Specific Files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       find &lt;span class="s2"&gt;"package.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Restore Files (Partial Restore)
&lt;/h3&gt;

&lt;p&gt;Restore only specific files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       restore b7e2c9a1 &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--target&lt;/span&gt; ~/restore &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--include&lt;/span&gt; &lt;span class="s2"&gt;"project-name/package.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Full Restore
&lt;/h3&gt;

&lt;p&gt;Restore the entire snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       restore latest &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--target&lt;/span&gt; ~/restore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;latest&lt;/code&gt; refers to the most recent snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Manage Generations (Retention Policy)
&lt;/h2&gt;

&lt;p&gt;As you continue to back up, the storage will increase. Set up "generation management" to automatically delete old snapshots.&lt;/p&gt;

&lt;h3&gt;
  
  
  forget Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       forget &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--keep-last&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--keep-daily&lt;/span&gt; 7 &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--keep-weekly&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--keep-monthly&lt;/span&gt; 6 &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--prune&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meaning of the options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--keep-last 5&lt;/code&gt;: Keep the latest 5 generations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--keep-daily 7&lt;/code&gt;: Keep daily backups for the past 7 days&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--keep-weekly 4&lt;/code&gt;: Keep weekly backups for the past 4 weeks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--keep-monthly 6&lt;/code&gt;: Keep monthly backups for the past 6 months&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--prune&lt;/code&gt;: Physically delete unnecessary data blocks after deletion&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Best Practices for Generation Management
&lt;/h3&gt;

&lt;p&gt;Recommended settings for development environments:&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;# If backing up frequently&lt;/span&gt;
&lt;span class="nt"&gt;--keep-last&lt;/span&gt; 3 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--keep-daily&lt;/span&gt; 7 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--keep-weekly&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--keep-monthly&lt;/span&gt; 3

&lt;span class="c"&gt;# If storage capacity allows&lt;/span&gt;
&lt;span class="nt"&gt;--keep-last&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--keep-daily&lt;/span&gt; 14 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--keep-weekly&lt;/span&gt; 8 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--keep-monthly&lt;/span&gt; 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Automation (Scheduling)
&lt;/h2&gt;

&lt;p&gt;It's easy to forget to back up manually. Let's automate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  macOS (Using launchd)
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;~/Library/LaunchAgents/dev.tumf.restic-backup.plist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;dev.tumf.restic-backup&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/opt/homebrew/bin/restic&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;--repo&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Volumes/Backup/restic-repo&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;--password-file&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/tumf/.restic-password&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;--exclude-file&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/tumf/.restic-excludes&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;--exclude-caches&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;backup&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/tumf/work&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StandardOutPath&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/tumf/.restic-backup.log&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StandardErrorPath&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/tumf/.restic-backup-error.log&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;launchctl load ~/Library/LaunchAgents/dev.tumf.restic-backup.plist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will execute automatic backups daily at 3 AM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Linux (Using systemd)
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;~/.config/systemd/user/restic-backup.service&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Restic backup service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/restic --repo /backup/restic-repo &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="s"&gt;--password-file /home/tumf/.restic-password &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="s"&gt;--exclude-file /home/tumf/.restic-excludes &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="s"&gt;--exclude-caches &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="s"&gt;backup /home/tumf/work&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;~/.config/systemd/user/restic-backup.timer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Restic backup timer&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*-*-* 03:00:00&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;restic-backup.timer
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start restic-backup.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Key Management with Multiple Passwords
&lt;/h2&gt;

&lt;p&gt;Considering team development and emergency recovery, you can register multiple passwords.&lt;/p&gt;

&lt;h3&gt;
  
  
  Register Additional Passwords
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       key add
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you enter a new password, another key will be added.&lt;/p&gt;

&lt;h3&gt;
  
  
  List Passwords
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       key list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ID          User        Host        Created
--------------------------------------------------------------
*eb2e1c89    tumf        macbook     2025-12-28 15:30:00
 a3d7f9b2    tumf        macbook     2025-12-28 16:50:00
--------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;*&lt;/code&gt; indicates the currently used key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Cases
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Team Sharing&lt;/strong&gt;: Issue different passwords for each member&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emergency Key&lt;/strong&gt;: Store a backup password in a secure location that is not used regularly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotation&lt;/strong&gt;: Change passwords regularly and remove old keys&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Remove Old Keys
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       key remove a3d7f9b2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 9: Utilizing Cloud Backends
&lt;/h2&gt;

&lt;p&gt;Local disks alone do not satisfy the "offsite" requirement of the 3-2-1 rule. Let's add cloud storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backblaze B2 (Recommended for Low Cost)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.backblaze.com/b2/cloud-storage.html" rel="noopener noreferrer"&gt;Backblaze B2&lt;/a&gt; is S3 compatible, inexpensive, and works well with restic.&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;# Set environment variables&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;B2_ACCOUNT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-account-id"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;B2_ACCOUNT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-account-key"&lt;/span&gt;

&lt;span class="c"&gt;# Initialize repository&lt;/span&gt;
restic init &lt;span class="nt"&gt;--repo&lt;/span&gt; b2:bucket-name:restic-repo

&lt;span class="c"&gt;# Backup&lt;/span&gt;
restic &lt;span class="nt"&gt;--repo&lt;/span&gt; b2:bucket-name:restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--exclude-file&lt;/span&gt; ~/.restic-excludes &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--exclude-caches&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       backup ~/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt;AWS S3&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-access-key"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-secret-key"&lt;/span&gt;

restic init &lt;span class="nt"&gt;--repo&lt;/span&gt; s3:s3.amazonaws.com/bucket-name

restic &lt;span class="nt"&gt;--repo&lt;/span&gt; s3:s3.amazonaws.com/bucket-name &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       backup ~/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SFTP (e.g., Home NAS)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic init &lt;span class="nt"&gt;--repo&lt;/span&gt; sftp:user@nas.local:/backup/restic-repo

restic &lt;span class="nt"&gt;--repo&lt;/span&gt; sftp:user@nas.local:/backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       backup ~/work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 10: Verify Backups
&lt;/h2&gt;

&lt;p&gt;Regularly verify that backups are not corrupted.&lt;/p&gt;

&lt;h3&gt;
  
  
  check Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using temporary cache in /var/folders/...
create exclusive lock for repository
load indexes
check all packs
check snapshots, trees and blobs
no errors were found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  read-data Option (Full Verification)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;--repo&lt;/span&gt; /Volumes/Backup/restic-repo &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--password-file&lt;/span&gt; ~/.restic-password &lt;span class="se"&gt;\&lt;/span&gt;
       check &lt;span class="nt"&gt;--read-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reads and verifies all data blocks (it takes time). It is recommended to run this about once a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Example: My Development Environment Backup Configuration
&lt;/h2&gt;

&lt;p&gt;Finally, I will share the configuration I actually use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Directory Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/
├── work/                    # All projects
├── .restic-password         # Password file
├── .restic-excludes         # Exclude patterns
└── .zshrc                   # Environment variable settings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  .zshrc Configuration
&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;# restic aliases&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/Volumes/Backup/restic-repo"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_PASSWORD_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.restic-password"&lt;/span&gt;

&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'restic backup ~/work --exclude-file ~/.restic-excludes --exclude-caches'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'restic snapshots'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;rf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'restic forget --keep-last 5 --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;rc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'restic check'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Daily Operations
&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;# Execute backup (using alias)&lt;/span&gt;
rb

&lt;span class="c"&gt;# Check snapshots&lt;/span&gt;
rs

&lt;span class="c"&gt;# Delete old snapshots&lt;/span&gt;
rf

&lt;span class="c"&gt;# Monthly verification&lt;/span&gt;
rc &lt;span class="nt"&gt;--read-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's simple, but it allows me to focus on development with peace of mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I have explained the design of development environment backups using restic.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exclude Patterns&lt;/strong&gt;: Exclude items like &lt;code&gt;node_modules&lt;/code&gt; and &lt;code&gt;.venv&lt;/code&gt; that can be restored&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation Management&lt;/strong&gt;: Automatically delete old snapshots with &lt;code&gt;forget&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Passwords&lt;/strong&gt;: Register multiple keys for team sharing and emergencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation&lt;/strong&gt;: Schedule with launchd/systemd&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offsite&lt;/strong&gt;: Achieve the 3-2-1 rule with cloud backends&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regular Verification&lt;/strong&gt;: Check data integrity with the &lt;code&gt;check&lt;/code&gt; command&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"Backups are meaningless if they cannot be restored" — keep this in mind and don't forget to conduct regular restore tests.&lt;/p&gt;

&lt;p&gt;Next time, I plan to discuss managing multiple profiles using resticprofile and monitoring integrations using HTTP Hooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://restic.net/" rel="noopener noreferrer"&gt;restic Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://restic.readthedocs.io/" rel="noopener noreferrer"&gt;restic Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/restic/restic" rel="noopener noreferrer"&gt;GitHub: restic/restic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.backblaze.com/b2/cloud-storage.html" rel="noopener noreferrer"&gt;Backblaze B2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt;AWS S3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.borgbackup.org/" rel="noopener noreferrer"&gt;borg backup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://facebook.github.io/zstd/" rel="noopener noreferrer"&gt;zstd Compression Algorithm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bford.info/cachedir/" rel="noopener noreferrer"&gt;Cache Directory Tagging Standard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bebehei.de/simple-comparison-borg-restic-rustic-2024/" rel="noopener noreferrer"&gt;Simple comparison of borg, restic and rustic (2024)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
