
I tried out the CLI "ax" that combines web page fetching and extraction into a single command for AI agents
This page has been translated by machine translation. View original
I'm Oguri, a big lover of whiskey, cigars, and pipes.
Yusuke Wada (yusukebe), the author of the Web framework Hono, has released a CLI tool called ax, positioning it as "The AI-era curl." Since it's described as a tool for streamlining web page retrieval by coding agents, I decided to try it out. (Personally, I still think of yusukebe as a Perl Monger.)
What is ax?
ax is an HTTP/HTML input/output tool for AI agents, claiming to be "fetch, discover, extract. One command." It is MIT-licensed OSS, and the latest version as of July 16, 2026 is v0.1.15.
When asking a coding agent to extract data from a web page, a common pattern is to write a throwaway Python script that fetches HTML with curl and parses it with regular expressions. The official site lists the following as a "Before" example:
# Example of a throwaway script that agents tend to write
python3 - <<'PY'
import re
h=open('page.html',errors='replace').read()
blocks=re.findall(
r'<li class="lesson">([\s\S]*?)</li>',h)
rows=[]
for b in blocks:
href=re.search(r'href="([^"]+)"',b)
title=re.search(r'<a [^>]*>([^<]+)</a>',b)
...
PY
This approach breaks with even minor changes to the HTML structure, and it wastes tokens by pulling raw HTML into the context. With ax, the equivalent operation becomes the following command:
ax https://site.example '.lesson' \
--row 'title=a, href=a@href, level=.cefr'
title href level
Small talk /lesson/1.htm A2
Directions /lesson/2.htm A2
...
ax: note: 50 rows extracted, no empty fields
Three Features
ax's functionality is organized into three parts: fetch, discover, and extract.
| Feature | Description | Main Options |
|---|---|---|
| fetch | Retrieves with a structured report including status, headers, and response time | curl-compatible formats like -X -H -d -u |
| discover | Understands page structure without outputting raw HTML | --outline --locate 'text' --count |
| extract | Extracts structured data with CSS selectors | --row --table --md --where |
Since it is designed for use with agents, a notable feature is that the output is designed to conserve tokens.
--budget <tokens>controls the overall output within a token budget- Row data output defaults to TSV (header output only once, approximately 40% of the token count compared to JSON)
- Output truncation is noted on stderr, and nothing is silently omitted
- URLs in parse mode are cached for about 2 minutes, preventing re-fetching during trial-and-error with selectors
Comparison with Conventional Methods
The documentation includes a comparison with conventional local methods such as throwaway Python scripts and htmlq.
| Feature | Throwaway Python | curl + htmlq | ax |
|---|---|---|---|
| Full report (status/headers/ms) on fetch | △ Need to write code | ✗ | ✓ |
| Structure discovery (--outline/--locate) | ✗ | ✗ | ✓ |
| CSS selector extraction | △ Requires bs4 | ✓ | ✓ |
| Multi-field rows in one call (--row) | △ Need to write code | ✗ | ✓ |
<table> → keyed rows |
△ Need to write code | ✗ | ✓ |
| Easy selector repair after markup changes | ✗ (regex) | ✓ | ✓ |
| Page → readable Markdown (--md) | ✗ | ✗ | ✓ |
| Token-shaped output (limits, --budget, notes) | ✗ | ✗ | ✓ |
| No code generation needed per task | ✗ | ✓ | ✓ |
Benchmarks
Benchmark results using actual Claude Code sessions have also been published, showing a 23–67% cost reduction compared to curl + throwaway scripts.
| Task | Cost Reduction |
|---|---|
| Extraction of 2 pages with markup changes | −67% ($0.458 → $0.150) |
| Catalog extraction of 60 items | −65% ($0.296 → $0.104, 24s → 14s) |
| Processing a real site with decoy elements | −23% ($0.248 → $0.191) |
| First-time ax use (including documentation loading) | −58% ($0.664 → $0.282) |
Details on the measurement methodology are available in bench/RESULTS.md.
Implementation
The implementation is in TypeScript, distributed as a single binary via Bun. The only runtime dependency is the DOM parser linkedom. It runs locally and deterministically, which I think makes it a reassuring design for a tool to embed in agents.
Mechanisms for Agents
ax has mechanisms to teach agents how to use it.
| Mechanism | Details |
|---|---|
ax agent-context |
Displays the complete manual for agents to standard output offline |
npx skills add yusukebe/ax |
Introduces it into a project as an agent skill (SKILL.md) |
| ax.yusuke.run/llms.txt | Provides a reference for all options in LLM-friendly text |
The intended usage is to paste the output of ax agent-context into a prompt for one-off sessions, and to introduce it as a skill for ongoing use.
Installation
Multiple installation methods are available.
# Installation script
$ curl -fsSL https://ax.yusuke.run/install | sh
# Homebrew
$ brew install yusukebe/tap/ax
# Nix
$ nix run github:yusukebe/ax
This time, I installed it via Homebrew to set it up on my local macOS machine.
$ brew install yusukebe/tap/ax
$ ax --version
0.1.16
Trying It Out
I'll be testing with ax v0.1.16 on macOS (Apple Silicon). Taking advantage of the opportunity, I'll target my own past articles on DevelopersIO (dev.classmethod.jp) and our company's corporate site (classmethod.jp).
Fetch: Retrieval with Structured Report
First, Fetch mode without a selector. Fetching the article I previously wrote about running Tsumiki with Gemini CLI, the status, final URL, response time, key headers, and body are all returned together in a single JSON report.
$ ax https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/
{
"status": 200,
"ok": true,
"url": "https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/",
"redirected": false,
"ms": 453,
"headers": {
"cache-control": "public, max-age=120",
"content-type": "text/html; charset=utf-8"
},
"headers_omitted": "16 (--headers for all)",
"body": "<!DOCTYPE html><html lang=\"ja\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>...",
"body_truncated": "394412 of 396412 chars hidden (--all or --budget T)"
}
Response headers that tend to be noise are omitted, but the number of omitted headers is announced as headers_omitted. Body truncation (the body in the above result has been shortened after retrieval due to its length) is also explicitly indicated by body_truncated, maintaining the design of never hiding anything implicitly. Unlike curl, a report is always returned even for empty bodies or error statuses, making it harder for agents to misinterpret results.
Discover: Understanding Page Structure
When you want to understand the structure of an unknown page, use --outline instead of dumping raw HTML.
$ ax https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/ --outlineax https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/ --outline
113 span
80 span.line
64 script
59 a.mb-2.block.text-footer-text.text-xs.hover:opacity-50
44 p.code-line
40 code
37 path
34 li.code-line
29 div
19 div.pb-1
19 a.text-footer-text.text-xs.hover:text-footer-text/50
18 div.animate-pulse.rounded-md.bg-skelton-background.h-2.w-full
12 button
11 div.code-block-container
11 svg.lucide.lucide-copy-icon.lucide-copy
...
ax: note: 18 more result(s) hidden (use --all, --limit N, or --budget T)
Tag and class combinations that appear repeatedly are listed with their counts (partially omitted here for brevity). You can see that body paragraphs have the structure p.code-line and code blocks are div.code-block-container. Elements with fewer occurrences are hidden by default, but adding --all also reveals things like h2.code-line (3 occurrences) for body headings.
You can use --locate to find where specific text appears. Let's look for the location of "agent-slash-sync" in the article.
$ ax https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/ --locate "agent-slash-sync"
[
{
"selector": "main.mx-auto.grid.gap-6.w-full.grid-cols-1.lg:max-w-screen-2xl.lg:grid-cols-[minmax(0,1fr)_calc(var(--spacing)*64)].lg:px-2.xl:grid-cols-[calc(var(--spacing)*64)_minmax(0,1fr)_calc(var(--spacing)*64)].2xl:grid-cols-[calc(var(--spacing)*64)_minmax(0,calc(var(--spacing)*213))_calc(var(--spacing)*64)] > div.w-full > div.bg-article-background.p-4.shadow-2xs.md:p-6 > article > header > div.my-4.text-article-text-muted",
"match": "AI 支援型テスト駆動開発フレームワークである『Tsumiki』は Claude Code が前提になっているのですが Gemini CLI に対応させて動かし…"
},
{
"selector": "main.mx-auto.grid.gap-6.w-full.grid-cols-1.lg:max-w-screen-2xl.lg:grid-cols-[minmax(0,1fr)_calc(var(--spacing)*64)].lg:px-2.xl:grid-cols-[calc(var(--spacing)*64)_minmax(0,1fr)_calc(var(--spacing)*64)].2xl:grid-cols-[calc(var(--spacing)*64)_minmax(0,calc(var(--spacing)*213))_calc(var(--spacing)*64)] > div.w-full > div.bg-article-background.p-4.shadow-2xs.md:p-6 > article > div.znc.mt-8 > p.code-line > a",
"match": "href=\"https://github.com/hatappo/agent-slash-sync\""
},
{
"selector": "main.mx-auto.grid.gap-6.w-full.grid-cols-1.lg:max-w-screen-2xl.lg:grid-cols-[minmax(0,1fr)_calc(var(--spacing)*64)].lg:px-2.xl:grid-cols-[calc(var(--spacing)*64)_minmax(0,1fr)_calc(var(--spacing)*64)].2xl:grid-cols-[calc(var(--spacing)*64)_minmax(0,calc(var(--spacing)*213))_calc(var(--spacing)*64)] > div.w-full > div.bg-article-background.p-4.shadow-2xs.md:p-6 > article > div.znc.mt-8 > p.code-line",
"match": "hatappo/agent-slash-sync をインストールします。"
},
...
]
You can see that the article body is located under article > div.znc. It matches not only text but also attribute values (href). Also, the second access notifies that the cache was used. Since the URL being parsed is cached for about 2 minutes, you don't need to send repeated requests to the target site while trial-and-erroring with selectors.
Extract: Structured Data Extraction
Once you understand the structure, use --row to extract multiple fields at once. The table of contents displayed in the sidebar of this page is rendered by JavaScript after the fact and therefore doesn't exist in the raw HTML, but the body headings (h2.code-line / h3.code-line) are server-rendered, so let's build the article's table of contents from them.
$ ax https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/ \
'h2.code-line, h3.code-line' --row 'section=, anchor=@id'
ax: note: 4 rows extracted, no empty fields
section anchor
AI 支援型テスト駆動開発フレームワーク『Tsumiki』 ai-%E6%94%AF%E6%8F%B4%E5%9E%8B%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%80%8Etsumiki%E3%80%8F
カスタムスラッシュコマンド %E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%82%B9%E3%83%A9%E3%83%83%E3%82%B7%E3%83%A5%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89
やってみた %E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F
さいごに %E3%81%95%E3%81%84%E3%81%94%E3%81%AB
It returned as TSV with the header once. In the --row field definition, an empty selector refers to the matched element itself, and prefixing with @ like @id refers to the attribute value.
HTML tables can be extracted as rows keyed by their headers using --table, and can be further filtered with --where. Let's pull out just 3 items from the company overview on our corporate site.
$ ax https://classmethod.jp/company/ 'table' --table --where 'col0 ~ /設立|資本金|従業員数/'
ax: note: 3 rows extracted, no empty fields
col0 col1
設立 2004年7月7日
従業員数 約820名(2025年7月現在、グループ全体)
資本金 1億円
--where uses a safe expression language that doesn't use eval, supporting conditions like price > 100 && name ~ /^foo/i. Since the table had no header row, column names like col0 and col1 appear to have been automatically assigned.
Reading Documentation as Markdown
Adding --md converts the page body to Markdown and returns it. Let's read the earlier article with a token budget.
$ ax https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/ --md --budget 200
ax: note: using 34s-old cached fetch (--fresh to refetch)
ウィスキー、シガー、パイプをこよなく愛する大栗です。
弊社クラスメソッドでは AI 支援型テスト駆動開発フレームワークである Tsumiki を公開しています。Tsumiki は Claude Code での利用を前提としています。本エントリでは Gemini CLI で動かしてみようと思います。
[https://classmethod.jp/news/20250729-tsumiki/](https://classmethod.jp/news/20250729-tsumiki/)
## [](#ai-%E6%94%AF%E6%8F%B4%E5%9E%8B%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%80%8Etsumiki%E3%80%8F) AI 支援型テスト駆動開発フレームワーク『Tsumiki』
Tsumiki は AI 駆動開発のためのテスト駆動開発フレームワークです。特に Claude Code を使用して効率的な開発プロセスを要件定義から設計、タスク分割、実装まで体系化して提供します。
Tsumiki は包括的開発フローをサポートするための Kairo コマンド、テストケースとコード実装を個別に行う TDD コマンド、リバースエンジニアリングを行う Rev コマンドなどがあります。
ax: note: 201 more result(s) hidden (use --all, --limit N, or --budget T)
Navigation and decorations are stripped away, and only the body content is returned as Markdown. This is perfect for agents reading references or blog articles, and the ability to control the amount included in the context with --budget is practically useful for AI agents.
Integrating with Claude Code
Finally, the main event: integration with Claude Code. The official installation method is npx skills add yusukebe/ax, which essentially just places the repository's skills/ax/SKILL.md into the project, but this time I'll run the official command.
$ npx skills add yusukebe/ax
The description in SKILL.md contains a trigger condition to the effect of "use the ax CLI instead of curl + throwaway scripts for fetching URLs, exploring unknown pages, and extracting structured data from HTML," designed to fire the moment an agent tries to write an inline script or bare curl.
Running Claude Code in this project, let's have it extract the table of contents from the earlier article.
Please extract the list of headings (table of contents) from the article at https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/
Excerpting the tool calls from the execution log, the flow was as follows:
- The ax skill fires automatically
- Without writing a throwaway script, it first forms a hypothesis about the heading selector and tries
--row - It uses
--textonh2to extract headings and answers with the table of contents
An example of the command Claude Code executed at this point is as follows:
ax https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/ 'h1, h2, h3, h4' --row 'tag=@tagName, text=' --all
ax https://dev.classmethod.jp/articles/tsumiki-an-ai-tdd-framework-with-gemini-cli/ 'article h2' --text; echo ---; ax https://dev.classmethod.jp/articles/tsumik…
In the end, the correct table of contents was returned: "AI-Assisted Test-Driven Development Framework 'Tsumiki' (h2) → Custom Slash Commands (h3) → Trying It Out (h2) → Conclusion (h2)."
⏺ The article's headings (table of contents) are as follows:
Article title (h1): Making the AI-Assisted TDD Framework Tsumiki Work with Gemini CLI
┌────────┬──────────────────────────────────────────────────┐
│ Level │ Heading │
├────────┼──────────────────────────────────────────────────┤
│ h2 │ AI 支援型テスト駆動開発フレームワーク『Tsumiki』 │
├────────┼──────────────────────────────────────────────────┤
│ h3 │ └ カスタムスラッシュコマンド │
├────────┼──────────────────────────────────────────────────┤
│ h2 │ やってみた │
├────────┼──────────────────────────────────────────────────┤
│ h2 │ さいごに │
└────────┴──────────────────────────────────────────────────┘
The body headings consist of 3 h2 elements and 1 h3 under the first section — a simple structure (no h4 or below).
No Python script generation or raw HTML dumping occurred at all, and it proceeded exactly according to ax's intended workflow of hypothesis → structure check → extraction. All fetches during trial-and-error hit the cache, keeping requests to the target site to a minimum. The whole thing completed in about 40 seconds.
Conclusion
After trying out ax end to end, I could feel firsthand that the typical stumbling blocks when agents handle web pages — "throwaway scripts break with structural changes" and "raw HTML overflows the context" — are all resolved at once. In particular, the output design of always announcing omissions and truncations, and adding notes even on errors, is a way of thinking that is uniquely suited to a tool for agents rather than humans.
I previously created a tool called Web2MarkDown using Cloudflare Workers, but I think ax, which works locally without requiring an API key, is wonderful.
Since it operates deterministically, I think the barrier to adding it to an existing environment is low. For those using coding agents like Claude Code, you can expect to reduce token consumption and costs by simply adding one skill, so I'd recommend giving it a try. I plan to incorporate it into my everyday agent environment and use it more and more going forward.