Claude Media
Anthropic Agent SDKでSlack常駐botを作る — 実装30分のミニチュートリアル

Anthropic Agent SDKでSlack常駐botを作る — 実装30分のミニチュートリアル

Anthropic Agent SDK(TypeScript)を使って、Slackに常駐してメンション応答するbotを30分で実装する手順。Claude Codeの代わりに自前Agentを立ち上げる入門編としても使えます。

はじめに

Agent SDKは、Claude Codeと同じ「モデル + ツール + 制御ループ」を、自分のアプリケーションから呼び出せるようにしたライブラリです。パッケージ名は @anthropic-ai/claude-agent-sdk で、内部ではClaude Codeのバイナリが動きます。CLIが肌に合わない場合や、独自の配信先(Slack / Discord / Web)でagentを動かしたい場合に向いています。Discordのスラッシュコマンドだけで足りるなら、SDKを使わずClaude Codeで完結でき、手順はClaude CodeでDiscord Botを作るにあります。

ここで作るのは、Slackのメンションに答えるbotです。完成すると次のように動きます。

  • Slackで @claude-bot この関数の意図を教えて とメンションすると、同じスレッドに返信する
  • 返信の途中で、Read / Glob / Grepなどのツールを使ってリポジトリを調べる
  • スレッドごとに会話の文脈を引き継ぐ
  • 許可していない操作は、確認待ちで止まらず拒否する

コードは @anthropic-ai/claude-agent-sdk 0.3.288と @slack/bolt 5.1.0の組み合わせを前提にしています。

前提条件

  • Node.js 20以上(Slack Bolt 5.1.0の要件。Agent SDK単体は18以上)
  • TypeScriptの基本知識
  • Anthropic API key(console.anthropic.com で取得)
  • Slack workspaceの管理権限(botをinstallできること)

認証はAPI keyで行います。Agent SDKを使う第三者の製品に、claude.aiのログインやプランの利用枠を提供することは、事前の承認がない限り認められていません。社内向けのbotでも、このチュートリアルはAPI key認証で作ります。

手順1: Slack Appを作る

Slack側の設定

Slack Appの作成から Install まで

  1. 1

    Appを作る

    api.slack.com/apps で Create New App → From scratch。App名(claude-bot など)とworkspaceを選びます。

  2. 2

    Socket Modeを有効にする

    左メニューの Socket Mode をEnableにします。WebSocketでイベントを受け取る方式で、公開URLを用意する必要がありません。接続は数時間に一度更新されますが、Boltが処理します。なお、Socket Modeを使うAppは、Slackの公開Marketplaceには現在は載せられません。

  3. 3

    App-Level Tokenを発行する

    Socket Mode用のApp-Level Tokenを生成し、xapp-1-... をメモします。

  4. 4

    Bot Token Scopesを足す

    OAuth & Permissions で app_mentions:read と chat:write を追加します。

  5. 5

    app_mentionを購読する

    Event Subscriptions をEnableにし、Subscribe to bot events に app_mention を加えます。

  6. 6

    Install to Workspace

    生成されたBot User OAuth Token(xoxb-...)をメモします。

scopeは2つで足ります。channels:history はメンション以外の発言を受け取る message.channels イベント用で、app_mention だけを購読するこのbotには要りません。

手順2: プロジェクト初期化

mkdir claude-slack-bot && cd claude-slack-bot
npm init -y
npm pkg set type=module
npm install @anthropic-ai/claude-agent-sdk @slack/bolt dotenv
npm install -D typescript @types/node tsx

npm pkg set type=module は、後で書くスクリプトの先頭で await を使うための設定です。

.env を作成します。

ANTHROPIC_API_KEY=sk-ant-...
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-1-...
BOT_WORKDIR=/path/to/your/repo

BOT_WORKDIR は、botに調べさせたいリポジトリのcloneです。SDKは .env を自動では読みません。次の手順のコードで dotenv を読み込むのはそのためです。

.gitignore:

node_modules
.env

手順3: 最小実装

src/bot.ts:

import "dotenv/config";
import { App } from "@slack/bolt";
import { query } from "@anthropic-ai/claude-agent-sdk";
 
const slack = new App({
  token: process.env.SLACK_BOT_TOKEN,
  appToken: process.env.SLACK_APP_TOKEN,
  socketMode: true,
});
 
// スレッドごとのセッション ID(再起動すると消える。本格運用なら外部に保存)
const sessionIds = new Map<string, string>();
 
async function ask(threadKey: string, prompt: string): Promise<string> {
  let reply = "";
  try {
    for await (const message of query({
      prompt,
      options: {
        model: "sonnet",
        systemPrompt:
          "あなたは Slack に常駐するアシスタントです。丁寧な敬語で、簡潔に答えてください。",
        cwd: process.env.BOT_WORKDIR ?? process.cwd(),
        allowedTools: ["Read", "Glob", "Grep"],
        permissionMode: "dontAsk",
        maxTurns: 10,
        maxBudgetUsd: 1,
        resume: sessionIds.get(threadKey),
      },
    })) {
      if (message.type === "result") {
        sessionIds.set(threadKey, message.session_id);
        if (message.subtype === "success") reply = message.result;
      }
    }
  } catch (err) {
    return `エラーが発生しました: ${err instanceof Error ? err.message : String(err)}`;
  }
  return reply || "回答を作れませんでした。質問を変えてもう一度お試しください。";
}
 
