はじめに
顧客案件でClaude Codeに作業を任せるとき、「こうしてほしい」は書いても、「これはしないでほしい」は書き忘れがちです。そして、書いたつもりでも、守られたかどうかを確かめる手段がないと、次の案件で同じ場所が抜けます。
この記事では、CLAUDE.md(Claude Codeがセッション開始時に読み込む、プロジェクト用の指示ファイル)に「やらないこと」を書くときの考え方と、書くだけでは足りないものを hook と事後検査に分ける方法をまとめます。例は、この記事のために作った架空のプロジェクトのものです。実際の案件のルールは載せていません。
環境
- macOS 26.6.2、Node.js v22.22.3、git 2.54.0、jq
- Claude Code 2.1.283(2026-09-27に確認)
hook や CLAUDE.md の挙動は、バージョンによって変わる可能性があります。
この記事をざっくり図解
[CLAUDE.md] 方針と理由を伝える。守るかどうかはAIの判断に任せる
↓ 取り返しがつかないものだけ
[PreToolUse hook] コマンドを実行する前に機械的に止める
↓ 実行後に確かめたいものだけ
[事後検査(grep / pre-commit / CI)] 差分に禁止事項が入っていないかを調べる
1 CLAUDE.mdに書くもの
私は、書く内容を3つに分けています。「やること(規約)」「やらないこと」「確認してから進めること」です。次が架空の例です。
# CLAUDE.md(架空の受発注管理システム用の例)
## このプロジェクトについて
- 受発注データを管理する社内向けWebアプリ。Node.js 22 / PostgreSQL
- テストは `npm test`、Lintは `npm run lint`
## やること(規約)
- ブランチ名は `feature/{チケット番号}-{短い説明}`
- コミットメッセージは Conventional Commits。AIの署名や生成元の記載は入れない
## やらないこと
- main に直接 push しない。force push もしない
- 理由: 履歴を戻せなくなる。代わりに: ブランチを切って PR にする
- `--no-verify` でコミット時の検査を飛ばさない
- 理由: 検査の意味がなくなる。代わりに: 落ちた原因を直す
- 顧客名、担当者名、契約金額を、コード・コメント・コミットメッセージに書かない
- 理由: リポジトリは委託先とも共有される。代わりに: 案件コード(PJ-XXXX)を使う
- `.env` など認証情報のファイルを読まない、書かない
- 本番DBに接続しない。確認は検証環境で行う
## 確認してから進めること
- DBのマイグレーションを追加するとき
- 既存APIのレスポンス形式を変えるとき
2 「やらないこと」の書き方
書くときに気をつけているのは、次の3点です。
- 理由と、代わりにやることをセットで書く。 「push しない」だけだと、AIは代替手段を自分で探します。「PR にする」まで書いてあれば、そちらに寄せてくれます。
-
数を絞る。 公式ドキュメントは、1つの
CLAUDE.mdを200行以下にすることを勧めていて、長くなるほど遵守率が下がるとも書いています。矛盾する指示があると、どちらかが任意に選ばれる、という記述もあります。なんでも書くと、大事な行が埋もれます。 -
検査できる形で書く。 「適切に対応する」ではなく、「
--no-verifyを使わない」のように、あとで grep で確かめられる文にします。
3 どこで守らせるか
すべてを hook にする必要はありません。書いた規則を、置き場所で分けます。
| 規則 | 置き場所 | 理由 |
|---|---|---|
main への push、force push、--no-verify
|
hook | コマンド文字列に現れ、事故ると戻せない |
| 顧客名を書かない | 事後検査 | コマンドには現れず、書かれた内容に出る |
| 認証情報の読み書き | hook | ツール呼び出しで判定できる(別記事) |
| 本番DBに接続しない | 権限で分ける | hook より前に、そもそも認証情報を渡さない |
| ブランチ名、コミット形式 | CLAUDE.md + CI | 守られなくても直せる |
| 「確認してから進める」 | CLAUDE.md のみ | 機械で判定できない |
公式ドキュメントにも、CLAUDE.md は「強制される設定としては扱われず、コンテキストとして読まれる」とあり、実行を確実に止めたいなら PreToolUse hook を使うよう書かれています。
4 hookで止める
まず、止める規則を3つだけ書いた hook です。Bash の実行前に、コマンド文字列を見ます。
#!/usr/bin/env node
"use strict";
// 「やらないこと」のうち、事故ると取り返しがつかないものだけをここで止める
const RULES = [
{
name: "force push",
pattern: /\bgit\s+push\b[^;&|]*\s(--force(?![\w-])|-f(?![\w-]))/,
message: "force push は禁止です。必要なら人間に確認してください。",
},
{
name: "main への直接 push",
pattern: /\bgit\s+push\b[^;&|]*\s(origin\s+)?main(?![\w./-])/,
message: "main への直接 push は禁止です。ブランチを切って PR にしてください。",
},
{
name: "検査の回避",
pattern: /\bgit\s+commit\b[^;&|]*\s--no-verify\b/,
message: "--no-verify は禁止です。検査で落ちた理由を直してください。",
},
];
async function readStdin() {
let data = "";
for await (const chunk of process.stdin) data += chunk;
return data;
}
async function main() {
let payload;
try {
payload = JSON.parse(await readStdin());
} catch {
process.exit(0); // 読めない入力は止めない(通常の許可フローに任せる)
}
const command = payload?.tool_input?.command;
if (payload?.tool_name !== "Bash" || typeof command !== "string") process.exit(0);
for (const rule of RULES) {
if (rule.pattern.test(command)) {
console.error(`BLOCKED (${rule.name}): ${rule.message}`);
process.exit(2);
}
}
process.exit(0);
}
main();
--force-with-lease は止めない設計にしています。main の後ろに続く文字を見ているのは、\b だけだと main-backup のようなブランチ名まで止めてしまうためです。
流し込んで確かめます。
#!/usr/bin/env bash
# 各コマンドをhookに流して、終了コードを見る
check() {
jq -n --arg c "$1" '{tool_name:"Bash",tool_input:{command:$c}}' | node block-risky-bash.cjs 2>/dev/null
printf 'exit=%s %s\n' "$?" "$1"
}
check 'git push origin feature/login'
check 'git push --force origin feature/login'
check 'git push -f origin feature/login'
check 'git push --force-with-lease origin feature/login'
check 'git push origin main'
check 'git commit -m "fix" --no-verify'
check 'git push origin +feature/login'
check 'git push origin HEAD:main'
check 'sh -c "git push -f"'
check './scripts/deploy.sh'
check 'git push origin main-backup'
check 'git push origin main'
exit=0 git push origin feature/login
exit=2 git push --force origin feature/login
exit=2 git push -f origin feature/login
exit=0 git push --force-with-lease origin feature/login
exit=2 git push origin main
exit=2 git commit -m "fix" --no-verify
exit=0 git push origin +feature/login
exit=0 git push origin HEAD:main
exit=2 sh -c "git push -f"
exit=0 ./scripts/deploy.sh
exit=0 git push origin main-backup
exit=2 git push origin main
登録は settings.json です(スクリプトには chmod +x が必要です)。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-risky-bash.cjs" }
]
}
]
}
}
5 事後に検査する
「顧客名を書かない」は、コマンドには現れず差分の中身に出るので、コミット前に検査します。語のリストはリポジトリの外に置きます。リポジトリに顧客名を書かないための規則なのに、リストを入れたら本末転倒だからです。
#!/usr/bin/env bash
# ステージ済みの差分に、書いてはいけない語(顧客名など)が入っていないか調べる
# 語のリストはリポジトリの外に置く(リポジトリに顧客名を書かないため)
set -euo pipefail
LIST="${BANNED_WORDS_FILE:-$HOME/.config/project-banned-words.txt}"
[ -f "$LIST" ] || { echo "banned words file not found: $LIST" >&2; exit 1; }
# 追加された行だけを対象にする(+++ のファイル名行は除く)
if git diff --cached -U0 | grep -E '^\+' | grep -v '^+++' | grep -i -F -f "$LIST"; then
echo "NG: 書いてはいけない語が含まれています。上の行を直してください。" >&2
exit 1
fi
echo "OK"
一時リポジトリで確かめると、通常の差分は OK、禁止語を含む差分は行を表示して終了コード1になります。
OK
+// acmetrading 用の暫定対応
NG: 書いてはいけない語が含まれています。上の行を直してください。
pre-commit hook や CI に組み込めば、AI が書いた変更も人が書いた変更も、同じ基準で確認できます。
できないこと
-
CLAUDE.mdは強制ではありません。 長い指示や、矛盾する指示は守られにくくなります。書いたから安心、とは考えないほうがよいです。 -
hook が見ているのは、ツール呼び出しだけです。 上のテストでも、抜け道が見つかりました。
git push origin +feature/login(+を付けた refspec は force push と同じ動きです)、git push origin HEAD:main、中で force push をするシェルスクリプト(./scripts/deploy.sh)は、いずれも exit=0 で通ります。正規表現を足せば塞げるものもありますが、コマンド文字列で判定する以上、完全にはなりません。 - hook 自体を AI が書き換えられる状態では、意味が薄れます。 hook ファイルの編集を権限で制限するなどの併用を検討してください(この記事では検証していません)。
- 事後検査は、追加された行の文字列一致だけです。 略称や表記ゆれは通りますし、語のリストは人が更新し続ける必要があります。
- 実際の案件で使っているルールの一覧や検知パターンは、案件ごとに調整しているため、この記事には載せていません。
最後に
「やらないこと」は、書く場所によって守られ方が変わります。理由を添えて CLAUDE.md に書き、事故ると戻せないものは hook で止め、内容に出るものは事後に検査する。この3段に分けるだけでも、書き忘れや抜けは見つけやすくなります。気になったところから、手元の環境で試してみてください。
参考文献
- Claude Code Docs「How Claude remembers your project」(CLAUDE.md、サイズと矛盾の注意、hook との使い分け)
- Claude Code Docs「Hooks reference」(PreToolUse の入力JSON、exit code 2、settings.json の書式)