AIエージェントのためにGoでUnixツール22個を作り直した理由

Dev.to / 2026/4/6

💬 オピニオンDeveloper Stack & InfrastructureIdeas & Deep AnalysisTools & Practical Usage

要点

  • 著者は、AIのコーディングエージェントが人間向けのコマンド出力を確実に解析できないという実務上の課題に対処するため、GoでUnixの中核ツール22個を再実装した。
  • `ls`、`grep`、`find` のようなツールが、列ベースでロケール依存かつ曖昧なテキストを出力すると、モデルは推測を強いられ、エージェントのワークフローはトークンを浪費し、見落としがちなバグにも遭遇すると主張している。
  • 提案する解決策は、見栄えの良い端末表示ではなく、ファイル名・サイズ・タイムスタンプ・種別・バイナリであるかどうかなどを含む、構造化されたラベル付きで機械可読なデータ(例:XML/フィールド)を返すことだ。
  • この記事では、これはAIエージェントに対するインターフェース変更として位置づけられており、人間が読みやすいことではなく、低い脆弱性で正確にデータを抽出することを目標としている。

Goでlsgrepcatfindstatdiff、そして他の16個のUnixコアユーティリティを、3週間かけて作り直しました。元のものが壊れているからではありません——あれらは何十年にもわたって使われ続けてきた、システムプログラミングの傑作です。作り直したのは、AIコーディングエージェントがそれらの出力を読むのがひどく下手だからです。

誰も話さなかった問題

AIエージェントがls src/を実行するたびに、だいたい次のようなものが返ってきます:

-rw-r--r--  1 user  staff  2048 Apr  6 12:00 main.go
drwxr-xr-x  3 user  staff    96 Apr  6 11:00 internal
lrwxr-xr-x  1 user  staff    12 Apr  6 10:00 link -> main.go

エージェントは、どの列がファイル名なのかを見極めなければなりません。それがサイズです。先頭のdがディレクトリを意味するのか。Apr 6が今年なのか去年なのか。推測します。ときどき推測を間違えます。そして間違った推測のたびにトークンが消費され、エラーが生まれ、書くコードの品質が低下します。

これを、1つのセッションでエージェントが実行するすべてのgrep、すべてのcat、すべてのfindに掛け算するとどうなるでしょう。トークンの無駄遣いは途方もなく大きいです。パースの脆さが、絶えず微妙なバグの原因になります。

インサイト

AIエージェントは、見栄えのいいターミナル出力を必要としていません。必要なのは構造化データです。main.goが2048バイトであること、3600秒前に更新されたこと、Goで書かれていること、MIMEタイプがtext/x-goであること、そしてバイナリファイルではないことを、知る必要があります。これらの情報は、ラベルが付いていて、曖昧さがなく、機械が読み取れる形である必要があります。

そこで私は問いかけました。もしlsがXMLを返したらどうなるでしょう?

<ls timestamp="1712404800" total_entries="3">
  <file name="main.go" path="src/main.go" absolute="/project/src/main.go"
        size_bytes="2048" size_human="2.0 KiB"
        modified="1712404800" modified_ago_s="3600"
        language="go" mime="text/x-go" binary="false"/>
  <directory name="internal" path="src/internal"/>
  <symlink name="link" target="main.go" broken="false"/>
</ls>

曖昧さゼロ。パース不要。エージェントは属性を読み取り、まさに何を見ているのかを正確に把握できます。列が揃ったテキストからファイル名を抜き出す正規表現は不要です。あるものがディレクトリかどうかを判定するための推測(ヒューリスティック)も不要です。考えなしの当て推量(ガスワーク)も不要です。

なぜXMLで、JSONではないのか?

いい質問です。JSONはAPIの共通語(リ lingua franca)です。でも、XMLにはAIのコンテキストウィンドウに関係する重要な構造上の利点があります:属性です。

同じファイルを表す次の2つを比べてみてください:

<file size_bytes="2048" language="go" mime="text/x-go"/>
{"size_bytes": 2048, "language": "go", "mime": "text/x-go"}

XML版は40文字です。JSON版は60文字です。これは33%の差です。1,000個のファイルを列挙する場合、節約できるトークンは数万単位になります。AIのコンテキストウィンドウは高価で、限られています。あらゆる文字が数えられます。

とはいえ、aictはすべてのツールで--jsonをサポートしています。スキーマは同一です。あなたのパイプラインが好む形式を使ってください。

なぜGo?

理由は3つあります:

単一バイナリ。 Goは、実行時依存関係ゼロの静的バイナリにコンパイルできます。aictは1つのファイルで、システムに置けば動きます。pip installも、npm installも不要です。管理すべき共有ライブラリもありません。コアユーティリティを置き換えることを目的としたツールなら、これは譲れません。

