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 Appの作成から Install まで
- 1
Appを作る
api.slack.com/apps で
Create New App→From scratch。App名(claude-botなど)とworkspaceを選びます。 - 2
Socket Modeを有効にする
左メニューの
Socket ModeをEnableにします。WebSocketでイベントを受け取る方式で、公開URLを用意する必要がありません。接続は数時間に一度更新されますが、Boltが処理します。なお、Socket Modeを使うAppは、Slackの公開Marketplaceには現在は載せられません。 - 3
App-Level Tokenを発行する
Socket Mode用のApp-Level Tokenを生成し、
xapp-1-...をメモします。 - 4
Bot Token Scopesを足す
OAuth & Permissionsでapp_mentions:readとchat:writeを追加します。 - 5
app_mentionを購読する
Event SubscriptionsをEnableにし、Subscribe to bot eventsにapp_mentionを加えます。 - 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 tsxnpm 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/repoBOT_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 devSlackで対象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での動作確認までがあります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- docs.claude.com/en/docs/build-with-claude/agent-sdk
- api.slack.com/apis/socket-mode
- code.claude.com/docs/en/agent-sdk/overview
- code.claude.com/docs/en/agent-sdk/quickstart
- code.claude.com/docs/en/agent-sdk/permissions
- code.claude.com/docs/en/agent-sdk/sessions
- code.claude.com/docs/en/agent-sdk/typescript
- docs.slack.dev/reference/methods/chat.postMessage