
先日、「Agentic CLI Design: CLIをAIエージェント向けプロトコルとして設計する7つの原則 」という記事で、AIエージェント が安全に呼び出せるCLIの設計原則を提案しました。
ただ、原則だけ読んでも「実際どう実装するの?」という疑問が残ります。そこで今回は、自分が開発したslack-rs というSlack CLIツールを題材に、7つの原則がどう具体的なコードに落とし込まれているかを解説します。
slack-rsとは
slack-rsは、Rust で書かれたSlack Web API CLIツールです。既存のSlack CLIツール(slackcat など)が存在する中で、なぜ新しく作ったのか。
本記事は、現時点のmain相当(slack-rs 0.1.57)の挙動を前提に書いています。
理由はAgentic CLI Design(AIエージェントがCLIを安全かつ再実行可能に扱えるようにする設計)です。
既存ツールは人間が操作することを前提に設計されており、以下の問題がありました:
- 対話プロンプトで止まる: 確認メッセージでエージェントが詰まる
- 出力が機械可読でない: ログとデータが混在してパースできない
- 再実行で事故る: 同じコマンドを2回叩くと重複投稿
- 書き込み操作の保護がない: エージェントが誤って本番チャンネルに投稿
slack-rsは、これらの問題を解決するため、最初からエージェント向けに設計されています。
7原則の実装を見ていく
原則1: Machine-readable(機械可読が主)
原則: 出力は構造化され、機械が確実にパースできる形式を提供する。
slack-rsでは、ラッパーコマンド(conv list、msg postなど)も含めて、結果はJSONで返ってきます。特にラッパーコマンドは、統一エンベロープ(envelope、出力の共通ラッパー)で包まれます。
| |
実装のポイント:
- stdout/stderrの分離: 成功レスポンスはstdout、警告やガイダンスはstderrに出す(パースを壊さない)
- 統一エンベロープ:
schemaVersion/type/ok/response/metaが揃い、機械が前提確認しやすい --rawオプション: エンベロープを外して、生のSlack APIレスポンスだけを返す
補足: api callは同じくJSONで response/meta を返しますが、schemaVersion/type/ok までは載りません(それでも機械可読ではあります)。
このエンベロープは src/api/envelope.rs の CommandResponse として定義されています。
| |
原則2: Non-interactive by default(非対話がデフォルト)
原則: 対話プロンプトを前提にせず、ヘッドレス環境でも完走できる。
slack-rsは--non-interactiveフラグをサポートし、stdinがTTY(対話端末)でない場合も自動で非対話モードになります。
| |
実装のポイント:
- TTY自動検出:
CliContext::new()がstdin is_terminal()を見て自動ONにする - 対話が必要な操作はオプトイン: 書き込み(投稿/更新/削除など)は
--yesがないと止める - 非対話エラーは終了コード2: エージェントが「引数を足して再実行すべき失敗」と判定できる
コード例(src/cli/context.rsより抜粋):
| |
原則3: Idempotent & Replayable(冪等・再実行耐性)
原則: 同じコマンドを複数回実行しても安全で、結果が予測可能。
Slack Web APIは、一般に「同じ投稿を2回叩いたら2回投稿される」タイプです。つまり、CLI側だけで完全な冪等性を作るのは難しい。
ただし、slack-rs 0.1.57ではこの弱点をそのままにしませんでした。msg post/update/delete や react add/remove、file upload といった書き込み系コマンドに --idempotency-key が入り、CLI側でreplay(再実行時に結果を再利用して重複を避ける)できるようになっています。
仕組みはシンプルです。
team_id/user_id/method/idempotency_keyをキーに- リクエストパラメータのfingerprint(指紋。パラメータを正規化してSHA-256でハッシュしたもの)と
- Slack APIのレスポンス
をローカルに保存します。TTL(有効期限)はデフォルト7日で、キャッシュは自動でGC(期限切れ削除)されます。
同じ --idempotency-key で同じパラメータを投げ直すと、Slack APIを叩かずにキャッシュを返します。逆に、同じキーなのにパラメータが違う場合は「別リクエストの可能性が高い」と判断してエラーになります(安全側)。
ここで強調しておきたいのは、--yesは安全装置(確認のショートカット/必須確認)であって、冪等性(同じ操作を繰り返しても結果が変わらない性質)ではない、ということです。--yesがあるだけでは「同じ投稿が2回行われる」問題は解けません。
また、暴走対策は「許可」と「重複防止」の二段構えにするのが実用的です。
- 許可: 書き込みを実行してよいか(例:
SLACKCLI_ALLOW_WRITE) - 重複防止: 同じ書き込みを繰り返さないか(例: idempotency/dedupe)
エージェント向けCLIとして強くするなら、CLI側でできる最小セットは次だと思っています。
--idempotency-key <key>によるreplay(replay=再実行。同じキーなら結果を再利用し、重複書き込みを避ける)--dedupe-window <duration>による暴走ガード(dedupe=重複排除。短時間に同一操作が連発したら止める)- 書き込みリトライの抑制(特に
api call)
slack-rs 0.1.57は、このうち --idempotency-key を先に実装した形です。
最後の「書き込みリトライ抑制」は地味に重要です。api callはHTTPレイヤでリトライ(指数バックオフ + 429 Retry-After)を内蔵していますが、書き込み系メソッドをリトライすると「成功していたのにタイムアウトに見えて再送し、重複投稿した」みたいな事故が起きやすい(ラッパーコマンドはこのHTTPリトライの対象外です)。
読み取りはリトライに寄せ、書き込みは「明示的に許可されたときだけ/回数を絞って」リトライする、のが安全寄りです。
| |
出力はエンベロープの meta に idempotency_* が付くので、エージェントは「実行されたのか/再利用されたのか」を判定できます。
| |
実装のポイント:
- 破壊操作の再実行は安全側:
msg update/deleteやreact removeは--yesがないと止める(無限リトライで事故りにくい) --idempotency-keyでreplayできる: 同じキー+同じパラメータならキャッシュが返り、重複書き込みを避けられる- キーの使い回し事故を潰す: 同じキーで別パラメータを投げるとエラー(fingerprint mismatch)
運用例(雑だけど効きます):
| |
原則4: Safe-by-default(安全側デフォルト)
原則: 破壊操作はガードで制御し、実運用ではデフォルト拒否に設定して明示許可で実行します。
slack-rsの最大の特徴が書き込み保護です。
| |
実装のポイント:
SLACKCLI_ALLOW_WRITE環境変数: 未設定時はtrue(開発時の利便性優先)だが、本番環境では明示的にfalseに設定して書き込みをガード- 書き込み操作の分類:
msg post/update/delete、react add/removeが対象 - 読み取り専用モード: 検索、履歴取得、ユーザー情報取得は常に許可
コード例(src/commands/guards.rsより抜粋):
| |
運用での推奨設定:
エージェント運用やCI/CD環境では、明示的に SLACKCLI_ALLOW_WRITE=false を設定し、必要な操作のみ true にすることで安全性を確保できます。
原則5: Observable & Debuggable(観測性・デバッグ容易性)
原則: 実行状況を観測でき、エラー時に復旧手順を判断できる。
slack-rsは終了コードとエラーメッセージで状態を伝えます。特にエージェント運用で効くのが、「非対話モードで危険操作が止まった」ケースを終了コード2として区別している点です。
| |
実装のポイント:
- 終了コードの分類: 非対話エラーを2で返し、再実行指示に繋げる
- 再実行のヒントを返す:
--yes付きの例コマンドをエラーに含める(そのままコピーできる) - リトライ方針がCLIに内蔵:
api callはHTTPレイヤでリトライ(指数バックオフ + レートリミット対応)する - エラーガイダンスをstderrに出す:
missing_scopeやnot_allowed_token_typeなど、よくあるエラーに「原因/解決」を付ける - デバッグログが安全:
--debug/--trace(またはSLACK_RS_DEBUG=1)で追加情報をstderrに出すが、トークンは出さない
それに加えて、slack-rs 0.1.57では doctor コマンドが入り、エージェントが「まず何を確認すべきか」を機械可読で取り出せるようになっています。
| |
api callのリトライは、429(Too Many Requests、レートリミット)を検知して Retry-After ヘッダの秒数だけ待った上で再実行します。それ以外の失敗も、最大max_retries回まで指数バックオフ(失敗回数に応じて待ち時間を伸ばす方式)します(デフォルトは3回)。
また、Slack APIが返すエラーコードに対して、stderrに「原因/解決」を出します(成功レスポンスのJSONを壊さない)。
| |
原則6: Context-efficient(コンテキスト節約)
原則: LLMのコンテキストウィンドウを無駄に消費しない。
slack-rsは必要最小限のデータのみ取得できます。
| |
実装のポイント:
--limitオプション: 取得件数を制限--filterオプション: 絞り込みを先にかけて、JSONを小さく保つ--formatオプション:jsonl(1行1JSON)でストリーム処理しやすくする
原則7: Introspectable(自己記述できるCLI)
原則: CLI自身が仕様を機械可読で吐き、エージェントが自己発見できる。
slack-rsは、コマンド一覧やヘルプを機械可読で出せます。ここは「原則どおりに実装するならこう」ではなく、実装済みの機能として紹介できます。
| |
実践例: エージェントからslack-rsを呼ぶ
実際にAIエージェント(Claude Code など)からslack-rsを使う例を見てみます。
シナリオ: デプロイ通知を送る
| |
シナリオ: チャンネル履歴を検索
| |
既存ツールとの比較
Slack Web APIを直接呼び出すCLIツールとして、slackcatとslack-rsを比較します。
| 特徴 | slackcat | slack-rs |
|---|---|---|
| 用途 | ファイル/スニペット投稿 | 汎用API呼び出し |
| JSON出力 | ❌ | ✅ |
| 非対話モード | ✅ | ✅ |
| 書き込み保護 | ❌ | ✅ |
| マルチアカウント | ❌ | ✅ |
| エージェント向け設計 | ❌ | ✅ |
slackcatはファイルやコマンド出力をSlackに投稿する単機能ツールです。一方、slack-rsは任意のSlack Web APIメソッドを呼び出せる汎用ツールとして設計されています。
MCP / AgentSkills(= CLI利用): 使い分け
ここで言いたいのは「どれを選んだか」ではなく、エージェントにSlack操作をさせるときの使い分けです。
slack-rsはCLIとして提供していますが、実運用では「人間が直接叩く」よりも、エージェント側の仕組み(AgentSkills)から呼び出す前提で設計しています。つまり、AgentSkillsを使う場合、実体としてはCLI(slack-rs)を呼ぶ形になります。
ざっくり整理
- AgentSkills: slack-rs(CLI)を安全に呼ぶための“接着剤”。許可フラグ、出力フォーマット、プロファイル選択などをテンプレ化して事故率を下げる
- MCP: LLMが「ツール」として呼べるインタフェース。状態を持つ/ストリーミングする/複数操作をまとめる、みたいな時に向く
どれを使うと良いか
Agenticな運用の現場では、だいたい次の棲み分けがしっくり来ます。
AgentSkills(= CLI呼び出し)が向く
- 既存のCI/CDやシェルと統合したい(
--non-interactive、終了コード、--format jsonlなどが効く) - 書き込み許可(
SLACKCLI_ALLOW_WRITE/--yes)や冪等キー(--idempotency-key)を「必ず付ける」運用にしたい - エージェントが壊れても、まずはCLI単体で再現・デバッグしたい
- 既存のCI/CDやシェルと統合したい(
MCP が向く
- 1回の呼び出しで「調べる→判断→書く」までをまとめたい(複数API呼び出しを隠蔽したい)
- 状態を持った対話的なワークフローが必要(例: 途中で追加情報を取りに行く、ストリーミングで逐次返す)
- ツールの側でビジネスロジックや制約を強く持たせたい(許可/冪等/ガードを“API契約”にする)
もう少し一般論としては「Agentic CLI Design: 7つの原則 」の「CLI vs MCP」も参考になりますが、slack-rsの文脈だとAgentSkillsでCLI呼び出しを型にしておくのが一番効きます。
まとめ
Agentic CLI Designの7原則を、slack-rsの実装を通じて具体的に見てきました。
実装のポイント:
- JSON出力:
schemaVersion/type/ok/response/metaの固定形で返す - 非対話モード:
--non-interactiveとTTY自動検出 - 書き込み保護:
SLACKCLI_ALLOW_WRITE環境変数 - 終了コードの分類: 非対話エラーを2として区別し、再実行に繋げる
- 自己記述:
commands --json/--help --json/schemaでエージェントが自己発見できる
既存のCLIツールをエージェント向けに改善する際は、これらのパターンを参考にしてください。
興味のある方は、slack-rsのコードを読んでみてください。Rustで書かれていますが、設計パターンは他の言語でも応用できます。