標準ライブラリのみ。 すべての機能——正規表現のマッチング、MIME検出、ファイルシステムの探索、XMLエンコード——はGoの標準ライブラリを使っています。外部依存ゼロは、サプライチェーン上のリスクゼロ、バージョン競合ゼロ、そして午後のうちにコードベース全体を監査できることにつながります。

性能は十分に良い。 はい、aict grepripgrepより遅いです。はい、aict lsezaより遅いです。でも、1,000ファイルの一覧表示にかかる時間を比べると、15ms対2msの話です。オーバーヘッドは、言語検出、MIMEのスニッフィング、そして構造化された出力——このプロジェクトの目的そのものの機能——に由来します。通常のコードベースでは、その差は知覚できません。

私が作ったもの

5つのカテゴリにまたがる22個のツール:

  • ファイル検査catheadtailfilestatwc
  • ディレクトリ&検索lsfindgrepdiff
  • パスユーティリティrealpathbasenamedirnamepwd
  • テキスト処理sortuniqcuttr
  • システム&環境envsystempsdfduchecksums

加えて、gitサブコマンドの一連(statusdifflogls-filesblame)と、ClaudeやCursorのようなAIアシスタントが呼び出し可能な関数としてすべてのツールを公開するMCPサーバーがあります。

すべてのツールは3つの出力モードをサポートします:XML、JSON、プレーンテキスト。すべてのエラーはstdout上の構造化データで返され、stderrではありません。すべてのパスは絶対パスです。すべてのタイムスタンプはUnixエポックの整数です。

MCPサーバー

ここからが面白いところです。aictには、aict-mcpという名前のMCP(Model Context Protocol)サーバー用バイナリが同梱されています。Claude DesktopまたはCursorでこれを設定すると、突然、すべてのツールが型付きで呼び出し可能な関数になります。

AIエージェントは aict ls src/ を実行するためにシェルへ出しません。{path: "src/"} を指定して ls 関数を呼び出し、構造化されたJSONを受け取ります。シェルの起動はありません。出力のパースもありません。曖昧さもありません。

これは、AIエージェントがファイルシステムとやり取りする未来です。人間がそうするように、ターミナルにコマンドを打ち込んで出力を読むのではありません。型付けされた関数を呼び出し、型付けされたレスポンスを受け取ることで実現します。

What I Didn't Build

意図的に書き込み操作を除外しました:cpmvrmmkdirchmodchown。これらは、人間の確認なしにAIエージェントに呼び出させると危険です。aict は読み取り専用のツールです。観察し、変更しません。

また、GNU coreutilsのフラグをフラグ単位で完全に一致させようとはしませんでした。AIのユースケースとして意味があるフラグは追加しました。そうでないものは省略しました。目標は互換性ではなく、AIエージェントのための実用性です。

The Honest Benchmark

aict を GNU coreutils とベンチマークしました。結果は以下のとおりです:

ツール GNU aict
ls(1,000ファイル) ~2ms ~15ms
grep(100k行) ~1ms ~100ms 100×
find(深いツリー) ~2ms ~9ms
cat(100k行) ~1ms ~23ms 17×
diff(1,000行) ~1ms ~10ms 10×

grepcat が遅いのは、すべてのファイルをMIMEタイプとして判定し、言語も検出するためです。内容だけが欲しい場合は --plain を使ってエンリッチ(付加情報の付与)をスキップしてください。トレードオフは意図的なものです:パースに使うトークンを増やす代わりに、より多くのセマンティック情報を返します。

Is This Actually Useful?

私は過去2か月、Claude と Cursor で aict を使っています。違いははっきり分かります。エージェントはファイルの種類について間違いが減ります。ディレクトリとファイルを混同しません。読み取ろうとする前にバイナリファイルを正しく特定します。コードベースの構造をより速く理解します。

トークンの節約は現実的です。プレーンテキストだと2,000トークンかかっていたディレクトリ一覧が、XMLでは情報密度を3倍にして800トークンで済むようになります。十数回ではなく、典型的なコーディングセッションでツール呼び出しが数十回行われるなら、その差は積み重なります。

Open Source

このプロジェクトはMITライセンスで、GitHubにあります。外部依存をゼロにしてGoで書かれています。週末ではなく“午後”あれば、コードベース全体を監査できます。ぜひ貢献してください。新しいツール、性能改善、バグ修正など歓迎です。

コードベースとやり取りするAIエージェントを作っているなら、ぜひ試してみてください。あなたのエージェントは喜ぶはずです。そして、あなたのユースケースでうまくいかなければ、それでも構いません。GNU coreutilsはどこにも行きません。

リポジトリは github.com/synseqack/aict です。