Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Feb 11, 2026 - 日記

Agentic CLI Designを実装する: slack-rsで学ぶ7原則の具体化

Agentic CLI Designを実装する: slack-rsで学ぶ7原則の具体化 cover image

先日、「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 listmsg postなど)も含めて、結果はJSONで返ってきます。特にラッパーコマンドは、統一エンベロープ(envelope、出力の共通ラッパー)で包まれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# ユーザー情報を取得(JSON出力)
slack-rs users info U123456 --profile my-workspace

# 出力例
{
  "schemaVersion": 1,
  "type": "users.info",
  "ok": true,
  "response": { "...": "Slack API response" },
  "meta": {
    "profile_name": "my-workspace",
    "team_id": "T123ABC",
    "user_id": "U456DEF",
    "method": "users.info",
    "command": "users info",
    "token_type": "user"
  }
}

実装のポイント:

  1. stdout/stderrの分離: 成功レスポンスはstdout、警告やガイダンスはstderrに出す(パースを壊さない)
  2. 統一エンベロープ: schemaVersion/type/ok/response/meta が揃い、機械が前提確認しやすい
  3. --rawオプション: エンベロープを外して、生のSlack APIレスポンスだけを返す

補足: api callは同じくJSONで response/meta を返しますが、schemaVersion/type/ok までは載りません(それでも機械可読ではあります)。

このエンベロープは src/api/envelope.rsCommandResponse として定義されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#[derive(Debug, Serialize, Deserialize)]
pub struct CommandResponse {
    #[serde(rename = "schemaVersion")]
    pub schema_version: u32,
    #[serde(rename = "type")]
    pub response_type: String,
    pub ok: bool,
    pub response: Value,
    pub meta: CommandMeta,
}

原則2: Non-interactive by default(非対話がデフォルト)

原則: 対話プロンプトを前提にせず、ヘッドレス環境でも完走できる。

slack-rsは--non-interactiveフラグをサポートし、stdinがTTY(対話端末)でない場合も自動で非対話モードになります。

1
2
# 非対話モードで実行(CI/CD環境など)
slack-rs --non-interactive msg post C123456 "Deployment completed" --yes

実装のポイント:

  1. TTY自動検出: CliContext::new()stdin is_terminal() を見て自動ONにする
  2. 対話が必要な操作はオプトイン: 書き込み(投稿/更新/削除など)は --yes がないと止める
  3. 非対話エラーは終了コード2: エージェントが「引数を足して再実行すべき失敗」と判定できる

コード例(src/cli/context.rsより抜粋):

1
2
3
4
pub fn new(explicit_non_interactive: bool) -> Self {
    let non_interactive = explicit_non_interactive || !std::io::stdin().is_terminal();
    Self { non_interactive }
}

原則3: Idempotent & Replayable(冪等・再実行耐性)

原則: 同じコマンドを複数回実行しても安全で、結果が予測可能。

Slack Web APIは、一般に「同じ投稿を2回叩いたら2回投稿される」タイプです。つまり、CLI側だけで完全な冪等性を作るのは難しい。

ただし、slack-rs 0.1.57ではこの弱点をそのままにしませんでした。msg post/update/deletereact add/removefile 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リトライの対象外です)。 読み取りはリトライに寄せ、書き込みは「明示的に許可されたときだけ/回数を絞って」リトライする、のが安全寄りです。

1
2
3
4
5
# メッセージ投稿(--idempotency-key なしだと重複の可能性があります)
slack-rs msg post C123456 "Hello" --yes

# idempotency key付き(同じキー+同じ内容ならreplayされます)
slack-rs msg post C123456 "Hello" --yes --idempotency-key deploy-20260207

出力はエンベロープの metaidempotency_* が付くので、エージェントは「実行されたのか/再利用されたのか」を判定できます。

1
2
3
4
5
6
{
  "meta": {
    "idempotency_key": "deploy-20260207",
    "idempotency_status": "executed"
  }
}

実装のポイント:

  1. 破壊操作の再実行は安全側: msg update/deletereact remove--yes がないと止める(無限リトライで事故りにくい)
  2. --idempotency-key でreplayできる: 同じキー+同じパラメータならキャッシュが返り、重複書き込みを避けられる
  3. キーの使い回し事故を潰す: 同じキーで別パラメータを投げるとエラー(fingerprint mismatch)

