はじめに
Socratic Tutor という VS Code 拡張機能を作って、Marketplace に公開しました。
ひとことで言うと、Claude Code を「答えを教えないチューター」に変える拡張機能です。
チューターモードを ON にすると、Claude Code は次のように振る舞います。
- 解答コード・修正済みコードを出さない
- ファイル編集もコマンド実行もしない(ツール権限レベルでブロック)
- 一度に 1 つだけ質問して、こちらの答えを待つ
- 「仮説 → 検証」の型でデバッグを進めさせる
こんな対話になります
User:
AppService での正常性チェック用のエンドポイントを実装してください
Assistant:
正常性チェック(Health check)エンドポイントの実装ですね。過去の障害(オートスケール不全・OOM)と関わる大事な仕組みなので、順を追って設計から一緒に考えましょう。
まず要件の確認から一つ質問します。
Azure App Service の「正常性チェック」機能は、指定したパスに対して何をして、その結果をどう使う仕組みだと理解していますか? (現時点の理解でかまいません。曖昧な部分は「ここが分からない」と言ってもらえれば、公式ドキュメントのどこを読むべきかを示します)
User:
指定したパスに対してリクエストをして、正常に結果が返ってくるかを確認します。異常が続くようであれば異常が起きているインスタンスを置換する仕組みです。
Assistant:
理解は正確です。特に「異常が続けばインスタンスが置換される」まで押さえているのは重要で、実装の設計にも直接影響します。
では次の一歩。その「正常に結果が返ってくる」を、App Service 側は具体的にどんな条件で判定しているでしょうか。
判定条件として、どんな要素が関わると思いますか?(思いつくものを挙げてみてください。根拠が薄い部分は「推測」と付けてもらえれば構いません)
ヒントとして調べる先を挙げておきます: Azure の公式ドキュメント「App Service の正常性チェック」の中に、判定に使う HTTP ステータスの範囲・タイムアウト・失敗回数のしきい値・リクエスト間隔について書かれた節があります。
User:
HTTP ステータスコード、タイムアウトなどだと思います。
Assistant:
方向は合っています。ここは推測のままにせず、事実を確定させましょう。実装するときに「何を返せばよいか」が数字レベルで決まらないと設計できません。
公式ドキュメント「App Service の正常性チェック」を読んで、次の3点を報告してください。
- 「正常」とみなされる HTTP ステータスコードの範囲
- リクエストが送られる間隔と、タイムアウトとみなされる時間
- 何回連続で失敗するとインスタンスが除外/置換されるか(設定で変えられるか)
検索キーワードの作り方の例: App Service 正常性チェック / App Service health check requirements。ドキュメント内では「正常性チェックのパスの要件」や「動作」あたりの節に書かれています。
読んだ結果を教えてください。そこから「では、そのパスは何を検査すべきか」に進みます。
User:
正常性チェックは1分間隔ですべてのインスタンスに ping が実行される
10 回要求しても 200 から 299 までのレスポンスコードが返ってこない場合異常と判断される
Assistant:
良い調査です。「1分間隔・全インスタンス・10回連続で 2xx が返らなければ異常」という具体的な数字が取れたので、設計の判断材料が揃いました。ここから逆算すると、「異常と判定されるまで約10分の猶予がある」ことも読み取れますね。
では設計に進みます。次の一歩の質問です。
そのパスにリクエストが来たとき、アプリ側では「何を確認できたら 200 を返す」べきだと思いますか? 検査対象の候補を挙げて、それぞれ「なぜ入れる/入れない」の理由も添えてください。
(考えるヒント: このアプリが動作するために依存しているものを列挙してみると候補が見えてきます。)
User:
アプリ側は Web サーバのプロセスが生きていることを確認出来たらよいと思います。
Assistant:
それは一つの明確な方針です。ただ、判断の根拠を言葉にしておくことが大切です。
なぜ「Web サーバのプロセスが生きていること」だけで十分だと考えましたか? 逆に、DB 接続やキャッシュ(Redis)などの依存先の確認を含めない理由は何でしょうか?
考えるヒント: 先ほど確認した「異常なインスタンスは置換される」という挙動と、「DB は全インスタンスが共有している」という事実を組み合わせると、依存先の確認を含めた場合に何が起きるか予想できます。
User:
DB やキャッシュサーバは全インスタンス共通のため、特定のインスタンスが異常である場合、DB やキャッシュサーバが死んでいる可能性は論理的にあり得ないからです
Assistant:
筋の通った推論です。「特定の1台だけが異常」という状況を想定すれば、共有リソースの死活は原因として矛盾する、という論理は正しく成立しています。
ではその推論をもう一段先に進めてみましょう。
逆のケースを考えてください。もし本当に DB が落ちた場合、DB 接続確認を含むヘルスチェックを全インスタンスが同時に受けたら、それぞれどう判定されますか? そして App Service 基盤側は、その判定結果を受けてどんな動作をすると思いますか?
(先ほど確認した「1分間隔・全インスタンス・10回連続失敗で置換」という仕組みを踏まえて考えてみてください)
User:
DB が死んでいる場合、すべてのインスタンスが異常となり、すべてのインスタンスを置換しようとします
Assistant:
その予測は妥当です。ここまでで、あなたは自力で重要な設計原則にたどり着きました。
その「全インスタンス置換」という動作は、DB 障害という問題に対して、事態を良くしますか、悪くしますか? なぜそう考えますか?
(考えるヒント: 置換中のインスタンスはリクエストを処理できません。DB 障害は置換したところで直るものかどうか、という観点も含めて考えてみてください)
User:
悪くします。
DB 障害は解決できないからです
Assistant:
その結論に至ったのは大きな前進です。DB 障害という「解決できない」問題に対して、インスタンス置換という「効かない対処」を自動的に発動させてしまう——これはまさに、正常性チェックを設計する際の重要な落とし穴です。
ここで一度、理解を整理してもらいます。
「正常性チェックのエンドポイントは何を確認すべきで、何を確認すべきでないか」を、あなた自身の言葉で一文か二文にまとめてみてください。
その上で、次のステップ——実際にどこに・どうやって実装するか(ルーティング、レスポンス内容)——に進みます。
User:
正常性チェックのエンドポイントは、Web サーバがリクエストを受け付け、レスポンスを返せているかどうかを確認すべきで、Web サーバが依存している他サービスとの疎通までは確認すべきではないです
Assistant:
良い定式化です。これで判定基準(何を含み、何を含まないか)が明確になりました。
次はいよいよ実装の段になります。ここは仮説検証というより設計・実装の話なので、通常の質問形式で進めます。
まず、このプロジェクトのフレームワークとバージョンを確認しましょう。
sample/composer.json を開いて、laravel/framework のバージョン(または require セクションの内容)を確認してください。何が書かれていますか?
(Laravel には比較的新しいバージョンから正常性チェック用のルートが標準機能として用意されている場合があります。バージョンによって「自分でルートを書く」か「標準機能を有効化する」かの選択肢が変わるので、まずここを確認します)
User:
Laravel の 9.11 以上という意味が記載されています。
Assistant:
Laravel 9系ですね。Laravel の正常性チェック標準機能(health ルーティング)は Laravel 11 以降で追加されたものなので、9系では自分でルートとレスポンスを実装する必要があります。これで方針が固まりました。
次に、実装する上で注意すべき点を考えてもらいます。
Azure の正常性チェックがこのエンドポイントにリクエストを送るとき、通常のブラウザからのアクセスと比べて「持っていないもの」は何だと思いますか?(例えば、認証情報、セッション、CSRF トークンなど)
そしてそれが、routes/web.php に定義されている既存のルートが通過する ミドルウェア と、どう関係してくるでしょうか?
(ヒント: sample/routes/web.php を開いて、ルートがどの middleware グループに属しているか確認してみてください)
User:
Azure 正常性チェックでは、Cookie や認証情報を持っていません。そのため、middleware でのチェックに弾かれると 200 系を返せなくなります。
Assistant:
良い気づきです。「持っていないものが何か」から「どのミドルウェアで弾かれるか」への接続ができています。
ここで実際のファイルを見てみましょう。
(routes/web.php を確認 — siteSecLog ミドルウェアで全ルートがラップされ、個別に confirmCookie ミドルウェアが適用されているルート群があることを確認)
このファイルを見ると、すべてのルートが siteSecLog という共通ミドルウェアでラップされていることが分かります。また confirmCookie は個別ルート群だけを保護する形になっています。
ここで一つ確認したいことがあります。最近のコミット履歴に「大量ログが ConsoleLog に出力される件の修正」というものがありました。
siteSecLog ミドルウェアの中身を見て、それが何を行っているか確認してください。そして、正常性チェックが1分ごとに全インスタイスへ送られ続けることを踏まえると、このミドルウェアの配下にヘルスチェック用ルートを置くと何が起こり得ると思いますか?
(app/Http/Kernel.php で siteSecLog が \App\Http\Middleware\SiteSecurityLog::class に対応することを確認)
sample/app/Http/Middleware/SiteSecurityLog.php の内容を見てください(handle メソッドの中で parent::logging(...) が毎リクエスト呼ばれています)。
このミドルウェアの配下にヘルスチェック用ルートを置いた場合、1分間隔・全インスタンスからのアクセスによって、何が積み重なっていくと思いますか? そして、それは以前修正された「大量ログが ConsoleLog に出力される件」とどう関係しますか?
User:
セキュリティログを出力します。正常性チェックのリクエストごとにログ出力をすると無駄なログが大量に記録されることになります。
Assistant:
正しく結びつけられました。これは前回修正されたログ大量出力の問題と全く同じ構造の"再発"になり得るという点、重要な気づきです。
では、この気づきを設計に反映させます。
その結論から、このヘルスチェック用ルートは web.php 内の Route::middleware('siteSecLog')->group(function () { ... }) の内側と外側、どちらに書くべきだと考えますか? 根拠も添えてください。
User:
外側です。不要なログを出力しないためです。
Assistant:
良い判断です。ただ、もう一段確認しておくべきことがあります。ファイルの先頭のコメントに書かれている通り、web.php 内のルートは基本的に RouteServiceProvider によって web ミドルウェアグループ(StartSession、VerifyCsrfToken など)に包まれています。siteSecLog の外に置いても、この web グループの影響は残ります。
セッションを開始する StartSession ミドルウェアが、1分ごと・全インスタンスへのヘルスチェックのたびに動作すると何が起きると思いますか? 特に、このアプリのセッションドライバ(config/session.php の SESSION_DRIVER)が何を使っているかを確認して、それと合わせて考えてみてください。
User:
SESSION_DRIVER は file となっています。.env でも file なので、セッションが開始されるとセッション管理用のファイルが大量に作成されることになります。
Assistant:
その気づきも正確です。これは以前の OOM 障害(ディスク/メモリ枯渇)とも関わる観点で、セッションファイルの増殖はサーバリソースを静かに消費し続けます。
ここまでで、ヘルスチェック用ルートには web ミドルウェアグループそのものを通したくない という結論に至っていますね。
RouteServiceProvider(app/Providers/RouteServiceProvider.php)を開いて、web.php や api.php がどのようにミドルウェアグループと結び付けられているかを確認してください。 そこから、「グループに属さない全く新しいルート」をどこに・どうやって登録すればよいか、手がかりが見えてくるはずです。何が書かれていましたか?
User:
どのファイルがどのミドルウェアグループに属するかが記載されています。routes/health_check.php を作成し、ルートに登録する、という感じでしょうか
Assistant:
(RouteServiceProvider.php の内容を確認 — boot() 内で Route::middleware('api')->prefix('api')->group(base_path('routes/api.php')); と Route::middleware('web')->group(base_path('routes/web.php')); が個別に登録されていることを確認)
この確認から、web.php / api.php がそれぞれ Route::middleware('web')->group(...) / Route::middleware('api')->prefix('api')->group(...) という形で boot() 内に個別登録されている、という構造が見えましたね。routes/health_check.php を新設する、という方向性は良い着眼点です。
では、この新しいファイルを web や api のようなミドルウェアグループに「入れずに」RouteServiceProvider::boot() に登録するには、上の2行のパターンをどう書き換えればよいと思いますか? (Route::middleware(...) の引数や、ミドルウェアを指定しない場合の書き方を考えてみてください。コードは書かなくてよいので、言葉か擬似コードで説明してください)
User:
Route::get('/health', function () { return 200;}); みたいな感じですか
Assistant:
その例は「ルートの中身(何をレスポンスするか)」の案ですね。今聞いているのはその一歩手前、RouteServiceProvider::boot() の中で、この新しいファイルを web ミドルウェアグループに入れずに読み込むには、どう書けばよいか という点です。
もう一度、先ほど見た2行と比較してみてください。
Route::middleware('api')->prefix('api')->group(base_path('routes/api.php'));
Route::middleware('web')->group(base_path('routes/web.php'));
health_check.php を読み込む3行目を、この形式に沿って考えるとどう書けますか?(middleware() に何を渡すか、あるいは呼ばないか、という点がポイントです)
User:
Route::group(base_path('routes/health_check.php')); って感じですか
Assistant:
方向性は良いところまで来ていますが、Route::group() の引数の数を確認してみましょう。
IDE の補完機能や vendor/laravel/framework/src/Illuminate/Routing/Router.php の group() メソッドの定義を見て、このメソッドが何を・何個引数として受け取るか確認してください。 それを踏まえて、web.php や api.php を読み込んでいる既存の2行との違いを考え直してみてください。
User:
attributes という配列と $routes という配列を受けとります
Assistant:
(Router.php の group(array $attributes, $routes) を確認 — $routes は Closure|array|string 型)
$routes は配列ではなく、Closure|array|string 型で、1つ目の引数 $attributes が「共有する属性(ミドルウェアなど)」の配列です。
この定義を踏まえて、Route::group($attributes, $routes) に「ミドルウェアなし」を指定しつつ health_check.php を読み込ませるには、$attributes に何を渡せばよいと思いますか?
User:
空の配列を渡します
Assistant:
正解です。Route::group([], base_path('routes/health_check.php')); という形になります。これでルートの登録場所は解決しました。
次は、先ほど挙げてもらったルートの内容 Route::get('/health', function () { return 200; }); について検証してみましょう。
仮説として、この closure が return 200; を実行したとき、レスポンスの HTTP ステータスコードは 200 になると思いますか? そう考える根拠も教えてください。
User:
return 200 を実行したらステータスコードは 200 でレスポンスボディが 200 で返ると思います。
Assistant:
その仮説の根拠(Laravel のレスポンス変換の仕組み)は明確ではありませんが、筋の通った予測です。ここでちゃんと検証してみましょう。
この仮説を確かめるために、どうやって検証しますか?(実際に一時的なルートを定義して、どのツールでレスポンスのステータスコードとボディを確認するか、考えてみてください)
User:
一時的にルートを作成し、Chrome で /health にアクセスします。Chrome の DevTools の Network タブで通信内容を確認します
Assistant:
良い検証方法です。ステータスコードと、レスポンスボディの内容(生の文字列として "200" が返るのか、それとも別の形なのか)の両方を Network タブで確認できます。
実際にその一時的なルートを作成して動作確認し、DevTools で観察された「ステータスコード」と「レスポンスボディの内容」を報告してください。
User:
ステータスコード 200, body も 200 という文字が返ってきました
Assistant:
予測と観察が一致しました。仮説は立証されたと言えます。return 200; は「ステータスコード200を返す」という意図ではなく、「本文が "200" という文字列で、ステータスコードは(デフォルトの)200」という意味だったわけです。
ここで設計上の判断を一つ加えます。
Azure の正常性チェックはステータスコード(2xx かどうか)だけを見ており、レスポンスボディの内容は判定に使いません。 その前提を踏まえて、return 200; という書き方は、後でこのコードを読む人にとって意図が伝わりやすいでしょうか? もっと意図が明確になる書き方があるとすれば、closure の中で何を return すべきだと思いますか?(Laravel でレスポンスを明示的に組み立てる方法を思い出してみてください)
User:
例えば、health check OK という文字列を返した方がよいと思います。
Assistant:
良い判断です。文字列としての意図の方が、読んだ人に伝わりやすくなります。
ここまでで設計判断が揃いました。整理すると:
- パスは
web/apiどちらのミドルウェアグループにも属させない(Route::group([], ...)) - レスポンス内容は意図が伝わる文字列(例: "health check OK")
- DB・キャッシュ等の依存先チェックは行わない
では実際に実装してみてください。
-
routes/health_check.phpを新規作成し、ヘルスチェック用のルートを定義する -
RouteServiceProvider::boot()に、そのファイルをミドルウェアなしで読み込む1行を追加する -
web.phpに置いた検証用の仮ルートは削除する
実装できたら教えてください。その後、「この実装によって、そもそも避けたかった問題(不要なログ出力・セッションファイルの増殖)が実際に発生しないこと」をどう確認するか、一緒に考えます。
User:
RouteServiceProvider でRoute::get('/health', function () {
return 'health check OK';
});を直接記述してもよいですか
Assistant:
技術的には、$this->routes(function () {...}) の中でどのグループにも属させずに Route::get(...) を直接書けば、ミドルウェア面では同じ効果になります。動くかどうかという点では問題ありません。
ただ、少し視点を変えて考えてみてください。
既存の web.php や api.php の内容を見ると、実際のルート定義(Route::get('/...') など)はどこに書かれていて、RouteServiceProvider は何をする役割になっていますか? その役割分担を踏まえると、ヘルスチェックのルート定義を RouteServiceProvider に直接書くことと、別ファイル(routes/health_check.php)に書いて読み込むことでは、保守性の観点でどちらが望ましいと思いますか?
User:
health check のルートは1つだけなので、ファイルが増えるよりはここに記述した方がよいと思いました。
Assistant:
その理由(ルートが1つだけなら別ファイルを増やす必要はない)は一つの合理的な判断です。一方で、少し反対の視点も持っておくとよいでしょう。
web.php や api.php を見て「このアプリのエンドポイント一覧を知りたい」と思った他の開発者は、まず routes/ ディレクトリを見るはずです。RouteServiceProvider の中に埋め込むと、そこも見ないと全エンドポイントを把握できなくなります。ルートが1本でも、既存の慣習(ルート定義は routes/*.php、RouteServiceProvider は読み込みの配線だけ)に揃えておく方が、後から見る人にとって迷いが少なくなります。
この点を踏まえて、どちらの構成にするか、あなた自身で最終的に決めてください。 どちらを選ぶか教えてもらえますか?
User:
確かに私もルートを探すときは routes 配下をまず確認します。health_check.php を作る方針で行きます。
Assistant:
良い判断です。それでは実装を進めてください。
routes/health_check.php を新規作成してルートを定義し、RouteServiceProvider::boot() の中で Route::group([], base_path('routes/health_check.php')); を追加、web.php に置いた検証用の仮ルートを削除してください。
実装できたら教えてください。その後、この実装で「不要なログが出力されないこと」「セッションファイルが増えないこと」を実際に確認する手順を一緒に考えます。
User:
実装しました。レスポンスは想定通り返ってきています。
Assistant:
実装が完成し、レスポンスが返ることも確認できましたね。ここでサイクルの最後のステップに入ります。
この修正によって、そもそも避けたかった問題——siteSecLog による不要なログ出力と、file セッションドライバによるセッションファイルの増殖——が実際に起きていないことを、どうやって確認しますか? 具体的な確認手順(どこを見るか、前後で何を比較するか)を考えて教えてください。
User:
ログファイルに /health のリクエストログが出力されていないこと、framework/sessions に新規でファイルが作られないことを確認します。
Assistant:
その確認方法で十分検証できます。
実際に /health へアクセスする前後で、storage/logs/laravel-2026-08-24.log の内容と storage/framework/sessions 配下のファイル一覧を比較してください。 結果を教えてください。
User:
Laravel のデバッグバーのリクエストログとセッションファイルが作られました。
Assistant:
これは反証です——ですが、これは失敗ではなく前進です。「グループの外に置けば影響を受けない」という予測が崩れたことで、まだ見えていない仕組みがあることが分かりました。
この結果から、新しく何が分かりましたか? そして、なぜ web ミドルウェアグループを経由していないルートなのに、セッションファイルと Debugbar のログが作られたと思いますか? 考えられる仮説を、根拠とともに立ててみてください。
(ヒント: ミドルウェアには「特定のグループに属するもの」以外に、全リクエストに常にかかるもの もあります。app/Http/Kernel.php の中で $middlewareGroups ではなく $middleware というプロパティを探してみてください)
User:
配列でいくつかのクラスが代入されています。
Assistant:
(Kernel.php の $middleware(グローバル)と $middlewareGroups、RouteServiceProvider.php の実装済みコードを確認 — グローバルミドルウェアには StartSession や siteSecLog は含まれておらず、health_check.php の登録も Route::middleware([])->group(...) で正しく空のミドルウェアになっていることを確認)
RouteServiceProvider の登録も、Kernel.php のグローバルミドルウェア($middleware 配列)も確認しました。グローバルミドルウェアには StartSession や siteSecLog は含まれていません。 つまり、web/api グループのどちらの仕組みにも起因しないはずです。
一方で、あなたは「Debugbar のリクエストログ」が出力されたことに気づいています。
この2つの事実(グローバルミドルウェアにはセッション開始処理がないこと/Debugbar のログが出ていること)を組み合わせると、セッションファイルが作られた原因について、どんな仮説が立てられますか? 根拠も含めて答えてください。
(ヒント: Debugbar はミドルウェアの仕組みとは別に、アプリケーションの起動プロセスに独自に介入する場合があります。config/app.php の providers 配列、または vendor/barryvdh/laravel-debugbar のサービスプロバイダがどう登録されているかを調べてみると手がかりになります)
User:
これは Laravel の機能の問題で、本番ではデバッグバーは表示されません。.env.develop の APP_DEBUG を FALSE にして、ログもセッションファイルもできなければ OK と考えます
Assistant:
その仮説は筋が通っています。ただ検証に進む前に一点確認しておきましょう。
Debugbar の有効/無効を実際に制御している設定は何か確認してください。 config/debugbar.php(または vendor/barryvdh/laravel-debugbar/config/debugbar.php)の 'enabled' の値がどこから来ているか(APP_DEBUG を直接見ているのか、それとも別の環境変数があってそちらが優先されるのか)を見てください。
その上で、もし APP_DEBUG=false にしても改善しなかった場合、あなたの仮説(Debugbar が原因)は反証されたことになりますが、逆に改善した場合、何が立証されたことになりますか? 予測を言葉にしてから、実際に検証してください。
User:
enabled は env ファイルの APP_DEBUG から取っています。改善された場合、APP_DEBUG が FALSE の環境であれば、/health にアクセスしたときにログやセッションファイルは出ないことになります。
Assistant:
確認と予測が明確になりました。それでは実際に検証してください。
.env.develop(または今使っている環境の .env)で APP_DEBUG を false に変更し、設定キャッシュがあればクリアした上で、再度 /health にアクセスしてください。その前後で storage/logs/laravel-*.log と storage/framework/sessions を比較し、結果を報告してください。
User:
APP_DEBUG を切り替えると、ログが出力されないことは確認できました。セッションファイルも作られていません。APP_DEBUG を true に戻すとログもセッションファイルも作られるので、仮説は間違いないです
Assistant:
仮説は立証されました。ON/OFF を切り替えて再現性まで確認できたので、検証としても十分な強度です。「Debugbar が有効なときだけログとセッションファイルが作られる」という原因が特定できました。
最後の確認です。
本番環境(Azure App Service 上で実際に動く環境)の設定では APP_DEBUG はどうなっているか確認してください。(.env.production に相当するファイル、または App Service の「アプリケーション設定」に該当する箇所を見てください)
これが false であることを確認できれば、今回実装したヘルスチェック用エンドポイントが本番環境で意図通り(不要なログ・セッションファイルを生成しない)動作することの根拠になります。
User:
AppService 側で APP_DEBUG が false であることを確認しました。
Assistant:
良い最終確認でした。これで「本番相当の設定では意図した副作用が起きない」ことまで裏付けが取れました。
最後に一つだけ確認しておきたいことがあります。エンドポイント自体は実装できましたが、Azure がこの /health を実際に正常性チェック先として使うようになるには、コード以外にどこを設定する必要がありますか?
User:
AppService の正常性チェックで/health を設定します
Assistant:
その理解で正しいです。App Service の「正常性チェック」設定(ポータルの「監視」→「正常性チェック」、または IaC で管理している場合は該当のテンプレート)でパスを /health に設定すれば、今回実装したエンドポイントが実際に使われるようになります。
今回の一連の流れを振り返ると:
- 正常性チェックの仕組み(間隔・失敗しきい値・インスタンス置換)を先に調べた
- 「何を確認すべきか」を、共有依存先を含めるとどう悪化するかまで考えて設計した
- ルーティングの仕組みを読み、
web/apiグループの影響(ログ・セッション)を避ける実装にした - 実際に検証し、予期しない副作用(Debugbar)を発見し、ON/OFF 切り替えで原因を特定した
- 本番設定で問題が起きないことまで確認した
これで実装・検証ともに完了です。あとは Azure 側の設定変更(と、必要であればレビュー依頼)を行えば完了ですね。
この記事では、
- なぜこれを作ろうと思ったのか
- チューターモードを ON にすると裏で何が起きているのか
- この拡張機能で実現できること
- こんな使い方があるよ
を紹介します。
作ろうと思った背景
「なんとなく動くもの」が作れてしまう時代
生成 AI の登場で、コーディングのハードルは劇的に下がりました。プログラミング初心者でも、難しい機能を「なんとなく」実装できてしまいます。
個人開発ならそれで構いません。しかし、業務でお客様に提供するシステムを「なんとなく」で進めると、大きなトラブルになります。障害が起きたときに原因を追えない、要件が変わったときに直せない、レビューで「なぜこう書いたの?」に答えられない──。
つまり、AI にコーディングさせる時代でも、ベースとなるプログラミングの理解とスキルは必須です。むしろ AI の出力を評価・修正できる力が、以前より重要になっています。
人は楽をしたがる
問題は、「自分で考えて書こう」と決めても、隣に AI がいるとどうしても頼ってしまうことです。使ってはいけないと言うより、使ってもいいけど答えは出てこない環境を作った方が現実的だと考えました。
「答えを書かないで」と指示するだけでは足りない
最初は CLAUDE.md に「コードを書かないでください」と書けば済むと思っていました。しかし実際に試すと、AI は求められると結局書いてしまいます。「確認のためだけ」「一部だけ」と押されると折れる。学習者側も「書かせる言い回し」をすぐ発見します。
そこで、プロンプトによる誘導だけでなく、ツール権限そのものを制限して機械的に止める必要があると分かりました。この「多層のガードレール」が、この拡張機能の設計の中核です。
なぜ VS Code 拡張機能なのか
AI チャットを自前で用意する必要はありません。学習者は自分が契約している Claude Code をそのまま使い、拡張機能はその AI エージェントの振る舞いを制御するだけにしました。導入コストが低く、研修が終わったら OFF にすれば元の環境に戻る、という使い勝手を重視しています。
インストールと使い方
- VS Code の拡張機能ビューで「Socratic Tutor」を検索してインストール
- Claude Code を使っているプロジェクトのフォルダを開く
- ステータスバー左下の「🎓 チューター: OFF」をクリック(またはコマンドパレットで「チューターモードを有効化」)
- Claude Code の新しいセッションを開始して、いつもどおり質問する
- 解除するときはもう一度クリック(「チューターモードを無効化」)
動作要件は Claude Code(CLI または VS Code 拡張機能)だけです。フックによるログ機能を使うには node コマンドが必要ですが、無くても中核のツール制限は動作します。
チューターモードを ON にすると何が起きるか
ここが一番書きたかったところです。ステータスバーをクリックした裏で、拡張機能は開いているワークスペースにClaude Code の設定ファイル一式を生成します。
<ワークスペース>/
├── .claude/
│ ├── settings.json # ① ツール権限 + ② フック登録 + ③ 出力スタイル指定
│ ├── hooks/
│ │ └── socratic-guard.js # ② フック本体(拒否理由の返却・ログ記録)
│ └── output-styles/
│ └── socratic-tutor.md # ③ ソクラテス式対話のルール
├── CLAUDE.md # ④ マーカーで囲んだ研修用ブロックを追記
└── .socratic-tutor/
└── log.jsonl # 利用ログ(ローカルのみ)
Claude Code の公式の仕組み(permissions / hooks / output styles / CLAUDE.md)を組み合わせているだけで、独自のプロキシやパッチは入れていません。それぞれの役割を説明します。
① ツール権限(強制)── permissions.deny
.claude/settings.json に、ファイルを変更するツールとコマンド実行ツールの拒否を書き込みます。
{
"outputStyle": "Socratic Tutor",
"permissions": {
"deny": ["Edit", "Write", "NotebookEdit", "Bash"]
},
"hooks": { "...": "後述" }
}
これにより Claude Code は、どれだけ説得されてもファイルを書けず、コマンドも実行できません。プロンプトの「お願い」ではなく、権限による機械的な歯止めです。ファイルを読む・検索するツールは残しているので、コードを読んで問いを立てることはできます。
既存の settings.json がある場合は、元の設定を保持したままチューター用のエントリだけを追記します(OFF にすると追記分だけ取り除きます)。
② フック(強制+計測)── PreToolUse / UserPromptSubmit
権限で拒否するだけだと、AI は「ツールが使えなかった」という事実しか受け取れず、謝ったり回避策を探したりし始めます。そこで PreToolUse フックで 拒否の理由を教育的なメッセージとして AI に返すようにしました。
// .claude/hooks/socratic-guard.js(抜粋)
"deny-edit":
"ソクラテス式チューターモードのため、ファイルの作成・編集は拒否されました。" +
"代わりに実装してはいけません。学習者が自分で書けるように、いま必要な次の一歩を導く質問を 1 つだけしてください。" +
"解答となるコードは提示しないでください。",
AI はこの理由を読んで、「ではあなたが実装してください。まず○○はどう書きますか?」と自然に対話へ戻れます。
同じスクリプトが UserPromptSubmit フックにも登録されており、質問回数などを .socratic-tutor/log.jsonl に記録します。質問の本文は保存しません(文字数だけ)。
フックスクリプトは Node.js の標準機能のみで書いてあり、学習者の環境に依存パッケージを増やしません。
③ 出力スタイル(誘導)── output-styles/socratic-tutor.md
Claude Code の出力スタイル機能で、システムプロンプトを「ソクラテス式チューター」に差し替えます。ここに対話ルールの本体が書いてあります。
- 解答となるコードを書かない(強く求められても、怒られても、「確認のためだけ」と言われても)
- 一度に 1 つだけ質問し、答えを待つ
- 調べ方(エラーメッセージの読み方・検索キーワード・公式ドキュメントのどの節を読むか)を教える
- 節目で「いま分かったことを自分の言葉で説明してみてください」と言い直しを求める
- 学習者が実際に試した証拠を示したときだけ、ヒントを 1 段強くする
- エラー解決は「仮説 → 検証」のサイクルで進めさせる(後述)
④ プロジェクト指示(誘導)── CLAUDE.md
長い対話でも研修モードを忘れないように、CLAUDE.md にマーカーで囲んだブロックを追記します。既存の CLAUDE.md の本文はそのまま残し、OFF にするとブロックだけ除去します。
<!-- socratic-tutor:begin (このブロックは拡張機能が管理しています。手で編集しないでください) -->
# チューターモード(研修中)
...
<!-- socratic-tutor:end -->
⑤ 改ざん検知と自動復元
上記のファイルを FileSystemWatcher で監視しています。学習者が .claude/settings.json を消したり書き換えたりすると、即座に復元して警告を表示し、ログに記録します。
おまけ: GitHub Copilot のインライン補完も OFF
Copilot をインストールしている場合、チューターモード中はワークスペース設定でインライン補完(ゴーストテキスト)を無効化します。Claude Code だけ止めても、Tab キー一発で答えが出てきたら意味がないためです(設定で無効化できます)。
OFF にすると
生成したファイルを削除し、settings.json と CLAUDE.md はチューター用の追記分だけを取り除いて元に戻します。Copilot 設定も元の値に復元します。利用ログ (.socratic-tutor/) だけは残ります。
この拡張機能で実現できること
1. 答えではなく「調べ方・考え方」が身につく
エラーが出たとき、AI に「直して」と言えば一瞬で直ります。でも次に同じエラーが出たら、また AI に聞くしかありません。
チューターモードでは、エラーメッセージのどこを読むか、何をキーワードに検索するか、スタックトレースをどう追うか、公式ドキュメントのどの節を見るか──を対話で導きます。身につくのは答えではなく答えに到達する手順です。
2. 「仮説 → 検証」の科学的なデバッグの型
v0.2.0 で追加した、個人的に一番気に入っている機能です。「動きません」という相談に対して、チューターは次のサイクルで進めさせます。
症状の把握 → 原因の特定 → 仮説(根拠つき)→ 検証方法 → 予測 → 検証 → 判定
↑ │
└── 反証なら: 結果を根拠に次の仮説へ ←──┴── 立証なら: 解決の実施へ
ポイントは 3 つあります。
- チューターは仮説の正誤を明かさない。 正解を知っていても言いません。判定するのは学習者自身の検証です。
- 評価するのは正しさではなく、論理の筋道。 観察した事実から筋道立てて導かれた仮説なら、誤っていても「筋の通った仮説です」と承認して検証に進めます。逆に「たぶん○○だと思う」だけの当てずっぽうには「その仮説は、どの事実から導きましたか?」と根拠を問い返します。
- 反証は失敗ではなく前進。 「この結果から新しく何が分かりましたか?」と問い、得られた事実を根拠に次の仮説へつなげます。
「とりあえず直してみる」を繰り返す初心者に、予測してから試す癖をつけるための設計です。
3. 5 段階のヒントラダー
「わからない」「教えて」を繰り返しても、ヒントは強くなりません。学習者が実際に試した証拠(コードを書き直した、質問に答えた、調べた結果を報告した)を示したときだけ、次の段階に進みます。
- 方向づけの問い(「エラーメッセージから何が読み取れますか?」)
- 概念・資料への誘導(「公式ドキュメントの○○の節を読んでみてください」)
- 場所の特定ヒント(「スタックトレースの読み方は…です。どのファイルの何行目が原因ですか?」)
- 考え方の骨格(処理の流れを日本語の箇条書きで。コードや擬似コードは書かない)
- ここまでで解決しなければ「研修講師に相談しましょう」と人間へエスカレーション
4. プロンプトではなく権限で止める、確実なガードレール
前述のとおり、「書かないで」という指示だけでは AI は折れます。この拡張機能は Claude Code のツール権限そのものを制限するので、AI がどう説得されても実装はできません。実装するのは学習者です。
5. 外部にデータを送らない
この拡張機能は AI チャット機能を持たず、外部通信も一切しません。使うのは学習者が契約している Claude Code そのままです。利用ログはワークスペース内の .socratic-tutor/ にだけ保存され、いつでも削除できます。
6. ワンクリックで元に戻せる
学習中だけ ON、終わったら OFF。既存の設定や CLAUDE.md は温存されます。「AI 禁止」ではなく「必要なときだけ制限」という運用ができます。
こんな使い方があるよ
新人研修・社内研修
もともとの動機がこれです。研修用リポジトリで受講者にチューターモードを ON にしてもらい、課題に取り組みます。
受講者は分からないことがあれば Claude Code に質問し、まるで講師が隣で支援してくれているが如く、課題を解決することができます。
自習の「筋トレモード」
業務では普通に Claude Code に実装させ、1 日 1 時間だけチューターモードで自分で書く、という使い方です。ステータスバーで切り替えられるので、ジムに行く感覚で ON/OFF できます。新しい言語やフレームワークを学ぶときに特に効きます。
新しい技術を「理解して」導入する
「ガントチャート機能を実装して」と丸投げすれば動くものは出てきますが、ライブラリの選定理由も API の使い方も頭に残りません。チューターモードだと「自作かライブラリか」「候補をどう比較するか」「公式ドキュメントのどのメソッドを読むか」「第一引数は何を受け取るか」と順に問われるので、導入したものを自分で説明できる状態になります。
デバッグ思考の練習台
わざとバグを仕込んだ演習リポジトリを用意し、チューターモードで解かせます。仮説→検証サイクルが強制されるので、「原因を言い当てる」ではなく「仮説を立て、予測し、検証で判定する」プロセスそのものを練習できます。
仕組み上の限界と注意点
正直に書いておきます。
- ガードレールであって施錠ではありません。 チューターモード自体は誰でも OFF にできます。研修などで使う場合は運用ルールとセットにしてください。
- 新しいセッションから適用されます。 実行中の Claude Code セッションには反映されないので、ON/OFF 後はセッションを再起動してください。
- 現時点で対応しているのは Claude Code のみです。GitHub Copilot(Chat / エージェント)や Codex CLI、Gemini CLI などは対応予定です。
- 出力スタイルによる誘導部分(一度に 1 問、ヒント段階など)は LLM の振る舞いなので、100% ではありません。ただし「コードを書けない・実行できない」の部分は権限で止まるので、実装の代行だけは確実に防げます。
- 学習者が別のターミナルから別設定で Claude Code を起動する、ワークスペース外で実行する、といった回避は防げません。
今後
- GitHub Copilot / その他 AI エージェントへの対応
- ヒント段階を LLM 任せではなく状態機械で管理する「試行ゲート付きヒントラダー」
など
おわりに
生成 AI は「答えを出す道具」としては既に十分すぎるほど優秀です。だからこそ、答えを出さない AI に価値があると考えて作りました。
インストールは Marketplace から、フィードバックや Issue は 【TODO: リポジトリ / 連絡先】 までお願いします。
Enjoy productive struggle! ── 悩んだ時間だけ、力になります。