slack.event("app_mention", async ({ event, say }) => {
  const threadTs = event.thread_ts ?? event.ts;
  const prompt = event.text.replace(/<@[^>]+>/g, "").trim();
  const text = await ask(`${event.channel}:${threadTs}`, prompt);
  await say({ text, thread_ts: threadTs });
});
 
await slack.start();
console.log("Claude Slack bot is running");

package.json のscriptsに追加します。

{
  "scripts": {
    "dev": "tsx watch src/bot.ts"
  }
}

オプションの意味

query() は、メッセージを順に返す非同期イテレータです。Claudeが考え、ツールを呼び、結果を見て次に進むたびにメッセージが流れてきます。bot側が欲しいのは最後の result だけなので、ループの中でそれだけを拾っています。

オプションこのbotでの役割
modelこのbotでの役割"sonnet" などのエイリアスか、完全なモデル名を渡す
cwdこのbotでの役割エージェントが作業する場所。ファイルの読み取りもここが起点
allowedToolsこのbotでの役割確認なしで実行してよいツール
permissionModeこのbotでの役割"dontAsk" にすると、許可されていない操作は確認待ちにならず拒否される
maxTurns / maxBudgetUsdこのbotでの役割ツール往復の回数と、クライアント側の概算コストの上限
resumeこのbotでの役割前回の session_id を渡して、同じ会話を続ける

permissionModeを明示する理由

permissionMode を省略すると、セッションがauto modeで始まることがあります。TypeScript SDKはv0.3.286より前なら default と同じ扱いでしたが、それ以降は違います。人が画面の前にいないbotでは、挙動を毎回同じにするために dontAsk を明示します。

セッションをスレッド単位で持つ理由

continue: true は「そのディレクトリで最後に使ったセッション」を引き継ぎます。複数のスレッドが同じ cwd を使うと、別の会話に割り込んで続きを始めてしまいます。スレッドごとに session_id を覚えて resume に渡せば、会話は混ざりません。

resultを受け取るときの癖

error_max_turns や error_max_budget_usd で終わった場合、result メッセージを返したあとで query() が例外を投げます。try / catch で囲んでおけば、Slackにエラー文を返せます。session_id はその前に保存されるので、次のメッセージから同じ会話を再開できます。

手順4: 起動と動作確認

npm run dev

Slackで対象channelを開き、botをinviteしてからメンションします。

@claude-bot README を読んで、セットアップ手順を3行で教えてください。

botが同じスレッドで返答すれば動作確認OKです。続けてそのスレッドで質問を重ね、前の回答を踏まえた返事が来れば、resume によるセッションの引き継ぎも動いています。

botに何を許すか — 権限の設計

Claude Codeと違って、botには「許可しますか?」に答える人がいません。何を実行させるかは、コードの時点で決めておく必要があります。

読み取りだけで足りるか

手順3の設定でも、ls cat grep find git log のような読み取り専用のBashコマンドは動きます。公式の権限ドキュメントでは、これらは確認なしで実行される組み込みの一覧にあたり、allowedTools に書かなくても通ります。Bash を許可リストに加えなくても、読み取りだけの調査なら Bash を許可リストに加える必要はありません。

テストの実行など書き込みを伴うコマンドを許すなら、コマンドの形まで絞ります。

const options = {
  allowedTools: ["Read", "Glob", "Grep", "Bash(npm test *)"],
  disallowedTools: ["Bash(rm *)"],
  permissionMode: "dontAsk" as const,
};

Bash(npm test *) は、npm test で始まるコマンドだけを許します。npm test && curl ... のような複合コマンドは、区切られた部分ごとに別々に判定されます。curl の側が許可されていなければ、全体が拒否されます。

dontAskとbypassPermissionsの取り違え

くらべる

allowedTools の効き方は permissionMode で変わる

許可リストが上限になる

dontAsk

allowedTools に書いたものだけが動き、ほかは拒否されます。確認が要るはずの操作も、確認待ちにはならず拒否されます。無人で動くbotの既定にしやすい組み合わせです。

許可リストは上限にならない

bypassPermissions

allowedTools: ["Read"] と書いても、Bash や Write が通ります。リストは「確認を省く対象」を増やすだけで、ほかのツールを止めないためです。止めたいツールは disallowedTools に書きます。

disallowedTools に裸の名前("Bash" など)を書くと、そのツールはClaudeに見えなくなります。

自前のツールを足すとき

社内APIのような独自の処理は、tool() と createSdkMcpServer() でツールを定義し、mcpServers に渡します。許可リストには mcp__<サーバー名>__<ツール名> という名前で書きます。たとえば repo というサーバーの recent_commits なら mcp__repo__recent_commits です。スキーマは zod で記述します。

