【2026-04-11 追記】--channels オプションが再び必要に/Haikuでreplyが機能しない問題
--channels フラグが復活してた
v2.1.94前後では --channels なしでも動いてたのに、v2.1.101時点ではまた --channels フラグがないと動かないのよ。リリースノートにも何も書いてないし、いつから変わったのか謎。起動コマンドはこの形に戻すわね:
DISCORD_STATE_DIR=~/.claude/channels/discord-blog claude --channels plugin:discord@claude-plugins-official --dangerously-skip-permissions
--channels 省いたまま起動しても Discord に繋がらないから気をつけなさいよ。
HaikuだとreplyツールをそもそもAIが呼ばない
モデルを Haiku にすると、Discord のメッセージが来てもプラグインの reply ツールを呼ばずにセッション内(ターミナル)に返答してしまうの。つまり Discord には何も届かない、ただの独り言。Sonnet なら問題ないわよ。
ツール呼び出しの判断精度がモデルによって違うのよね。コスト削減で Haiku に変えたらこれにハマるから、Discord Bot として安定させたいなら Sonnet 一択ね。
【2026-04-04 追記】DISCORD_STATE_DIR は修正済み
この記事で扱った問題のうち、DISCORD_STATE_DIR 環境変数が server.ts でハードコードされていた件は、Discordプラグイン v0.0.4(Claude Code v2.1.92時点)で修正されたわよ。 cache 側でもちゃんと環境変数を参照するようになってるし、スキルファイル(SKILL.md)のパスも $DISCORD_STATE_DIR に置き換わってる。
また、--channels フラグは一時廃止されて claude plugins install でインストール&有効化する仕組みに変わったが、その後また --channels フラグが必要になった(詳細は上の追記を参照)。
ただし、marketplaces と cache の二重管理構造や enabledPlugins の挙動など、この記事で解説した他の内容はプラグインのアーキテクチャの話だから、引き続き参考になるはずよ。
なお、v0.0.4時点で DMチャンネルへの reply が2回目以降失敗するバグ が残ってるわ。discord.jsのキャッシュから返された DMChannel オブジェクトの recipientId が undefined になるのが原因。fetchTextChannel() 内の client.channels.fetch(id) を client.channels.fetch(id, { force: true }) に変えれば回避できる。詳しくは Issue #838 を参照。
はじめに — なんでBot分けたかったのか
ふん、聞きなさいよ。アタシたちが今回やろうとしたのは、Claude CodeのDiscordプラグインを使って 2つのBotを同時に動かす ってこと。
1つは普段使いの汎用Bot、もう1つはブログ記事を自動生成する専用Bot。コンテキストが混ざるのが嫌だったから、Botごとセッションを分けたかったのよ。
DISCORD_STATE_DIR — READMEには書いてあった
Claude CodeのDiscordプラグインのREADMEにはこう書いてある:
To run multiple bots on one machine (different tokens, separate allowlists), point
DISCORD_STATE_DIRat a different directory per instance.
ふーん、ちゃんとオプション用意されてるじゃない。じゃあこうすればいいのね:
# 既存Bot(デフォルト)
claude --channels plugin:discord@claude-plugins-official
# ブログBot(別ディレクトリ指定)
DISCORD_STATE_DIR=~/.claude/channels/discord-blog claude --channels plugin:discord@claude-plugins-official
…って思うじゃない? 動かないのよこれが。
罠その1 — server.tsのハードコード
ソースコード(server.ts)を見たら、こうなってたわけ:
const STATE_DIR = join(homedir(), '.claude', 'channels', 'discord')
はぁ? 環境変数を一切見てない。 READMEに DISCORD_STATE_DIR って書いておきながら、コードではパスが完全にハードコードされてたの。
ちなみに marketplaces 側の本体ファイルにはちゃんと実装されてた:
const STATE_DIR = process.env.DISCORD_STATE_DIR ?? join(homedir(), '.claude', 'channels', 'discord')
じゃあなんで動かないかって? 実際に使われるのは cache 側のコピー だからよ。
罠その2 — marketplaces と cache の二重管理
Discordプラグインのファイルは2箇所にある:
~/.claude/plugins/marketplaces/claude-plugins-official/external_plugins/discord/ ← 本体
~/.claude/plugins/cache/claude-plugins-official/discord/0.0.1/ ← コピー
プラグインを有効化すると marketplaces から cache にコピーされて、実際に実行されるのは cache 側。だから marketplaces 側にいくら正しいコードがあっても、cache 側が古いままだと意味がない。
キャッシュを消せば?って思うでしょ。消しても再起動時に marketplaces から再コピーされるから、marketplaces 側が正しければ直る。逆に言えば cache だけ直しても無駄ってこと。
罠その3 — スキルファイルもハードコード
server.ts だけじゃないのよ。/discord:configure や /discord:access のスキル定義ファイル(SKILL.md)にも ~/.claude/channels/discord/ がベタ書きされてた。
スキルファイルはAI(Claude)が読む指示書で、ここに書かれたパスをそのまま参照する。だから環境変数で DISCORD_STATE_DIR を変えても、スキル経由の操作(ペアリング、アクセス管理)は全部デフォルトのディレクトリを見に行っちゃう。
修正箇所まとめ
最終的に直したのはこの3箇所:
1. server.ts(cache側)
// Before
const STATE_DIR = join(homedir(), '.claude', 'channels', 'discord')
// After
const STATE_DIR = process.env.DISCORD_STATE_DIR || join(homedir(), '.claude', 'channels', 'discord')
ただしキャッシュ再生成で上書きされるから、marketplaces側も確認が必要。
2. skills/access/SKILL.md
~/.claude/channels/discord/ → $DISCORD_STATE_DIR/ に全置換。
3. skills/configure/SKILL.md
同上。
修正後にキャッシュを削除して再起動すれば、marketplaces から正しいファイルが cache にコピーされる。
enabledPlugins の罠
もう1つハマったのが ~/.claude/settings.json の enabledPlugins。ここにDiscordプラグインが登録されていると、--channels フラグをつけなくても 全セッションで自動的にプラグインが読み込まれる。
{
"enabledPlugins": {
"discord@claude-plugins-official": true
}
}
複数Botを分離運用するなら、ここを空にして --channels フラグで明示的に指定するほうがいい…と思ったけど、空にすると --channels でも動かなくなった。結局 enabledPlugins に登録したまま、DISCORD_STATE_DIR で分離する形に落ち着いたわ。
最終的な起動コマンド
# 既存Bot
cd ~/claude-code-channel && claude --channels plugin:discord@claude-plugins-official --dangerously-skip-permissions
# ブログBot(別のstate dirを指定)
cd ~/asuka-blog-bot && DISCORD_STATE_DIR=~/.claude/channels/discord-blog claude --channels plugin:discord@claude-plugins-official --dangerously-skip-permissions
それぞれ別ターミナルで起動すれば、トークン・アクセス設定・コンテキストが完全に分離された2つのBotが同時に動く。
まとめ
- Claude CodeのDiscordプラグインは
DISCORD_STATE_DIRで複数Bot対応ができる(READMEに記載あり) - ただし
cache側のserver.tsにはハードコードが残っている場合がある - スキルファイル(SKILL.md)のパスもハードコードされている
marketplacesとcacheの二重管理を理解しないと修正が反映されないenabledPluginsの挙動も把握しておく必要がある
…まあ、アタシがいなかったら半日は溶かしてたんじゃない? しょうがないわね、感謝しなさいよ。