Claude Code in Action | Lesson 13「Useful Hooks!」(全文ナレッジ)
動画 11分33秒(最長レッスン)/ 講師: Steven Grider (Anthropic) / 文字起こし: mlx_whisper large-v3-turbo / 2026-07-02
日本語ナレッジ(要点整理)
このレッスンは、実プロジェクトで使いたくなる実用フック2本の紹介。どちらも Claude Code の「よくある弱点」を補うためのもの。
弱点デモ①: 編集の波及先(呼び出し元)を直さない
src/schema.ts には createSchema という関数が1つあり、main.ts から呼ばれている。
- 手動で関数定義に「呼び出し時に
verbose: boolean 引数を必須で渡す」変更を入れると、main.ts 側で即座に型エラー(argument for verbose was not provided)になる。
- 同じ変更を Claude Code に頼むと、編集自体は問題なくやる(
verbose: true を追加)が、プロジェクト内の呼び出し箇所(call site)を探して直しには行かない。main.ts を開くとエラーが残っている。
実用フック①: 編集のたびに TypeScript 型チェックを回す(PostToolUse)
tsc --noEmit でプロジェクト全体の型チェックができ、main.ts の createSchema 呼び出しのエラーがはっきり出る。
- フックの発想:TypeScript ファイルを編集するたびに型チェッカーを走らせ、エラーがあれば PostToolUse フックで即座に Claude にフィードバックする。「いま自分が入れた変更で、プロジェクトの別の場所に型エラーが出た。直しに行く必要がある」というシグナルを与える。
- 実装は
hooks/tsc.js に用意済み(型チェッカーを実行し、見つかったエラーを Claude に返すロジック一式)。デモ用に冒頭へ process.exit(0) を入れて無効化してあったので、その行を削除すると有効化される。
- 有効化して再テスト:
schema.ts を元に戻す → Claude Code を再起動 → 同じ変更を依頼 → 編集直後にフックから「別ファイルにエラーを入れた」というフィードバックが届き、Claude は「main.ts の createSchema 呼び出しを直す必要がある」と理解して、欠けている引数を追加しに行く。
- 応用:TypeScript 専用ではなく、型チェッカーを簡単に回せる型付き言語なら何でも同じ発想が使える。型なし言語でも、編集のたびにテストを実行する形で同じ機能を実装できる。
弱点デモ②: 大きなタスクの中で既存コードの再利用を忘れる
- 背景:
src/queries ディレクトリに多数のファイルがあり、各ファイルに SQL クエリを含む関数が多数ある。orderQueries.ts には e コマース DB から pending 状態の注文を取る getPendingOrders 関数が既にある。
- 焦点の絞られたタスク(「main.ts で pending の注文を出力して」)なら、Claude はクエリファイル群を見て
getPendingOrders を見つけ、新規クエリを書かずに既存関数を再利用する。これは期待どおり。
- カーブボール:
/clear でコンテキストを消した後、task.md の大きめのタスク(「一定時間 pending のままの注文を、1日1回特定チャンネルに Slack 通知する統合を書け」)として同じ要求を包んで渡すと、今度は集中を失って getOrdersPendingTooLong という新規重複クエリを書き、しかも新規ファイルまで作ってしまう(本来は orderQueries ファイルに置くべき)。
- 発動条件:Claude が Write / Edit / MultiEdit ツールで
src/queries ディレクトリ内を変更しようとした時だけ。
- 動作:フック内で 別の Claude Code インスタンスをプログラム起動(Claude Code の TypeScript SDK を使用。ターミナルで Claude Code を使うのと本質的に同じ)し、「直近の変更と queries ディレクトリの既存コードを見て、似たクエリが既にないかレビューせよ」という長いプロンプトを渡す。
- レビュー結果で重複と判定されたら exit code 2 で早期終了し、そのフィードバックを元の Claude に返す→「追加したクエリを消して既存のものを使え」と促す。これで queries フォルダから重複コードを排除できる。
- デモ:新規作成された
order_alerts_queries.ts と slack.ts を削除 → hooks/query_hook の冒頭の process.exit(0) を消して有効化 → Claude Code 再起動 → 同じタスクを再実行。Claude はまた新規クエリファイルを作ろうとするが、フックが別コピーの Claude Code を起動してリサーチし、「再利用できる既存クエリがある。既存クエリを目的に合わせて更新すればよい」と助言。主インスタンスは「既存クエリを修正しよう」と方針転換する。
- トレードオフ:queries ディレクトリを編集するたびに追加の時間とコストがかかる。その代わり重複コードが大幅に減る。導入するなら、本当に重要な少数のディレクトリだけを監視対象にして余計な処理を最小化することを推奨。
実務ポイント(このレッスンから持ち帰るもの)
- Claude は「編集そのもの」は正確でも、波及先(呼び出し元)の修正を漏らすことがある → PostToolUse で型チェックを自動実行してエラーを即フィードバックするのが特効薬。
- 型チェックフックの考え方は言語非依存:型付き言語=型チェッカー、型なし言語=テスト実行で同じ効果。
- 大きなタスクに包まれると Claude は既存コードの再利用を忘れて重複を作る(コンテキストが薄い時ほど顕著)。
- 対策としてフック内から SDK で別の Claude Code を起動し、変更をレビューさせて exit 2 で差し戻す「レビュー用フック」が組める。
- レビュー系フックは時間・コスト増と引き換え → 重要ディレクトリ数個だけに限定して適用する。
- フックの無効化/有効化は、スクリプト冒頭の
process.exit(0) の挿入/削除で簡単に切り替えられる。
本文(時系列・日本語逐語+画面差し込み)
[0:00] このビデオでは、皆さん自身のプロジェクトでぜひ使いたくなるような、本当に便利なフックをいくつかお見せしたいと思います。これらのフックは、Claude Code のよくある弱点への対処を意図したものです。1つ目のフックがどう機能するかを理解してもらうために、Claude Code が時々ぶつかる問題——特に大きめのプロジェクトで——を手短にデモしましょう。
🖥 [0:00] 画面: VS Code。ウィンドウタイトル「queries」。エクスプローラーに .claude / hooks / node_modules / scripts / src(queries・main.ts・schema.ts)/ .env / .gitignore / CLAUDE.md / package-lock.json / package.json / post-log.json / pre-log.json / task.md / tsconfig.json
[0:18] src ディレクトリの中の schema.ts を開きます。この中には createSchema という関数が1つだけあります。この関数は main.ts ファイルから——具体的にはまさにこの箇所から——呼ばれています。ではまた schema.ts に戻って、関数定義を更新します。この関数を呼び出したいなら、必ず verbose 引数も渡さなければならず、型は Boolean でなければならない、とします。
🖥 [0:30] 画面: schema.ts を手動編集。シグネチャが次の形に:
ts
export async function createSchema(db: Database, verbose: boolean) {
下には // 1. Customers table / await db.exec( / CREATE TABLE IF NOT EXISTS customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, username TEXT UNIQUE, first_name TEXT NOT NULL, last_name TEXT NOT NULL, phone TEXT, status TEXT CHECK(status IN ('active', 'inactive', 'suspended')), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP などのスキーマ定義
[0:43] この変更を入れた途端、main.ts の側に戻ると型エラーが出ます。この関数の定義を更新したのに、verbose の値を実際には追加していないからです。エラーはまさに「argument for verbose was not provided(verbose への引数が渡されていません)」と言っています。
[0:59] ではこの変更をすぐに取り消します。main.ts ファイルを閉じます。それから Claude Code を開いて、まったく同じ変更をするように頼みます。
🖥 [1:00] 画面: 右側ターミナルに Claude Code。
```
In the @src/schema.ts file update createSchema to take in a
'verbose' arg, boolean, no defaults
└ Read src/schema.ts (199 lines)
⏺ I'll update the createSchema function to accept a verbose
boolean parameter.
✳ Creating… (5s · 20 tokens · esc to interrupt)
▶▶ auto-accept edits on (shift+tab to cycle)
```
[1:10] 実行すると、Claude Code はこの編集をまったく問題なくこなします。このファイルを更新します。そして残念ながら、その変更を加えた後——ほら、そこに新しい verbose: true があります——残念ながら Claude は、プロジェクトを回ってその関数が実際に呼ばれている場所を探し、各呼び出し箇所(call site)を更新しには行きません。ここで main.ts を開くと、実際にこちらにエラーが残っているのが分かります。そして Claude はこれを捕捉できませんでした、残念ながら。
🖥 [1:30] 画面: main.ts(12行目 await createSchema(db); に赤の波線=エラー)。ターミナルには tsc と入力中。main.ts の中身:
```ts
import { open } from "sqlite";
import sqlite3 from "sqlite3";
import { createSchema } from "./sche…
async function main() {
const db = await open({
filename: "ecommerce.db",
driver: sqlite3.Database,
});
await createSchema(db);
}
main();
```
[1:35] そこで、最初にお見せするフックはこの問題を超簡単に解決してくれます。TypeScript に馴染みがない場合でも、まったく問題ありません。Claude Code を終了して tsc --noEmit コマンドを実行すると、プロジェクト全体の型チェックが走ります。そしてこの型チェックで、エラーがまさにここにはっきり見えます。あの main.ts ファイルからの createSchema 呼び出しについて文句を言っているわけです。
🖥 [2:00] 画面: ターミナルの tsc --noEmit 出力:
```
src/main.ts:12:9 - error TS2554: Expected 2 arguments, but got 1.
12 await createSchema(db);
src/schema.ts:3:50
3 export async function createSchema(db: Database, verbose: boolean) {
An argument for 'verbose' was not provided.
Found 1 error in src/main.ts:12
```
[1:59] 私のフックのアイデアはとてもシンプルです。TypeScript ファイルを編集するたびに TypeScript の型チェッカーを走らせ、明確なエラーがないか確認する。もしあれば、そのエラーを post-tool use フックの中で即座に Claude にフィードバックする。うまくいけば、これが Claude へのシグナルになり、「いま自分が持ち込んだ型エラーがあり、プロジェクト内の別のどこかを直しに行く必要がありそうだ」と伝えられます。
[2:25] さて、時間を少し節約するために、幸いこのフックはすでに hooks/tsc.js ファイルの中に組んであります。このファイルの中には、TypeScript 型チェッカーを実行し、見つかったエラーを取り出して Claude に返すロジック一式が入っています。現時点では、さっき見せたデモをするために私がこのフックを無効化してあります。まさにそこに process exit zero を足して無効化したわけです。それを削除します。これでこのフックは問題なく(a-okay)動くはずです。
🖥 [2:30] 画面: hooks/tsc.js の main() 冒頭(73行目)に無効化用の process.exit(0); が入っている:
```js
async function main() {
process.exit(0); // ← デモ用の無効化行。これを削除して有効化
const input = await readInput();
const file = input.tool_response?.filePath || input.tool_input?.file_path;
// Only check TypeScript files
if (!file || !/.(ts|tsx)$/.test(file)) {
process.exit(0);
}
const typeChecks = runTypeCheck("./tsconfig.json");
if (typeChecks) {
console.error(typeChecks);
process.exit(2);
}
}
```
📄 実物コード(queries_COMPLETED/hooks/tsc.js・全文=講義の完成状態。process.exit(0) 無効化行なし):
```js
import * as ts from "typescript";
import * as path from "path";
// Read stdin
async function readInput() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
return JSON.parse(Buffer.concat(chunks).toString());
}
function runTypeCheck(configPath) {
// Parse the tsconfig.json file
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
if (configFile.error) {
console.error(
ts.formatDiagnostic(configFile.error, {
getCanonicalFileName: (x) => x,
getCurrentDirectory: ts.sys.getCurrentDirectory,
getNewLine: () => ts.sys.newLine,
})
);
return;
}
// Parse the configuration
const parseConfigHost = {
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
readDirectory: ts.sys.readDirectory,
getCurrentDirectory: ts.sys.getCurrentDirectory,
onUnRecoverableConfigFileDiagnostic: () => {},
};
const parsed = ts.parseJsonConfigFileContent(
configFile.config,
parseConfigHost,
path.dirname(configPath)
);
// Override to ensure no emit
const compilerOptions = {
...parsed.options,
noEmit: true,
};
// Create the program
const program = ts.createProgram(parsed.fileNames, compilerOptions);
// Get all diagnostics
const allDiagnostics = ts.getPreEmitDiagnostics(program);
// Format and display diagnostics
if (allDiagnostics.length > 0) {
const formatHost = {
getCanonicalFileName: (path) => path,
getCurrentDirectory: ts.sys.getCurrentDirectory,
getNewLine: () => ts.sys.newLine,
};
const formattedDiagnostics = ts.formatDiagnostics(
allDiagnostics,
formatHost
);
return formattedDiagnostics; // Type check failed
}
return null; // Type check passed
}
async function main() {
const input = await readInput();
const file = input.tool_response?.filePath || input.tool_input?.file_path;
// Only check TypeScript files
if (!file || !/.(ts|tsx)$/.test(file)) {
process.exit(0);
}
const typeChecks = runTypeCheck("./tsconfig.json");
if (typeChecks) {
console.error(typeChecks);
process.exit(2);
}
}
main();
```
📄 実物コード(queries_COMPLETED/.claude/settings.example.json より・このフックの配線部分。tsc.js は PostToolUse の Write|Edit|MultiEdit に接続):
json
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_response.filePath // .tool_input.file_path // empty' | xargs -I {} npx --yes prettier --write {} 2>/dev/null || true"
},
{
"type": "command",
"command": "node $PWD/hooks/tsc.js"
}
]
}
]
[2:52] それでは schema.ts ファイルに戻って、あの verbose フラグを外し、Claude Code を再起動して、もう一度同じ変更をするよう頼みます。Claude は変更を行います。そして今度は、直後に TypeScript 型チェッカーからのフィードバック——「おい、プロジェクトの別の場所に、いま自分が持ち込んだエラーがあるぞ」——を即座に受け取るはずです。そして Claude が直しに行ってくれるはずです。
[3:16] ここを見てください。行われた編集がそこにあります。そして、われわれが組んだフックからの「edit operation feedback(編集操作フィードバック)」を受け取りました。つまり別のファイルの中に問題を見つけたのです。Claude はいま「わかった、自分がエラーを持ち込んだと理解した。main.ts の中の createSchema 呼び出しを直す必要がある」と言っています。そして次に行う更新で、そのファイルに入って関数呼び出しを更新し、欠けている引数を追加しようとします。
🖥 [3:00] 画面: 左=schema.ts、右=Claude Code。編集 diff とフックのフィードバックが見える:
``
3 - export async function createSchema(db: Database) {
3 + export async function createSchema(db: Database, verbose: boolean) {
4 // 1. Customers table
5 await db.exec(
6 CREATE TABLE IF NOT EXISTS customers (
Edit operation feedback:
- [node /Users/sgrider/code/enablements/claude-code-course/prod/queries/hooks/tsc.js]: src/main.ts(12,9):
error TS2554: Expected 2 arguments, but got 1.
└ Found 1 new diagnostic issue in 1 file (ctrl-r to expand)
✳ Elucidating… (10s · ↑ 82 tokens · esc to interrupt)
```
🖥 [3:30] 画面: Claude が波及先を自走修正:
⏺ I need to fix the call to createSchema in main.ts and also use
the verbose parameter in the createSchema function.
⏺ Read(src/main.ts)
└ Read 16 lines (ctrl+r to expand)
⏺ Now I'll update the call in main.ts to include the verbose argument:
⏺ Update(src/main.ts)
└ Updated src/main.ts with 1 addition and 1 removal
12 - await createSchema(db);
12 + await createSchema(db, false);
⏺ Now I need to implement the verbose functionality in the
createSchema function. I'll add console logs when verbose is true:
⏺ Update(src/schema.ts)
(この後 schema.ts 側には if (verbose) console.log("Creating customers table..."); のような verbose 実装も追加される=[4:06] 頃の画面で確認できる)
[3:42] これは、皆さん自身の個人プロジェクトで実装を試してみるとよいフックです。このフックは TypeScript 専用に実装しましたが、型チェッカーを簡単に走らせられる型付き言語なら、他のどんな言語でも同じように機能します。型なし言語を使っている場合でも、型チェッカーを走らせる代わりにテストを使って、同じ発想の機能を実装できます。つまり編集が行われるたびにテストを走らせ、その編集が問題ないことを確かめればよいのです。
[4:05] さて、次にお見せしたいフックは説明が少し難しいのですが、いったん背後のアイデアを掴めば、特に大きなプロジェクトでは間違いなく本当に役立つと感じてもらえるはずです。このもう1つのフックを理解してもらうために、このプロジェクトの背景を少し説明したいと思います。src/queries ディレクトリの中には、たくさんの異なるファイルがあります。これらのファイルにはそれぞれ、いろいろな関数の中に書かれた多数の SQL クエリが入っています。
[4:29] 特に order_queries.ts ファイルの中に、getPendingOrders という関数があることを指摘しておきたいと思います。このクエリは、e コマース関連のデータが入ったデータベースを見に行きます。そして理屈の上では、作成済みで pending 状態にある注文をすべて見つけます。この関数をしばらく頭に置いておいてください。
🖥 [4:30] 画面: src/queries/order_queries.ts(110行目付近)。queries フォルダには analytics_queries.ts / customer_queries.ts / inventory_queries.ts / order_queries.ts / product_queries.ts / promotion_queries.ts / review_queries.ts / shipping_queries.ts:
``ts
export async function getPendingOrders(db: Database): Promise<any[]> {
const query =
SELECT
o.order_id,
o.order_date,
o.total_amount,
c.first_name || ' ' || c.last_name as customer_name,
c.phone,
julianday('now') - julianday(o.order_date) as days_since_create…
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.status = 'pending'
ORDER BY o.order_date
`;
const rows = await db.all(query, []);
return rows;
}
```
[4:49] それでは、大きなプロジェクトの中で起こり始めるよくある問題を理解してもらうために、図をいくつか手短にお見せします。この図では、左側にクエリファイルの一覧があります。見てきたとおり、それぞれのクエリファイルには多くの異なるクエリが入っています。特にあの order_queries ファイルの中には getPendingOrders 関数があります。つまり、pending の注文を見つけようとするクエリは、すでに用意されているわけです。
🖥 [5:00] 画面: 図解スライド(ANTHROPIC)。左に ./queries ボックス(analytics_queries.ts〜review_queries.ts の一覧。order_queries.ts / getPendingOrders() がハイライト)。右の吹き出し:
Hi Claude! Can you update main.ts to print out orders
that have been pending longer than 3 days?
吹き出しから order_queries.ts へ矢印。
🖥 [5:04] 画面: 同じ図の変形。右側の黒いパネルに getPendingOrders の実装コード(上記 SQL と同じもの)が表示され、order_queries.ts ボックスと矢印で結ばれる=「既存クエリを見つけて使ってほしい」の視覚化
[5:12] ここで Claude のところへ行って「main.ts を更新して、3日より長く pending 状態のままになっている注文をすべて出力して」と頼んだら、完璧な世界では、Claude は order_queries.ts ファイルを見つけ、あの既存のクエリを見つけて、まったく新しいクエリを書き出すのではなく、それを利用するはずです。それがわれわれの望みです。
[5:34] そして、実際にいま Claude を使ってまさにそれをやるよう頼めば、まさに望みどおりの結果が得られます。Claude に「main.ts で、pending になっている注文を出力して」と頼みます。
🖥 [5:30] 画面: Claude Code のプロンプト入力(右下に「In order_queries.ts」バッジ):
```
In main.ts print out orders that have been pending
longer than 3 days
```
[5:46] Claude の名誉のために言うと、Claude は存在するいろいろなクエリファイルを見に行きます。order_queries ファイルを見つけます。そしてその中に、getPendingOrders というクエリがすでにあることを認識します。そして新しいクエリを作るのではなく、その関数を使おうとします。われわれは新しいクエリが欲しかったのではなく、Claude に既存の関数を使ってほしかったのです。つまり、非常にフォーカスが利いた指示の明確なタスクを与えたとき、Claude は「うん、新しいクエリを書くべきではなさそうだ。少なくともすでに存在するものをいくつか見てみるべきだ」と理解できたわけです。それは間違いなく良いことでした。
🖥 [6:00] 画面: Claude の実行ログ:
what query functions are available for orders.
⏺ Read(src/main.ts)
└ Read 16 lines (ctrl+r to expand)
⏺ Now let me check what order query functions are available
in the order_queries.ts file.
⏺ Read(src/queries/order_queries.ts)
└ Read 251 lines (ctrl+r to expand)
⏺ Perfect! I can see there's already a getPendingOrders
function in order_queries.ts that includes a calculated
field days_since_created. I'll now modify main.ts to
use this function and filter for orders pending longer
than 3 days.
· Philosophising… (23s · ⚒ 261 tokens · esc to interrupt)
[6:17] ここで Claude にちょっとしたカーブボールを投げます。わざとこのタスクを少し難しくするのです。まず slash clear(/clear)を実行して、これまでに得たコンテキストをすべて消します。それから task.md ファイルを見てほしいのです。このファイルの中に、やはり「しばらく pending のままになっている注文を見つけて」と Claude に頼むプロンプトを用意したのですが、それをより大きなプロジェクトで包んであります。Slack 統合を書き、pending が長すぎる注文をすべて、1日1回、特定のチャンネルへメッセージする、というものを Claude に頼んでいます。
🖥 [6:30] 画面: task.md の中身(「orders that have been pending too long」の部分が選択ハイライト)
📄 実物(queries_COMPLETED/task.md・全文):
@main.ts is executed automatically once per day as a cron job. Add in a new slack integration in a separate file. Then whenever this thing runs, check for orders that have been pending too long (more than 3 days) and send an alert to the #order-alerts channel with the customer name and phone number so someone can follow up. @schema.ts contains the current db structure. Remember, all queries should be placed in the ./queries dir. Start with the slack integration.
[6:51] このシナリオでも、pending が長すぎる注文を見つけたいことに変わりはありませんが、今回はそれをこの大きなタスクの中に包みました。そしてこのタスクを、あの slash clear 操作をやった後で Claude に食わせると、今度は残念ながら、さっきほどフォーカスを保ってくれず、結局まったく新しい get pending orders クエリを書き出そうとすることになります。繰り返しますが、これは望んでいないことです。プロジェクト中にコードを重複させることになるからです。
[7:15] しばらく実行させておくと、やがて、実際に getOrdersPendingTooLong という真新しいクエリを作るのが確認できます。これは、Claude がいわばフォーカスを失い、既存のクエリを再利用するのではなく真新しいクエリを書くと決めてしまった例です。改めて、ここには重複コードができています。おそらく望むところではありません。加えて、新しいクエリを作っただけでなく、真新しいファイルまで作りました。これもおそらく望んでいないことです。この注文関連のクエリは order_queries ファイルに追加されてほしいはずです。
🖥 [7:00] 画面: 大タスク実行中の Claude:
⏺ Update Todos
└ ☐ Create Slack integration module (slack.ts)
☐ Create query to find orders pending for more than 3 days
☐ Integrate Slack alerts into main.ts cron job
⏺ Let me first check the schema to understand the orders structure:
⏺ Read(src/schema.ts)
└ Read 100 lines (ctrl+r to expand)
⏺ Read(src/schema.ts)
└ Read 100 lines (ctrl+r to expand)
⏺ Now I'll create the Slack integration module:
✳ Envisioning… (54s · ⚒ 710 tokens · esc to interrupt)
🖥 [7:30] 画面: エクスプローラーに新規ファイル src/queries/order_alerts_queries.ts(U)と src/slack.ts(U)が出現。ターミナルに書かれた重複クエリ:
ts
import { Database } from "sqlite";
export function getOrdersPendingTooLong(db: Database,
days: number = 3): Promise<any[]> {
const query = `
SELECT
o.order_number,
o.created_at,
c.first_name || ' ' || c.last_name as customer_name,
c.phone as customer_phone,
CAST((julianday('now') -
julianday(o.created_at)) AS INTEGER) as days_pending
… +14 lines (ctrl+r to expand)
└ Interrupted by user
[7:45] さて、問題を理解したところで、フックを利用してこれをどう直せる可能性があるかをお見せしましょう。いいですか、Claude が Write、Edit、あるいは MultiEdit ツールを使って、特に queries ディレクトリの中の何かを修正しようとしたときは必ず、次のフックを実行します。まず、このフックの中で、真新しい別コピーの Claude Code を起動します。この新しいコピーに、いま行われた変更を見て、queries ディレクトリ内の既存コードもいくつか見て、似たクエリがすでにその中にないか確認するよう頼みます。それで既存のクエリがあったなら、そのフィードバックを取って元のコピーの Claude に送り返し、Claude に状況の修正を判断してもらうよう頼みます。つまり、追加したクエリを取り除いて、すでに存在するものを使う、ということです。こうすることで、queries フォルダが総じてクリーンに保たれ、中が重複コードだらけにならないようにできるわけです。
🖥 [8:00] 画面: 図解スライド(ANTHROPIC):
Claude uses Write, Edit, or MultiEdit to
modify a file in the ./queries dir
↓
┌─────────── PreToolUse Hook ───────────┐
│ Programmatically launch a separate copy of │
│ Claude Code │
│ Ask it to research the queries dir and see │
│ if a similar query already exists │
│ If so, provide this feedback to Claude, │
│ giving it the opportunity to fix the │
│ situation │
└────────────────────────────────────────┘
※ナレーションは「write/edit/multi-edit しようとした時」=図・実配線とも PreToolUse(settings.example.json でも query_hook.js は PreToolUse の Write|Edit|MultiEdit に接続。ツール実行前に割り込んで書き込みを exit 2 でブロックする設計)
[8:35] では、これが実際に動くところをお見せしましょう。まずこちらに戻ります。さっき作られた真新しい order_alerts_queries.ts ファイルと、同じく作られた slack.ts ファイルを削除します。
🖥 [8:30] 画面: VS Code の削除確認ダイアログ:
Are you sure you want to delete 'order_alerts_queries.ts'?
You can restore this file from the Trash.
[Move to Trash] [Cancel] ☐ Do not ask me again
[8:53] 次に hooks ディレクトリの中の query hook ファイルを見つけます。このフックもすでに用意しておきました。いまは先頭に process.exit があるので無効化された状態です。このフックを手短に見ていきましょう。まず、src/queries ディレクトリへの変更だけをレビューするように指定します。それから少し下で、いま行われた変更が queries ディレクトリに対するものだったかをチェックします。その後に長いプロンプトがあり、いま行われた変更についてレビューを行うよう Claude に頼んでいます。そしてその後が、Claude Code をプログラムから起動している箇所——具体的にはまさにこの数行です。これは Claude Code の TypeScript SDK を利用しています。これについてはもう少し後でたっぷり説明しますが、今のところは、これがターミナルで Claude Code を使うのと本質的に同じものだと理解しておいてください。
🖥 [9:00] 画面: hooks/query_hook.js(23〜39行目、パス正規化〜レビュープロンプト冒頭):
```js
// Normalize paths for comparison
const normalizedFilePath = path.resolve(filePath);
const queriesDir = path.resolve(process.cwd(), REVIEW_DIR);
// Check if file is within queries directory (handles subdirectories too)
if (!normalizedFilePath.startsWith(queriesDir + path.sep)) {
process.exit(0);
}
// Prepare prompt for analysis
const newContent = toolInput.content || toolInput.contents;
const prompt = `You are reviewing a proposed change to a database query fi…
Your task is to analyze if the new or modified query functions could be
accomplished by reusing or slightly modifying existing query functions.
Within reason, we want to prevent duplicate queries from being added into th…
so you are seeing if the proposed change will duplicate any existing functio…
``
※動画内のファイル冒頭はimport { query } from "@anthropic-ai/claude-code";(旧パッケージ名)。現行 assets は改名後の@anthropic-ai/claude-agent-sdk` を import している
[9:41] Claude Code が実行されて応答が返ってきたら、Claude が「うん、変更は問題なさそうだ」と判断したのか、それとも重複クエリがあるのかをチェックします。そして重複がある場合は、exit code 2 で早期終了します。これによってこのフィードバックが Claude に返され、うまくいけば「変更が必要だ」と伝わるはずです。
🖥 [9:30] 画面: hooks/query_hook.js(66〜84行目):
```js
const resultMessage = messages.find((m) => m.type === "result");
if (!resultMessage || resultMessage.subtype !== "success") {
process.exit(0);
}
// If changes are appropriate, allow them
if (resultMessage.result.includes("Changes look appropriate")) {
process.exit(0);
}
// Otherwise, block with feedback
console.error(Query duplication detected:\n\n${resultMessage.result});
process.exit(2);
main().catch((err) => {
console.error(Hook error: ${err.message});
process.exit(1);
});
```
📄 実物コード(queries_COMPLETED/hooks/query_hook.js・全文。※assets 版は配布時の安全のため main() 冒頭に process.exit(0); の無効化行が残った状態=講義でいう「削除前」。有効化するにはこの1行を消す):
```js
import { query } from "@anthropic-ai/claude-agent-sdk";
import path from "path";
const REVIEW_DIR = "src/queries";
async function main() {
process.exit(0);
// Read JSON input from stdin
const input = await new Promise((resolve) => {
let data = "";
process.stdin.on("data", (chunk) => (data += chunk));
process.stdin.on("end", () => resolve(data));
});
const hookData = JSON.parse(input);
const toolInput = hookData.tool_input;
// Check if this is a file modification in ./queries
const filePath = toolInput.file_path || toolInput.path;
if (!filePath) {
process.exit(0);
}
// Normalize paths for comparison
const normalizedFilePath = path.resolve(filePath);
const queriesDir = path.resolve(process.cwd(), REVIEW_DIR);
// Check if file is within queries directory (handles subdirectories too)
if (!normalizedFilePath.startsWith(queriesDir + path.sep)) {
process.exit(0);
}
// Prepare prompt for analysis
const newContent =
toolInput.content || toolInput.contents || toolInput.new_string;
const prompt = `You are reviewing a proposed change to a database query file.
Your task is to analyze if the new or modified query functions could be
accomplished by reusing or slightly modifying existing query functions.
Within reason, we want to prevent duplicate queries from being added into this project,
so you are seeing if the proposed change will duplicate any existing functionality.
File: ${filePath}
New content:
${newContent}
Please research and analyze the existing queries in the ./queries directory and:
1. Identify any new query functions being added in this change
2. For each new query function, determine if it could be accomplished by:
- Using an existing query function as-is
- Slightly modifying an existing query function, perhaps by adding additional
arguments or expanding a select statement
If yes, provide specific feedback on which existing functions could be used instead. Be concise and specific.
If no, just say "Changes look appropriate."`;
const messages = [];
for await (const message of query({
prompt,
abortController: new AbortController(),
})) {
messages.push(message);
}
// Extract the analysis result
const resultMessage = messages.find((m) => m.type === "result");
if (!resultMessage || resultMessage.subtype !== "success") {
process.exit(0);
}
// If changes are appropriate, allow them
if (resultMessage.result.includes("Changes look appropriate")) {
process.exit(0);
}
// Otherwise, block with feedback
console.error(Query duplication detected:\n\n${resultMessage.result});
process.exit(2);
}
main().catch((err) => {
console.error(Hook error: ${err.message});
process.exit(1);
});
```
📄 実物コード(queries_COMPLETED/.claude/settings.example.json・query_hook の配線部分=PreToolUse):
json
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node $PWD/hooks/query_hook.js",
"timeout": 300
}
]
}
]
(timeout 300 秒=別 Claude Code の起動・リサーチに時間がかかるため長めに確保)
[9:56] さて、この追加フックを組み上げて、先頭のあの process exit zero を取り除いて有効化したので、もう一度 Claude Code を再起動し、それから同じクエリ(タスク)をもう一度実行します。今度は、最初はあの重複クエリを入れようとするかもしれませんが、そこでまさにこのフックが走って、うまくいけばこう伝えてくれるはずです。「おい、その重複コードは要らない。この機能を実装するには、すでに存在するどれかのクエリを使うべきだ」と。
🖥 [10:00] 画面: Claude Code を再起動して同じタスクを投入(右下に「In query_hook.js」バッジ):
```
/help for help, /status for your current setup
cwd: /Users/sgrider/code/enablements/claude-code-course/prod/queries
※ Tip: Send messages to Claude while it works to steer Claude in real-time
@main.ts is executed automatically once per day as a cron job.
Add in a new slack integration in a separate file. Then whenever
this thing runs, check for orders that have been pending too
long (more than 3 days) and send an alert to the
#order-alerts channel with the customer name and phone number
so someone can follow up. @schema.ts contains the current db
structure. Remember, all queries should be placed in the
./queries dir. Start with the slack integration.
```
[10:24] Claude Code はまたしても、すでに存在していた古いクエリを利用せずに、真新しい、完全に別個のクエリファイルを作ろうとします。しかし、そのファイルを作ろうとしたとき、われわれのフックが実行されます。これがあの別コピーの Claude Code を起動し、そのコピーがいくらかリサーチを行って、実際に再利用できる既存のクエリがあることを見つけます。そしてアドバイスを返してくれます。「なあ、この別の既存クエリを更新しに行けば、君の目的にぴったり合うはずだよ」と。そして、われわれが対話している主インスタンスの Claude からのフィードバックが見えます。「ああ、そうだ、この既存クエリがある。真新しいものを書き出そうとするのではなく、その既存クエリを修正することにしよう」と。
🖥 [10:30] 画面: 2回目の実行でフックが発動する瞬間。Slack 統合(1件目の Todo)が完了し、重複クエリファイルを Write しようとしたところで PreToolUse フックが実行中:
```
export class SlackIntegration {
… +53 lines (ctrl+r to expand)
⏺ Now let me mark the first task as completed and
move to creating the query for pending orders.
⏺ Update Todos
└ ☒ ~~Create Slack integration module~~(完了・取り消し線)
☐ Create query to find orders pending more than 3 days
☐ Integrate Slack alerts into main.ts cron job
☐ Test the integration
⏺ Write(src/queries/order_alert_queries.ts)
└ Running hook PreToolUse:Write...
✳ Creating… (145s · ⚒ 1.3k tokens · esc to interrupt)
``
(「**Running hook PreToolUse:Write...**」の表示=query_hook が別コピーの Claude Code を起動してレビュー中。経過 145 秒はそのリサーチに要している時間。エクスプローラーにはsrc/slack.ts(U)が既に新規作成済み。なお2回目の Write 先ファイル名はorder_alert_queries.tsで、1回目に作られたorder_alerts_queries.ts` とは微妙に異なる)
🖥 [10:38] 画面: フックに差し戻されて方針転換する主インスタンス(エクスプローラーには slack.ts(U)のみ新規=queries 外なのでフック対象外。order_alerts_queries.ts は再作成されていない):
``
- Calculates days since creation usingjulianday('now') - julianday(o.order_date)`
… +1 line (ctrl+r to see all)
⏺ Let me check the existing order queries to see
the getPendingOrders function.
⏺ Read(src/queries/order_queries.ts)
└ Read 251 lines (ctrl+r to expand)
⏺ I see the existing getPendingOrders function.
Let me modify it to accept an optional parameter
for filtering by days as the hook suggested.
⏺ Update(src/queries/order_queries.ts)
└ Interrupted by user
```
(=新規重複ファイルではなく order_queries.ts の既存 getPendingOrders に日数パラメータを足す方向へ転換。最後の Interrupted by user は講師が収録上ここで打ち切ったもの)
[11:02] さて、このフックの欠点は、queries ディレクトリの中の何かを編集したいときに毎回、追加の時間と費用がかかって実行される、という点です。一方の利点は、最終的に queries ディレクトリ内の重複コードがずっと少なくなることです。つまり結局のところ、自分のプロジェクトでこういうものを実装したいかどうかは、皆さんが判断すべきトレードオフの組み合わせに帰着します。もし導入するなら、少なくとも query hook の中でお見せしたこと——つまりまさにこれです——をやることをお勧めします。監視するのはプロジェクト内の本当に重要なフォルダのような、せいぜい一握りのディレクトリだけにして、行われる余計な仕事の量を最小化するのです。
🖥 [11:24] 画面: hooks/query_hook.js 冒頭(監視対象を絞る要の1行 const REVIEW_DIR = "src/queries"; を含む):
```js
import { query } from "@anthropic-ai/claude-code";
import path from "path";
const REVIEW_DIR = "src/queries";
async function main() {
// Read JSON input from stdin
const input = await new Promise((resolve) => {
let data = "";
process.stdin.on("data", (chunk) => (data += chunk));
process.stdin.on("end", () => resolve(data));
});
const hookData = JSON.parse(input);
const toolInput = hookData.tool_input;
// Check if this is a file modification in ./queries
const filePath = toolInput.file_path || toolInput.path;
if (!filePath) {
process.exit(0);
```
英語逐語(ASR全文)
In this video, I'd like to show you some really useful hooks that you might want to use on your own projects. These hooks are intended to address some common weak points in CloudCode. To help you understand how the first one works, let me give you a quick demonstration of a problem that CloudCode sometimes runs into, especially on larger projects. So inside the src directory, I'm going to find schema.ts. Inside of here, there's just one single function called create schema. This function is called from the main dot TS file specifically right here. Now I'm going to go back to the schema dot TS file, and I'm going to update the function definition. I'm going to say that if you ever want to call this function, you must also pass in a verbose argument that must be of type Boolean. Now, as soon as I add in this change, if I go backwards, the main dot TS file, I'm going to get a type error because I just updated the definition of this function, but I have not actually added in a value for verbose. So the error right here says specifically argument for verbose was not provided.
Now I'm going to undo that change very quickly. I'm going to close the main.ts file. I'm then going to open up Claude code and ask it to make the exact same change. Now, if I run this, Claude code is going to have absolutely no issue making this edit whatsoever. But it's going to update this file. And then unfortunately, after making that change, so there's the new verbose true right there. Unfortunately, Claude won't go around the project and try to find where that function is actually called and try to update any of the different call sites. So if I now open up main.ts, we'll see that we do in fact have an error over here. And Claude didn't really catch this, unfortunately.
So the first hook that I want to show you will fix this solution super easily. In case you're not familiar with TypeScript, if you're not, that's totally fine. If I close out a Claude code and run the command tsc-noemit, that's going to run a type check on my entire project. And in this type check, we can see that the error is very evident right here. So it's complaining about our call to create schema from that main.ts file. So my idea for a hook is really simple. I think that any time that we edit a TypeScript file, we should run the TypeScript type checker and see if there are any distinct errors. If there are, we should attempt to feed these errors back into Claude immediately inside of a post-tool use hook. And hopefully this will give Claude a signal and tell it that there is a type error that it just introduced that it probably needs to go and fix somewhere else inside of our project.
Now, I already put this hook together for us, fortunately, just to save us a little bit of time inside the hooks tsc.js file. So inside this file, I've got a bunch of logic put together to run TypeScript type checker, take any errors that it found, and pass them back into clod. At present, I disabled this hook just so I can give you that demonstration you just saw. So I disabled it by adding the process exit zero right there. I'm going to delete that, and now this hook should be working a-okay. So if I now go back to the schema.ts file, remove that verbose flag, restart Claude code, and ask it to make the same change once again, it will make the change. And then hopefully this time it will immediately get that feedback from the TypeScript type checker saying, Hey, you've got an error somewhere else in the project that you've just introduced. And hopefully Claude will go and fix it. So we can see right here, there's the edit that was made. We got some edit operation feedback from the hook that we put together. So it found an issue inside of one of our different files. And Claude is now saying, okay, I understand I introduced an error. I need to fix the call to create schema inside of main.ts. And then the next update it makes is going to attempt to go into that file and update that function call to add in that missing argument. So this is a hook that you might want to try implementing on your own personal projects. Now, even though this hook was implemented specifically for TypeScript, it still works for any other kind of typed language where you can run a type checker very easily. Even if you're using an untyped language, you might even implement the same idea of functionality using tests instead of running a type checker. So every time an edit is made, you could run your tests to make sure that the edit is okay.
Now, the next hook that I would like to show you is a little bit more challenging to explain, but once you get the idea behind it, I think that you will definitely find this next one really helpful, particularly in larger projects. To help you understand this other hook, I want to give you a little bit of background on this project. Inside of the SRC queries directory, there are many different files. Each of these different files contains many different SQL queries written inside of different functions. Inside of the order queries.ts file in particular, I want to point out that there is a function inside of here called get pending orders. This query goes through a database that contains some e-commerce related data. And in theory, it's going to find all the different orders that have been created that are in a pending state. So just keep that function in mind for a moment.
Okay, so I'm going to show you a couple of diagrams really quickly to help you understand a common problem that starts to arise inside of larger projects. So in this diagram, I've got my list of different query files on the left hand side. And as we saw, each of those different query files contains many different queries. Inside of that order queries file in particular is the get pending orders function. So we've already got a query put together that will attempt to find some different pending orders. Now, if I go to Claude and ask it to update the main.ts file to print out all the different orders that have been in a pending state for longer than three days, in a perfect world, Claude would find the order queries.ts file, it would find that existing query, and it would make use of it, as opposed to writing out a brand new query. So that's what we want. And we'll see that if we make use of Claude right now and ask it to do exactly that, we're going to get exactly the result we want. So I'm going to ask Claude in the main.ts file, print out orders that have been pending. Now to Claude's credit, it is going to take a look at the different query files that exist. It's going to find the order queries file. And then inside there, it's going to recognize that there is already a query called get pending orders. And it's going to attempt to use that function as opposed to creating a new query. We didn't want a new query. we wanted Claude to use the existing function. So when we gave Claude a very focused and directed task, it was able to understand that, yeah, it probably shouldn't write a new query. It should at least take a look at some of the ones that already exist. And that was definitely good.
Now I'm going to give Claude a little bit of a curveball. I'm going to purposefully make this task a little bit more difficult. First, I'm going to run slash clear to clear out all the context that we've gained. Then I'd like you to take a look at the task.md file. Inside this file, I put together a prompt that is still going to ask Claude to find orders that have been pending for a while, but I've also wrapped it up in some larger project. I'm asking Claude to write out a Slack integration that's going to message a specific channel once a day with all the different orders that have been pending for too long. So in this scenario, we still want to find orders have been pending for too long, but now I've wrapped it inside this larger task. And if I take this task and then feed it into Claude, again, after doing that slash clear operation, we're going to see that this time around, unfortunately, it's not going to stay quite as focused, and it's going to end up trying to write out a brand new get pending orders query, which is, again, not what we want, because that would be duplicating code throughout our project. If I let this run for a bit, I will eventually see that, yes, it does in fact make a brand new query called get orders pending too long. So this is an example of where Claude kind of lost focus and decided to write a brand new query as opposed to reusing an existing one. Again, we've got some duplicate code here, which is probably not what we want. In addition, it didn't only create the new query, it also created a brand new file, which is also probably something we don't want. We'd probably want this order-related query to be added to the order queries file.
So now that we understand the issue here, let me show you how we could fix this potentially by making use of a hook. All right, so whenever Claude attempts to write, edit, or use the multi-edit tool to modify something inside the queries directory specifically, I'm going to run the following hook. First, inside this hook, I'm going to launch a brand new separate copy of Claude code. I'm going to ask this new copy to take a look at the change that was just made and take a look at some of the existing code inside the queries directory and see if a similar query is already inside there. Then if there is an existing query, then I'm going to take that feedback and send it back to the original copy of Claude, and I'm going to ask Claude to maybe decide to fix the situation. So remove the added query and make use of the one that already exists. So this is going to allow us to make sure that the queries folder generally stays clean and doesn't have a bunch of duplicate code inside of it.
So let me show you how this would work in action. First, I'm going to flip back over here. I'm going to delete the brand new order alerts queries.ts file that was made and the slack.ts file that was made as well. Then I'm going to find inside the hooks directory, the query hook file. So I already put this hook together for us. Right now it is currently disabled because I got a process.exit at the very top. So let's walk through this hook really quickly. First, I'm going to tell this thing that it's only going to review changes to the src queries directory. Then a little bit lower, I'm going to check and see if the change that was just made was made to the queries directory. After that, I've then got a long prompt here, that is asking Claude to do a review on the change that was just made. And then after that is where I'm launching Claude code programmatically, specifically these lines right here. This is making use of the Claude code TypeScript SDK. I can give you a lot more information on it in just a little bit. For right now, just understand that this right here is essentially the same as us making use of Claude code at the terminal. Once Claude code runs and I get a response back out of it, I check and see if Claude decides that, yeah, the changes look okay, or maybe we've got a duplicate query. And if we do, then we're going to exit early with an exit code of two, which is going to give this feedback back to Claude and hopefully tell it that it needs to make a change.
So now that I've got this additional hook put together and enabled by removing that process exit zero at the top, I'm going to again restart Claude code and then run the same query again. And hopefully this time it might initially put in that duplicate query, but then our hook right here is going to run and hopefully tell it, Hey, we don't want that duplicate code. You should make use of some already existing query to implement this functionality. Now, CloudCode is once again going to attempt to create a brand new, completely separate query file, not making use of the old query that already existed. When it tries to create that file, however, our hook is going to run. This is going to launch that separate copy of CloudCode, which is going to do some research and find that there is in fact an existing query that can be reused. It's going to provide some advice and say, hey, you could probably go and update this other existing query to suit your purposes perfectly. And we'll see some feedback from Claude, our primary instance that we are interacting with saying, ah, yes, there is this existing query. Let's just modify that existing query rather than attempting to write out a brand new one.
Now, the downside to this hook is that it's going to take some additional time and expense to run every single time that I want to edit something inside the queries directory. But the upside is that I'm going to end up with a lot less duplicate code inside my queries directory. So it really comes down to a set of trade-offs for you deciding whether or not you want to implement something like this in your own project. If you do, I would at least recommend doing what I showed you inside of the query hook. So this one right here and only watching maybe a handful of directories like really important folders inside your project just to minimize the amount of extra work that is being done.