運用例(雑だけど効きます):

1
2
3
4
5
6
7
# CIのrun IDなど、外部の一意キーをidempotency keyにする
key="deploy:${GITHUB_RUN_ID:-manual}"

slack-rs msg post C123456 "Deploy started" --yes --idempotency-key "$key"

# 同じrunで再実行されてもreplayされる(重複投稿を避ける)
slack-rs msg post C123456 "Deploy started" --yes --idempotency-key "$key"

原則4: Safe-by-default(安全側デフォルト)

原則: 破壊操作はガードで制御し、実運用ではデフォルト拒否に設定して明示許可で実行します。

slack-rsの最大の特徴が書き込み保護です。

1
2
3
4
5
6
# 環境変数で書き込み操作を制御
export SLACKCLI_ALLOW_WRITE=false

# この状態でメッセージ投稿を試みるとエラー
slack-rs msg post C123456 "Hello"
# Error: Write operation denied. Set SLACKCLI_ALLOW_WRITE=true to enable write operations

実装のポイント:

  1. SLACKCLI_ALLOW_WRITE環境変数: 未設定時は true(開発時の利便性優先)だが、本番環境では明示的に false に設定して書き込みをガード
  2. 書き込み操作の分類: msg post/update/deletereact add/removeが対象
  3. 読み取り専用モード: 検索、履歴取得、ユーザー情報取得は常に許可

コード例(src/commands/guards.rsより抜粋):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pub fn check_write_allowed() -> Result<(), ApiError> {
    match std::env::var("SLACKCLI_ALLOW_WRITE") {
        Ok(value) => {
            let normalized = value.to_lowercase();
            if normalized == "false" || normalized == "0" {
                return Err(ApiError::WriteNotAllowed);
            }
            Ok(())
        }
        Err(_) => Ok(()), // 未設定時は許可(開発時の利便性優先)
    }
}

運用での推奨設定:

エージェント運用やCI/CD環境では、明示的に SLACKCLI_ALLOW_WRITE=false を設定し、必要な操作のみ true にすることで安全性を確保できます。

原則5: Observable & Debuggable(観測性・デバッグ容易性)

原則: 実行状況を観測でき、エラー時に復旧手順を判断できる。

slack-rsは終了コードとエラーメッセージで状態を伝えます。特にエージェント運用で効くのが、「非対話モードで危険操作が止まった」ケースを終了コード2として区別している点です。

1
2
3
4
5
6
# 終了コードの分類
# 0: 成功
# 1: 一般的なエラー
# 2: 非対話モードでの確認エラー
slack-rs msg delete C123456 1234567890.123456 --non-interactive
echo $?  # 2 (非対話モードで確認が必要)

実装のポイント:

  1. 終了コードの分類: 非対話エラーを2で返し、再実行指示に繋げる
  2. 再実行のヒントを返す: --yes付きの例コマンドをエラーに含める(そのままコピーできる)
  3. リトライ方針がCLIに内蔵: api callはHTTPレイヤでリトライ(指数バックオフ + レートリミット対応)する
  4. エラーガイダンスをstderrに出す: missing_scopenot_allowed_token_type など、よくあるエラーに「原因/解決」を付ける
  5. デバッグログが安全: --debug / --trace(または SLACK_RS_DEBUG=1)で追加情報をstderrに出すが、トークンは出さない

それに加えて、slack-rs 0.1.57では doctor コマンドが入り、エージェントが「まず何を確認すべきか」を機械可読で取り出せるようになっています。

1
2
# 診断情報(JSON)
slack-rs doctor --json

api callのリトライは、429(Too Many Requests、レートリミット)を検知して Retry-After ヘッダの秒数だけ待った上で再実行します。それ以外の失敗も、最大max_retries回まで指数バックオフ(失敗回数に応じて待ち時間を伸ばす方式)します(デフォルトは3回)。

また、Slack APIが返すエラーコードに対して、stderrに「原因/解決」を出します(成功レスポンスのJSONを壊さない)。

1
2
3
Error: missing_scope
Cause: The token does not have the required OAuth scope for this API method
Resolution: Re-authenticate with the required scopes. Run: slack-rs auth login

