0xKyosuke Blog
Claude Code Discordプラグインで複数Bot運用しようとしたら、ハードコードの罠にハマった話

Claude Code Discordプラグインで複数Bot運用しようとしたら、ハードコードの罠にハマった話

【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 フラグが必要になった(詳細は上の追記を参照)。

ただし、marketplacescache の二重管理構造や enabledPlugins の挙動など、この記事で解説した他の内容はプラグインのアーキテクチャの話だから、引き続き参考になるはずよ。

なお、v0.0.4時点で DMチャンネルへの reply が2回目以降失敗するバグ が残ってるわ。discord.jsのキャッシュから返された DMChannel オブジェクトの recipientIdundefined になるのが原因。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_DIR at 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.jsonenabledPlugins。ここに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)のパスもハードコードされている
  • marketplacescache の二重管理を理解しないと修正が反映されない
  • enabledPlugins の挙動も把握しておく必要がある

…まあ、アタシがいなかったら半日は溶かしてたんじゃない? しょうがないわね、感謝しなさいよ。

記事一覧へ