Claude CodeでProbotのGitHub Appを作る — fixtureテストと権限の決め方
ProbotでGitHub Appを作るとき、webhookのfixtureとprobot receiveで反復を回し、秘密鍵はdenyで守り、権限の絞り込みは人が決める。CLAUDE.mdの書き方を示します。
ProbotはNode.jsでGitHub Appを作るフレームワークです。webhookの受信と検証、認証まわりの面倒を引き受けてくれるので、書くのは「このイベントが来たら何をするか」の部分だけで済みます。Claude Codeに任せやすい題材ですが、任せ方を決めておかないと、実イベントを待つ手戻りと、権限の広がりすぎが起きます。
この記事の結論は2点です。反復はwebhookのpayloadを保存したfixtureで回すこと。権限とイベントの購読は、Claude Codeに広げさせず人が決めること。この2点をCLAUDE.mdとpermissions.denyに書く型を、Probotの手順に沿って示します。
ProbotのGitHub Appをなぜ実イベントなしで反復するのか
GitHub Appを作るとき、素朴にやると「issueを立てる、ログを見る、直す、また立てる」を繰り返すことになります。Probotのテスト解説も、実際のGitHubイベントを作り続けるのは面倒だと述べ、先にテストを書く進め方を勧めています。
Claude Codeは、コードを書いてテストを走らせ、失敗を読んで直すループが得意です。このループが回るのは、実行結果がその場で返るときだけです。実イベントを待つ構成では、Claude Codeが人間の操作待ちで止まります。そこで次の3層に分けます。
Probotの反復を回す3つの層
probot.receiveのテスト
Jestなどでアプリを組み立て、
probot.receiveにfixtureを渡します。GitHub APIはnockで置き換えます。Claude Codeが単独で何度でも回せる層です。probot receiveコマンド
保存したpayloadを
probot receiveで流し込みます。ネットワーク上のGitHubを経由せず、アプリのコードだけを動かせます。実機のwebhook
smee.ioを経由して手元の開発サーバーに実イベントを届けます。GitHub Appの登録と、テスト用リポジトリへのインストールが要ります。最後に人が確かめる層です。
Claude Codeに任せるのは上の2層です。3層目は登録とインストールが絡むため、人が手を動かします。
ひな形を作って開発サーバーを動かす
Probotの開発にはNode.jsの18.0.0以上が要ります。ひな形はcreate-probot-appで作ります。
node -v
npx create-probot-app my-first-app質問に答えるとテンプレートを選べます。選択肢はbasic-js、basic-ts、checks-js、git-data-js、deploy-jsです。TypeScriptで書くならbasic-tsを選びます。TypeScriptの場合はstrictNullChecksを有効にするよう案内されています。無効のままだと、webhookのpayloadの一部のフィールドがneverと推論され、context.payload.issue.idのような参照で型エラーになるためです。
生成されたindex.jsが、アプリのコードを書く場所です。npm startは内部でprobot run ./index.jsを実行します。初回は「setup mode」で起動し、APP_IDとPRIVATE_KEYが.envに入るまでwebhookを受け取れません。
イベントの受け取り方は、app.onにイベント名を渡す形です。
export default (app) => {
app.on("issues.opened", async (context) => {
const params = context.issue({ body: "Hello World!" });
return context.octokit.issues.createComment(params);
});
};issues.openedのようにアクション名まで付けて絞れます。複数のイベントは配列で渡せ、購読したイベントを全部ログに出したいときはapp.onAnyを使います。context.issue()は、イベントからowner・repo・numberを取り出して、API呼び出しの引数に混ぜてくれます。
webhookのpayloadをfixtureとして保存する
反復の土台になるのがfixtureです。作り方はProbotの手順どおりで、GitHub Appの設定ページの「Advanced」タブにある「Recent Deliveries」を開きます。展開した配信のPayloadをコピーして、test/fixtures/issues.labeled.jsonのようなファイルに保存します。
保存できるのはJSONのpayloadだけです。GitHubが送るヘッダーは含まれません。イベント名は、配信履歴のX-GitHub-Eventヘッダーで確かめます。ここで使う名前はpull_requestやissuesのようなイベント名で、labeledのようなアクション名ではありません。
この作業は、Claude Codeではなく人が一度だけ行います。保存したfixtureは、以後Claude Codeが何度でも使えます。注意点は1つあります。実リポジトリのpayloadには、ユーザー名やリポジトリ名、メールアドレスが含まれます。公開リポジトリにコミットする前に、必要な項目だけ残してダミー値に置き換えてください。
probot receiveでアプリを直接動かす
保存したfixtureは、コマンドラインから流し込めます。
node_modules/.bin/probot receive \
-e pull_request \
-p test/fixtures/pull_request.labeled.json \
./index.js-eにイベント名、-pにfixtureのパス、最後にアプリのエントリを渡します。Claude Codeはこのコマンドを実行し、出力のログを読んで、ハンドラの挙動を判断できます。ログを詳しく見たいときは、LOG_LEVEL=traceを付けて起動します。
Jestとnockで自動テストにする
回数を重ねるなら、自動テストに落とします。Probotのテスト解説は、Jestでアプリの関数をProbotインスタンスに読み込み、probot.receive({ name: "issues", payload })でイベントを流す形を示しています。GitHub APIへの呼び出しはnockで受けます。
骨組みは次のとおりです。テスト用のインスタンスでは、再試行とスロットリングを無効にして、失敗を早く見えるようにします。
import nock from "nock";
import { Probot, ProbotOctokit } from "probot";
import myProbotApp from "../index.js";
import payload from "./fixtures/issues.opened.json" with { type: "json" };
describe("My Probot app", () => {
let probot;
beforeEach(() => {
nock.disableNetConnect();
probot = new Probot({
githubToken: "test",
Octokit: ProbotOctokit.defaults({
retry: { enabled: false },
throttle: { enabled: false },
}),
});
myProbotApp(probot);
});
test("issueにコメントする", async () => {
nock("https://api.github.com")
.post("/app/installations/2/access_tokens")
.reply(200, { token: "test" });
nock("https://api.github.com")
.post("/repos/hiimbex/testing-things/issues/1/comments")
.reply(200);
await probot.receive({ name: "issues", payload });
});
afterEach(() => {
nock.cleanAll();
nock.enableNetConnect();
});
});nock.disableNetConnect()が効いているため、想定していないAPI呼び出しはテストの失敗として現れます。Claude Codeに「期待するAPI呼び出しだけをnockで宣言する」よう指示しておくと、書かせたコードが余計なAPIを叩いていないことまで確認できます。
CLAUDE.mdに書く型
ここまでの手順を、CLAUDE.mdに書き写します。Claude CodeのドキュメントはCLAUDE.mdを「強制される設定ではなく文脈」と位置づけ、検証できる具体的な書き方を勧めています。「テストを書く」ではなく「npm testを実行する」のように書く形です。長さは1ファイル200行以内が目安とされています。
## このリポジトリはProbot(GitHub App)
- ハンドラは `index.js` の `app.on(...)` に書く
- 新しいイベントを扱うときは、先に `test/fixtures/` のpayloadを使う失敗テストを書く
- fixtureは人が保存する。新しいイベントのfixtureが無いときは、作らずに私へ依頼する
- テストは `npm test`。GitHub APIは必ずnockで宣言し、実ネットワークは使わない
- 動作確認は `node_modules/.bin/probot receive -e <イベント名> -p <fixture> ./index.js`
- 購読イベントと権限(GitHub Appの設定)は変更しない。変更したいときは理由を添えて提案する
- `.env` と `*.pem` は読まない・出力しない最後の2行がこの記事の要点です。fixtureが無いときに創作させず、権限は提案に留める。この運用を文章で書いておくと、Claude Codeが手元で完結しない作業に突き当たったとき、勝手に埋めずに人へ戻します。
秘密鍵と.envはdenyで守る
Probotの開発では、APP_ID、PRIVATE_KEY(またはPRIVATE_KEY_PATH)、WEBHOOK_SECRETを.envに置きます。ダウンロードした秘密鍵の.pemファイルは、プロジェクトのルートに置くと、ファイル名にかかわらずProbotが自動で見つけます。便利な反面、Claude CodeのReadで読める場所に秘密鍵が並びます。
CLAUDE.mdの「読まない」はあくまで文脈です。読ませたくないファイルは、permissions.denyで止めます。
{
"permissions": {
"deny": [
"Read(.env)",
"Read(*.pem)"
]
}
}Claude Codeのドキュメントでは、Read(.env)のような裸のファイル名はgitignoreの規則に従い、プロジェクト内のどの深さの.envにも一致します。拒否ルールは許可ルールより先に評価されるため、あとからallowで穴を開けることもできません。
このルールはBashのcat .envのような読み取りにも効きます。ただしgrep -rのようにファイル名を指定しない読み取りや、スクリプトが自分でファイルを開く場合(pythonやnodeなど)は対象外です。OSのレベルで止めたいときはサンドボックスを使います。秘密をそもそも別の場所に置く設計も選択肢です。実際の手順は、webhookと秘密の扱いを取り上げたStripe CLIのwebhook開発が同じ型で書いています。
権限と購読イベントは人が絞り込む
Probotの開発手順では、GitHub Appの設定の「Permissions & events」で、アプリが触れるデータと購読するイベントを選びます。ここで選んだ範囲が、アプリが受け取れるwebhookを決めます。たとえばissueのイベントだけを有効にすると、pull requestのwebhookは受け取れません。
開発用の手順では、権限とイベントをすべて有効にすることが勧められています。試行錯誤の段階ではそれで動きます。ただし本番に出すときは、そのまま残さない判断が必要です。Probotのドキュメントがこの絞り込みを直接指示しているわけではありません。ここは運用上の判断として、次の流れを提案します。
権限を絞るまでの流れ
- 1
開発中は広く開ける
手順どおり、開発用のAppでは権限とイベントを広めに有効にして、動くことを確かめます。
- 2
ハンドラが使う範囲を一覧にする
Claude Codeに、
app.onで購読しているイベントと、context.octokitで呼んでいるAPIを洗い出させます。洗い出しは読み取りだけの作業なので、任せて問題ありません。 - 3
人が設定ページで絞る
一覧と見比べながら、人がAppの設定を絞ります。絞った後は、テスト用リポジトリで実イベントを流して確かめます。
権限を変えられるのはGitHubの設定ページを操作できる人だけで、Claude CodeのReadやEditの範囲外です。CLAUDE.mdに「変更せず提案する」と書くのは、この役割分担を明文化するためです。
一括操作にはdry runを付ける
権限を絞るのと並んで効くのが、アプリ自身の振る舞いの設計です。Probotのベストプラクティスは、ユーザー操作への応答は、インストールされていれば許可されたものとみなせるとしています。一方、全ての未解決issueにラベルを付けるような一括操作には、明示的な許可が必要だと述べています。
例として挙がっているのがstaleアプリで、対象リポジトリに.github/stale.ymlがあるときだけ、古いissueとpull requestの走査を行います。さらに、破壊的な操作を行うアプリには、実行せずに「何をするはずだったか」だけをログに出すdry runを備える必要があるとされ、staleアプリは設定ファイルが無いときdry runになります。
設定は対象リポジトリの.githubディレクトリに置くのが基本で、context.configで読みます。組織の.githubリポジトリから設定を引き継ぐこともできます。Claude Codeに一括操作を書かせるときは、CLAUDE.mdに次の1行を足しておくと安全です。
- リポジトリ全体に作用するハンドラ(一括ラベル付け、一括クローズ等)は、設定ファイルが無ければ dry run でログだけ出す実機で確かめる段階
fixtureで挙動が固まったら、最後に実機で確認します。手順はProbotの「Configuring a GitHub App」に沿います。npm startで起動すると、http://localhost:3000に案内が出ます。「Register a GitHub App」を押すとApp Manifestの仕組みで設定が作られ、.envにも値が書き込まれます。サーバーを再起動し、テスト用リポジトリにAppをインストールしてwebhookを発火させます。
手動で設定する場合は、次の項目を埋めます。
| 項目 | 内容 |
|---|---|
WEBHOOK_PROXY_URL | 内容smee.ioで作ったチャネルのURL。Appの「Webhook URL」にも同じものを入れる |
WEBHOOK_SECRET | 内容既定値はdevelopment。.envとAppの設定で一致させる |
APP_ID | 内容Appの設定ページの上部に表示される数値 |
| 秘密鍵 | 内容設定ページで生成してダウンロードし、プロジェクトのルートに置く |
GitHubの画面ではWebhook secretは任意ですが、Probotのアプリでは安全のために必須とされています。値が食い違うと署名検証で配信が通らないため、実機で何も起きないときは、まずここを疑ってください。
LOG_LEVELにはfatalからtrace、silentまでの段階があり、既定はinfoです。実機で原因が分からないときだけtraceに上げ、確認が済んだら戻します。
本番へ出すときの形
本番では、Probotのサーバーを自分で動かす方法のほかに、createNodeMiddlewareを使って既存のサーバーやサーバーレス環境に載せる方法もあります。このとき、webhookを受ける既定のパスは/api/github/webhooksです。変えるときはwebhooksPathで指定します。createProbot()を使うと、runと同じ環境変数から設定を読み込めます。
デプロイ先を決める段階では、Claude Codeには環境変数の一覧と、必要な手順の整理を任せられます。秘密の値そのものは、デプロイ先のシークレット管理に人が登録します。この線引きは、CLAUDE.mdの「.envと*.pemは読まない」と同じ考え方です。
GitHub App自体の運用に近いテーマは、次の記事も参考になります。Anthropicが提供するGitHub Appの導入はClaude GitHub Appのインストール手順に、組織単位の設定はGitHub Actionsの組織導入にまとめています。Webhookを受けるBotの別の例には、LINE Botの作り方があります。
まとめ
ProbotのGitHub Appは、payloadのfixtureがあれば、実イベントなしでClaude Codeに反復させられます。人がやるのはfixtureの保存、秘密の管理、権限とイベントの絞り込みの3つです。それ以外のハンドラの実装、nockを使ったテスト、probot receiveでの確認は、CLAUDE.mdに書いた手順でClaude Codeに任せられます。
最初の一手として効くのは、CLAUDE.mdの「購読イベントと権限は変更せず提案する」の1行と、.env・*.pemのdenyです。この2つを置いてから、1つ目のハンドラを書かせてください。