原則6: Context-efficient(コンテキスト節約)

原則: LLMのコンテキストウィンドウを無駄に消費しない。

slack-rsは必要最小限のデータのみ取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# ページングで大量データを扱う
slack-rs conv history C123456 --limit 50

# フィルタリングで絞り込み(--filter は複数回指定できる)
slack-rs conv list --filter is_member:true --filter is_private:false

# 出力形式を変える(JSON Lines、1行1JSONの形式も選べる)
slack-rs conv list --format jsonl

# ついでに「素のSlack APIレスポンスだけ」に寄せる
slack-rs conv list --raw

# api callは、環境変数でrawをデフォルトにできる
export SLACKRS_OUTPUT=raw
slack-rs api call users.info user=U123456 --get

実装のポイント:

  1. --limitオプション: 取得件数を制限
  2. --filterオプション: 絞り込みを先にかけて、JSONを小さく保つ
  3. --formatオプション: jsonl(1行1JSON)でストリーム処理しやすくする

原則7: Introspectable(自己記述できるCLI)

原則: CLI自身が仕様を機械可読で吐き、エージェントが自己発見できる。

slack-rsは、コマンド一覧やヘルプを機械可読で出せます。ここは「原則どおりに実装するならこう」ではなく、実装済みの機能として紹介できます。

1
2
3
4
5
6
7
8
# コマンド一覧(JSON)
slack-rs commands --json

# ヘルプ(JSON): `--help --json` は全コマンドに効く
slack-rs conv list --help --json

# コマンドのスキーマ(JSON Schema、JSONの構造を定義する仕様)
slack-rs schema --command msg.post --output json-schema

実践例: エージェントからslack-rsを呼ぶ

実際にAIエージェント(Claude Code など)からslack-rsを使う例を見てみます。

シナリオ: デプロイ通知を送る

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 1. 環境変数で書き込み許可(本番環境では慎重に)
export SLACKCLI_ALLOW_WRITE=true

# 2. 非対話モードで実行(書き込みは --yes を付ける)
slack-rs --non-interactive msg post C123456 "Deployment to production completed" \
  --profile prod-workspace --yes

# 3. 終了コードで成功/失敗を判定
if [ $? -eq 0 ]; then
  echo "Notification sent successfully"
else
  echo "Failed to send notification"
fi

シナリオ: チャンネル履歴を検索

1
2
3
4
5
6
7
# 1. 履歴を取得(JSON出力)
slack-rs conv history C123456 --limit 100 --profile my-workspace > history.json

# 2. jqでフィルタリング
cat history.json | jq '.response.messages[] | select(.text | contains("error"))'

# 3. エージェントがエラーメッセージを分析

既存ツールとの比較

Slack Web APIを直接呼び出すCLIツールとして、slackcatとslack-rsを比較します。

特徴slackcatslack-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単体で再現・デバッグしたい
  • MCP が向く

    • 1回の呼び出しで「調べる→判断→書く」までをまとめたい(複数API呼び出しを隠蔽したい)
    • 状態を持った対話的なワークフローが必要(例: 途中で追加情報を取りに行く、ストリーミングで逐次返す)
    • ツールの側でビジネスロジックや制約を強く持たせたい(許可/冪等/ガードを“API契約”にする)

もう少し一般論としては「Agentic CLI Design: 7つの原則 」の「CLI vs MCP」も参考になりますが、slack-rsの文脈だとAgentSkillsでCLI呼び出しを型にしておくのが一番効きます。

まとめ

Agentic CLI Designの7原則を、slack-rsの実装を通じて具体的に見てきました。

実装のポイント:

  1. JSON出力: schemaVersion/type/ok/response/meta の固定形で返す
  2. 非対話モード: --non-interactiveとTTY自動検出
  3. 書き込み保護: SLACKCLI_ALLOW_WRITE環境変数
  4. 終了コードの分類: 非対話エラーを2として区別し、再実行に繋げる
  5. 自己記述: commands --json / --help --json / schema でエージェントが自己発見できる

既存のCLIツールをエージェント向けに改善する際は、これらのパターンを参考にしてください。

興味のある方は、slack-rsのコードを読んでみてください。Rustで書かれていますが、設計パターンは他の言語でも応用できます。

参考リンク