状態はどこに残るか

botの会話は3か所に分かれて保存されます。再起動や引っ越しで何が消えるかは、場所ごとに違います。

再起動で消えるもの

会話の状態が置かれる3か所

  • Slackのスレッド

    thread_ts で識別され、botを止めても残ります。

  • sessionIds の Map

    Node.jsのプロセスのメモリ上にあり、再起動で消えます。消えると、同じスレッドに続きを書いても新しい会話として始まります。

  • SDKのセッションファイル

    session_id に対応する履歴で、作成したマシンのディスクにあります。別のホストには引き継がれません。

コンテナを入れ替えるデプロイや複数台構成にするなら、Mapの中身を外部に保存するだけでは足りません。セッションの履歴自体も共有する必要があり、公式は SessionStore アダプターで共有ストレージに写す方法を案内しています。

よくあるつまずき

症状1: botが反応しない

  • Slack AppのEvent Subscriptionsで app_mention が登録されているか
  • Bot Token Scopesに app_mentions:read と chat:write があるか
  • botがchannelにinviteされているか(/invite @claude-bot)
  • Socket ModeがEnableか。有効にするとイベントはWebSocket経由でだけ届き、HTTPには来ません

症状2: Error: invalid_auth

  • .env のtokenが正しいか(Bot Tokenは xoxb- で始まり、App-Level Tokenは xapp- で始まります)
  • tokenをコピペしたときに空白が混入していないか

症状3: Not logged in や Invalid API key

SDK側の認証エラーです。ANTHROPIC_API_KEY が、botを動かしているプロセスの環境変数に入っていません。SDKは .env を読まないので、import "dotenv/config" が先頭にあるかを見ます。

症状4: 「エラーが発生しました」と返る

maxTurns を使い切ると error_max_turns、コストの上限に当たると error_max_budget_usd で終わります。どちらも query() が例外を投げるので、手順3の catch に入り、エラー文が返ります。maxTurns を増やすか、質問を絞ります。

「回答を作れませんでした」と返るのは、例外にならずに終わったのに result の本文が空だったときです。質問を変えて試します。

症状5: 返答が遅い

  • 応答時間を優先するなら、model を "haiku" のような軽いモデルのエイリアスに変える選択肢があります
  • query() は呼び出しのたびにClaude Codeのサブプロセスを起動します。TypeScript SDKの startup() で事前に起動しておくと、最初の呼び出しからこの待ちを外せます。使い方はstartup()とWarmQueryの解説にあります
  • 時間がかかる質問には、say で「調べています」と先に返しておくと、スレッドが止まって見えません

症状6: 返信が途中で切れる

Slackの chat.postMessage は、text を4,000字以内にするよう案内しています。4万字を超えると切り捨てられます。長い回答は、systemPromptで簡潔さを指示するか、スニペットとして添付する形にします。

症状7: 手元のClaude Codeと挙動が違う

settingSources を省略すると、SDKはユーザー・プロジェクト・ローカルの設定ファイルを読みます。botを動かすマシンの ~/.claude や、cwd にある .claude/settings.json の権限ルールが効く可能性があります。設定の影響を切りたいなら、settingSources: [] を渡します。

拡張アイデア

土台ができたら、次の方向に広げられます。

拡張概要
MCPサーバー接続概要mcpServers に既存の社内API / DBを渡す
セッションの永続化概要session_id の対応表をRedisなどに保存する
監査ログ概要PreToolUse hookで、実行しようとしたコマンドを別channelに流す
書き込みツールの追加概要Edit / Write を許す。作業用のcloneを分け、本番のワーキングツリーを渡さない
権限ロール概要発言者のSlack user IDに応じて、渡す allowedTools を切り替える

並列で複数のagentに分担させたいならSubagents並列実行パターンが、定期実行に載せるならClaude Code Routines完全実践ガイドが起動経路の選び方を扱っています。

まとめ

メンションに答えるだけのbotなら、query() に dontAsk と読み取り系の allowedTools、スレッド単位の resume を渡す数十行で動きます。難しいのは接続よりも、無人で動くagentに何を許すかの線引きです。許可は小さく始め、必要になったツールを1つずつ足す形が扱いやすくなります。

配信先をLINEにする場合は、Claude CodeでLINE Botを作る手順がWebhook受信からSDK応答までを扱っています。botの開発そのものをClaude Codeに任せる側は、Claude CodeでSlack Botを作る手順にSocket Modeでの動作確認までがあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.claude.com/en/docs/build-with-claude/agent-sdk
  2. api.slack.com/apis/socket-mode
  3. code.claude.com/docs/en/agent-sdk/overview
  4. code.claude.com/docs/en/agent-sdk/quickstart
  5. code.claude.com/docs/en/agent-sdk/permissions
  6. code.claude.com/docs/en/agent-sdk/sessions
  7. code.claude.com/docs/en/agent-sdk/typescript
  8. docs.slack.dev/reference/methods/chat.postMessage
この記事を共有:XはてブLinkedIn