Claude CodeでMisskeyのbotを作る — APIトークンの権限と投稿の検証
MisskeyのbotをClaude Codeに書かせる手順。write:notesだけに絞ったアクセストークンの発行、denyルール、テストインスタンスでの投稿検証までコードで示します。
Misskeyのbotに、Botトークンという別物はありません。アカウントに紐づく通常のアクセストークンを発行し、そのトークンが持つ権限の範囲でAPIを呼びます。投稿するだけのbotなら、権限はwrite:notesの1つで足ります。
この記事は、Claude Codeにbotを書かせる前に決めておくこと、つまりトークンの権限、置き場所、denyルール、テストインスタンスでの検証を、順に示します。AT Protocolを使うBlueskyのbotは別の記事にあります。
Misskeyのトークンは「権限つきの合鍵」
Misskey APIのHTTPエンドポイントはすべてPOSTで、リクエストもレスポンスもJSONです(ファイルを送るdrive/files/createだけは例外)。ベースURLはhttps://{サーバーのドメイン}/apiで、REST形式ではありません。
アクセストークンは、ユーザーに紐づく認証情報です。誰がAPIを使っているかの識別に加え、トークンごとにどの操作を許可するかが管理されます。1人のユーザーに複数のトークンを発行できます。この「一対多」が、権限設計の土台になります。
トークンの渡し方は2通りあります。
Authorization: Bearer <トークン>ヘッダー- リクエストボディJSONの
iというパラメータ
この記事ではヘッダー方式を使います。iをボディに入れると、ボディを丸ごとログに出したときにトークンも漏れるためです。
botの権限は何が必要か
権限の一覧ページにはread:とwrite:の組が並びます。投稿に関わるのは次の項目です。
| 権限 | 公式の説明 | botで必要か |
|---|---|---|
write:notes | 公式の説明ノートを作成・削除する | botで必要か投稿するなら必須 |
read:account | 公式の説明アカウントの情報を見る | botで必要か自分の情報を確認したいときだけ |
write:reactions | 公式の説明リアクションを操作する | botで必要かリアクションを付けるbotだけ |
write:drive | 公式の説明ドライブを操作する | botで必要か画像を添付するbotだけ |
write:following | 公式の説明フォロー・フォロー解除する | botで必要かフォローバックbotだけ |
/notes/createのAPI定義には、必要な権限がwrite:notesと書かれています。つまり、テキストを投稿するだけのbotに、ドライブやフォローの権限は要りません。
一覧にはwrite:admin:suspend-userやwrite:admin:delete-accountのような管理者用の権限も載っています。プロフィールに触れるwrite:accountも含め、botのトークンには付けない前提で設計します。トークンが漏れたとき、write:notesだけなら被害は「勝手な投稿」と「ノートの削除」に収まります。write:followingやwrite:driveが付いていれば、フォローの乱発やファイルの操作まで広がります。
権限一覧のページは、misskey-jsから自動生成されており、ドキュメントや翻訳が不完全な場合があると注記されています。その表示バージョンはv2025.12.2です。使うサーバーの版によって項目が違うことはあり得るので、発行画面に出る項目名が最終的な基準です。
トークンを発行する2つの経路
公式のトークン取得ページは、2つの場面を分けています。
トークン発行の2経路
手動発行
Misskey Webの「設定 > API」で、自分のアカウントのトークンを発行します。bot専用アカウントを1つ作り、そのアカウントで発行すれば、自分だけが使うbotはこれで済みます。
利用者に発行させる
MiAuth(v12.27.0以降)かOAuth 2.0(v2023.9.0以降)で、利用者に許可を求めます。旧来のアプリ作成方式は、v12.27.0未満のサーバー向けです。
手動発行の手順は、公式ページに「設定 > API」とだけ書かれています。権限を選ぶ画面の細部は、サーバーの版で違う可能性があります。記事の手順ではなく、画面に出た項目を読んでwrite:notesだけを選んでください。
利用者にトークンを発行させる場合、MiAuthのURLはクエリパラメータで権限を指定します。
https://misskey.example/miauth/<セッションID>?name=MyBot&permission=write:notespermissionには、要求する権限をカンマ区切りで並べます。セッションIDはUUIDで、毎回新しく生成し、使い回さない決まりです。許可後にhttps://{host}/api/miauth/{session}/checkへPOSTすると、トークンを含むJSONが返ります。
旧来のアプリ作成方式では、app/createのpermission配列に["write:notes"]のように渡します。ただし、こちらは古いサーバーを相手にするときだけの経路です。
専用アカウントを用意してBotフラグを立てる
コードの前に、アカウントを分けます。
- bot専用のアカウントを作る
- プロフィール設定でBotフラグをオンにする
- そのアカウントの「設定 > API」から、
write:notesだけのトークンを発行する
Botの作成ページは、Botを作ったらプロフィール設定のBotフラグをオンにしておくことを強くおすすめしています。公式が名指しで勧める設定なので、省かずに入れます。
普段使いのアカウントでbotを動かすと、投稿の履歴もトークンの被害も本人のアカウントに及びます。専用アカウントなら、トークンを失効させて作り直すだけで済みます。
環境変数名は、中身が分かる名前にします。
mkdir -p misskey-bot && cd misskey-bot
git init
printf 'MISSKEY_HOST=\nMISSKEY_TOKEN=\n' > .env.example
cp .env.example .env
printf '.env\nnode_modules/\n' > .gitignore.envの値は、Claudeに触らせず自分で入れます。MISSKEY_HOSTには、https://を付けないドメインだけを書く決めにすると、URL組み立ての食い違いが減ります。
コードより先にdenyルールを入れる
実装を頼む前に、.claude/settings.jsonで.envの読み取りを止めます。
{
"permissions": {
"deny": [
"Read(./.env)",
"Bash(curl *)",
"Bash(wget *)"
]
}
}Claude Codeの権限ドキュメントによると、ルールはdeny、ask、allowの順に評価され、最初に一致したものが結果を決めます。ルールの具体性で順序は変わらないので、allowでdenyの例外は作れません。
Read(./.env)はカレントディレクトリの.envを指す書き方で、ドキュメントにもこの例があります。Read(./.env.*)とまとめると、変数名を教える.env.exampleまで読めなくなるため、ここでは1ファイルだけを挙げました。
Bashのdenyには限界があります。Bash(curl *)は、Claudeが書いたコマンド文字列に一致するだけです。/usr/bin/curlのようにパスで呼ぶ形や、sh -c 'curl ...'の中身は止まりません。公式は、このルールを「セキュリティ境界ではない」と明記しています。コマンドの全文を検査したいなら、PreToolUseフックが使えます。書き方はHooksガイドにあります。
もう1点、見落としやすい事実があります。botをnode bot.jsで走らせると、Nodeプロセスが.envを読みます。これはdenyの対象外です。プロセスが標準出力やログにトークンを出さなければ、Claudeには渡りません。ここは権限設定ではなく、コードの規律になります。ルールの確認と調整は/permissionsコマンドの解説が参考になります。
botの実装をClaude Codeに頼む
制約ごと依頼します。効くのは、「読めないファイル」と「ログに出さない値」をプロンプトに入れることです。
Misskeyに投稿するbotをTypeScript(Node.js 20以上、標準のfetchのみ)で書いて。
- MISSKEY_HOSTとMISSKEY_TOKENを環境変数から読む。.envは読めない
- 認証はAuthorizationヘッダーのBearer。リクエストボディにiを入れない
- 投稿はPOST https://{host}/api/notes/create。visibilityはhome、localOnlyはtrue
- 失敗時はHTTPステータスとエラーのcodeだけをログに出し、トークンは出さない
- 429のときは再試行せず、そのまま終了する出力は毎回変わります。次は、この指示から期待する実装の一例です(公式のfetch例の形に沿っています)。
const host = process.env.MISSKEY_HOST;
const token = process.env.MISSKEY_TOKEN;
if (!host || !token) throw new Error("MISSKEY_HOST と MISSKEY_TOKEN が必要です");
async function post(text: string): Promise<string> {
const res = await fetch(`https://${host}/api/notes/create`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text, visibility: "home", localOnly: true }),
credentials: "omit",
});
if (!res.ok) {
throw new Error(`投稿に失敗: HTTP ${res.status}`);
}
const { createdNote } = await res.json();
return createdNote.id;
}
post("botのテスト投稿です").then((id) => console.log("投稿しました:", id));生成されたコードは、次の3点を目で確かめます。
console.logにprocess.envやリクエストヘッダーを丸ごと渡していないかiをボディに入れる実装に化けていないか- 429や5xxでループ再試行していないか
投稿の公開範囲を決める
/notes/createのvisibilityはpublic、home、followers、specifiedの4値で、省略時はpublicです。localOnlyの既定はfalseで、trueにすると連合先のサーバーに配信されません。
テスト中のbotは、省略するとpublicで連合に流れます。コード例がhomeとlocalOnly: trueを明示しているのはそのためです。本番に出すときに、この2つを意図して外します。
textの長さは1〜3000字、cw(閲覧注意の見出し)は1〜100字です。これはmisskey.ioが公開しているAPI定義の値で、サーバーによって上限が違う場合があります。長い文章を扱うbotは、自分のサーバーの/api-docで確かめてください。
テストインスタンスで投稿を検証する
本番のサーバーに、検証用の投稿を何十件も流すのは避けたいところです。自前のテストインスタンスなら、投稿もアカウントも使い捨てにできます。
Misskey Hubには、Docker Composeでサーバーを立てる手順があります。公式の流れは次のとおりです。
git clone -b master https://github.com/misskey-dev/misskey.git
cd misskey
cp .config/docker_example.yml .config/default.yml
cp .config/docker_example.env .config/docker.env
cp ./compose_example.yml ./compose.yml
# default.yml と docker.env を編集してから
sudo docker compose build
sudo docker compose run --rm web pnpm run init
sudo docker compose up -ddefault.ymlとdocker.envは、ファイル内の説明に従って編集します。ポートを変えたい場合はcompose.ymlも直します。起動したら、ブラウザでテストインスタンスにbot用アカウントを作り、そこでトークンを発行します。MISSKEY_HOSTをテストインスタンスのドメインにして、スクリプトを走らせます。
ローカルで立てたサーバーをhttp://localhost:3000のように扱う場合、サンプルコードのhttps://は書き換えが必要です。公式のサンプルはhttps前提なので、テスト環境用にスキームを環境変数へ切り出すと安全に動かせます。
権限が効いているかを確かめる
投稿が成功するだけでは、権限を絞った確認になりません。「許可していない操作が弾かれる」ことも確かめます。
write:notesだけのトークンで、別の権限が要るエンドポイントを呼びます。例えば/i(自分のアカウント情報)は、API定義上read:accountが必要です。
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
-H "Authorization: Bearer $MISSKEY_TOKEN" \
-H "Content-Type: application/json" \
-d '{}' "https://$MISSKEY_HOST/api/i"$MISSKEY_TOKENはあなたの端末のシェルで読み込み、Claudeに実行させないでください(denyルールでcurlを止めているため、そもそも実行できません)。API定義には401と403のエラーが載っており、権限が足りなければ拒否されるはずです。拒否されずに200が返ったなら、発行時に余計な権限を付けています。トークンを作り直します。
確認の対象を整理すると、次の順になります。
投稿検証の順序
- 1
テストインスタンスで投稿する
createdNote.idが返り、タイムラインに投稿が出ることを確かめます。 - 2
公開範囲を確認する
投稿が
homeで、連合先に配信されていないことを管理画面か別アカウントで見ます。 - 3
権限外の操作を弾く
write:notesだけのトークンで/iを呼び、拒否されることを確かめます。 - 4
失敗系を確認する
トークンを無効な値に変え、401系のエラーでログにトークンが出ないことを見ます。
本番に出す前の切り替えと漏れたとき
テストが済んだら、MISSKEY_HOSTとMISSKEY_TOKENを本番のサーバーの値に差し替えます。トークンは、本番のbot専用アカウントでwrite:notesだけを選んで新しく発行します。テストインスタンスのトークンは使い回しません。
visibilityとlocalOnlyの指定は、意図して外すまで残します。連合に流す必要がないbotなら、localOnly: trueのまま運用するのも選択肢です。
トークンが漏れた疑いがあれば、「設定 > API」から該当のトークンを取り消し、新しく発行し直します。1人のユーザーに複数のトークンを発行できる仕様なので、botごと、環境ごとにトークンを分けておくと、1本を取り消すだけで済みます。
チャットbotの側では、Telegram Botの記事がトークン権限の設計を扱っています。トークンを絞り、置き場所をdenyで守る考え方は共通です。
よくある質問
トークンのwrite:notesだけで、投稿の削除もできますか
権限の説明は「ノートを作成・削除する」で、削除も同じwrite:notesに含まれます。削除だけを禁じる細かい権限はありません。
レート制限の回数は決まっていますか
API定義には429(Too many requests)のレスポンスが載っていますが、/notes/createの具体的な上限は、APIの概要ページと権限一覧ページに載っていません。サーバーによって運営者が設定できるため、再試行はせず、一定間隔で投稿する設計にしておくと安全です。
リアクションやフォローも自動化したいときは
write:reactionsやwrite:followingを追加したトークンを、別に発行します。1つのトークンに足すのではなく、機能ごとにトークンを分けると、漏れたときの取り消しの影響が小さくなります。