Claude Code に push まで任せると、人が毎回の差分を読むことはできなくなります。そこで、人のレビューの代わりに push のたびに必ず走る検査 を門番にします。
この記事は、Zenn の記事・本の原稿と小さな Web ツール、それを動かすスクリプトを1つのリポジトリに置き、Claude Code のセッションが無人で更新・push している運用で、実際に使っている検査一式の紹介です。検査は scripts/check.sh 1本(約150行)と GitHub Actions 2本で、全部まとめて約18秒で終わります。
どの検査も「わざと壊すと本当に止まるか」を確かめ、その出力をそのまま載せています。
結論
- 検査は3段:① push 前にローカルで
check.sh→ ② push ごとに CI で同じcheck.sh、失敗したら Issue → ③ 毎日、公開したはずのものが本当に公開されているかを外から確かめる -
check.shが見ているのは10種類。コードの正しさより、AI が無人で書くと起きやすい事故(秘密情報の混入、予約投稿の設定ミス、記事に載せたコードと原本のずれ、実測表の古さ、記憶用ファイルの肥大)を止める項目が中心 - 効いた工夫は3つ。記事の中のコードを原本と1文字単位で突き合わせる、記事に貼った実測表を毎回スクリプトで作り直して一致を確かめる、エラーに直し方まで書く
- 毎回失敗する検査は無視されます。公開時刻の余裕を「48時間」にした案は、運用(当日12時に公開)と両立せず毎回落ちるので「2時間」にしました。検査は、通るべきものが通ることも同じくらい大事
なぜ CI の検査が要るのか
人がレビューしているなら、多くの事故は差分を読めば気づけます。無人で回すと、その「読む人」がいません。指示(CLAUDE.md)に「秘密情報を書かない」「push 前に確かめる」と書いても、それはお願いで、守られたかどうかを誰も見ていません。
このリポジトリで実際にあった(または起きかけた)ことと、それを止める検査の対応はこうです。
| 起きたこと | 止める検査 |
|---|---|
| 秘密情報をリポジトリに書きかけた(運用ルールに「過去の失敗」として記録) | 1. 秘密情報らしき文字列の検出 |
| 投稿数の上限で予約記事が取り込まれず、上限が明けた瞬間に一斉公開されかけた(運用記録 2026-10-08) | 4. 公開の歯止め(予約なしの公開・直前の予約を止める) |
| 本と記事と配布物の3か所に同じスクリプトを載せていて、直すたびにずれうる | 6. 記事に載せたコードと原本の一致 |
| 実測表を記事に貼ったあと、スクリプトを直して表が古くなりうる | 7. 実測表を作り直して突き合わせる |
| 状態を書くファイル(STATE.md)が追記で膨らみ、次のセッションが読み切れなくなる | 10. STATE.md の行数 |
検査はどれも「終了コードが0か非0か」で決まります。AI に読ませて判断させる部分はありません。
全体の形
Claude Code のセッション
├─ 編集
├─ bash scripts/check.sh ← ① push 前(指示書で必須にしている)
└─ git push
│
GitHub Actions
├─ check.yml:push ごとに同じ check.sh ← ② 落ちたら Issue を立てる
└─ qiita-publish.yml:毎日 8:15 JST に audit_public.py ← ③ 公開物の実在を外から確認
①と②は同じスクリプトです。ローカルで通したのに CI で落ちる、ということを減らすため、検査の本体は1か所にだけ置いています。
check.sh の骨組み
#!/usr/bin/env bash
# 公開・push 前の品質チェック。問題があれば非0で終了する。
set -uo pipefail
root="$(cd "$(dirname "$0")/.." && pwd)"
cd "$root"
fail=0
# 検査対象:追跡中のファイル+まだ追加していない新規ファイル(.gitignore は除く)
files() { git ls-files -co --exclude-standard "$@"; }
set -e を使わず、fail=1 を立てて最後まで走らせます。1つ目で止めると、AI が「1つ直す → push → また落ちる」を繰り返すからです。全部の NG を一度に見せた方が、直す回数が減ります。
対象を git ls-files -co --exclude-standard にしているのは、まだ git add していない新しいファイルも検査に含める ためです。AI が新しく作ったファイルこそ見たいので、ここは外せません。
最後はこうなっています。
# 7. STATE.md の長さ
lines=$(wc -l < STATE.md); [ "$lines" -le 100 ] || { echo "NG: STATE.md が ${lines} 行(100行以内に)" >&2; fail=1; }
[ $fail -eq 0 ] && echo "check: OK"
exit $fail
普段の実行結果です(2026-10-17)。
$ time bash scripts/check.sh
block-dangerous: 39+3 件すべて期待どおり
checker: 33 件すべて期待どおり
test_fe: OK(10問の答えが参照実装と一致、単体テスト通過)
check: OK
real 0m18.027s
以下、検査を1つずつ、わざと壊したときの出力 と一緒に見ていきます。壊す作業は、本物の作業ツリーを汚さないよう git worktree add で作った別の作業ツリーで行い、毎回 git checkout -- . で元に戻しました。
1. 秘密情報らしき文字列
secret='[A-Za-z0-9._%+-]+@(gmail|yahoo|outlook|hotmail|icloud)\.(com|co\.jp)|sk-ant-[A-Za-z0-9]|ghp_[A-Za-z0-9]{20}|github_pat_[A-Za-z0-9_]{20}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY|xox[baprs]-[A-Za-z0-9-]{10}|password\s*[:=]'
if files -z | xargs -0 grep -nIE "$secret" -- 2>/dev/null | grep -vE '^scripts/check.sh:'; then
echo "NG: 秘密情報・個人情報らしき文字列があります" >&2; fail=1
fi
フリーメールのアドレス、Anthropic・GitHub・AWS・Slack のトークンの形、秘密鍵の見出し、「password」の直後にコロンか等号が続く形を拾います。noreply のアドレス(users.noreply.github.com)は対象外の形にしてあるので、Actions のコミット設定は引っかかりません。
GitHub のトークンの形をした文字列を、調査メモのファイルに1行書いてみました。
### 秘密らしき文字列
research/tmp-note.md:1:token: ghp_aaaa…(実際の出力では24文字すべて表示)
NG: 秘密情報・個人情報らしき文字列があります
exit=1
ファイル名と行番号が出るので、どこを消せばよいかがすぐ分かります。
限界もはっきりしています。形の決まった秘密しか拾えません。住所や口座番号、任意の文字列のパスワードは素通りします。だからこのリポジトリでは「秘密はリポジトリに書かない、GitHub のシークレットに置く」をルールにし、この検査は「うっかり」の網として使っています。GitHub 側の secret scanning を併用するのも手です。
2. 公開する記事に、未置換の URL や体験談の言い回しがないか
for f in $(grep -rl "^published: true" articles 2>/dev/null) ...; do
if grep -n '【.*URL】' "$f"; then echo "NG: $f に未置換の URL プレースホルダ" >&2; fail=1; fi
if grep -nE '私が(現場|実際に|以前)|エンジニア歴|誰でも(簡単に)?稼|必ず稼|月[0-9]+万円稼' "$f"; then echo "NG: $f に体験談・経歴の表現(D-010)" >&2; fail=1; fi
done
AI に記事を書かせると、下書きの段階で 【本のURL】 のような穴を残すことがあります。下書きなら問題ありませんが、公開する記事に残ると恥ずかしい事故です。
2行目は、このリポジトリの方針「架空の経歴・体験談を書かない」「誇大な表現をしない」を、よく出る言い回しだけでも機械で止めるためのものです。公開済みの記事の末尾に2つとも足してみました。
### 公開記事に未置換URLと体験談
69:詳しくは【本のURL】へ。私が現場で使っている設定です。
NG: articles/claude-code-completion-criteria.md に未置換の URL プレースホルダ
69:詳しくは【本のURL】へ。私が現場で使っている設定です。
NG: articles/claude-code-completion-criteria.md に体験談・経歴の表現(D-010)
exit=1
対象を published: true の記事だけに絞っている のがポイントです。下書きまで止めると、書きかけを push できなくなります。言い回しの検査は数パターンだけなので、すり抜けは当然あります。ここも「網」であって「保証」ではありません。
3. 下書きに予約時刻を書かない
for f in $(ls articles/*.md 2>/dev/null); do
if grep -q '^published: false' "$f" && grep -q '^published_at:' "$f"; then echo "NG: $f は下書きなのに published_at がある(Zenn のデプロイが中断する)" >&2; fail=1; fi
done
Zenn の GitHub 連携で、下書き(published: false)に published_at を書いた組み合わせを止める検査です。メッセージにある「デプロイが中断する」は、この規則を入れたときの記録に基づくもので、今回あらためて Zenn 側で確かめたものではありません。どちらにしても「下書きなのに公開時刻がある」は意図があいまいな状態なので、止めて困ることはありません。
### 下書きに予約時刻
NG: articles/claude-code-prompt-patterns-5.md は下書きなのに published_at がある(Zenn のデプロイが中断する)
exit=1
4. 公開の歯止め:予約なしの公開と、直前の予約を止める
一番効いている検査です。別ファイル scripts/check_frontmatter.py に分けています。
LEAD_HOURS = 2
def base_text(path):
base = os.environ.get("CHECK_BASE") or f"origin/{BRANCH}"
if re.fullmatch(r"0+", base):
return None # 新しいブランチの最初の push
r = subprocess.run(["git", "cat-file", "-e", f"{base}^{{commit}}"], capture_output=True)
if r.returncode != 0:
print(f"注意: 比較元 {base} が見つからないため、公開時刻の検査を省略", file=sys.stderr)
return ""
r = subprocess.run(["git", "show", f"{base}:{path}"], capture_output=True, text=True)
return r.stdout if r.returncode == 0 else None # None = その時点では存在しない新規ファイル
# (記事ごとのループの中)
# 公開の歯止め:公開に関わる項目がこの push で変わったものだけを見る
if pub == "true":
old = base_text(path)
if old == "":
continue
ofm = fm_of(old.replace("\r\n", "\n")) if old is not None else None
changed = ofm is None or (ofm.get("published"), unq(ofm.get("published_at", ""))) != (pub, at)
if changed and (t is None or t < now + datetime.timedelta(hours=LEAD_HOURS)):
errors.append(f"{path}: published: true に変える記事は、published_at を今から{LEAD_HOURS}時間以上先にする(今 {at or 'なし=即時公開'})")
やっていることは「この push で published か published_at が変わった記事 のうち、published: true になったものは、公開時刻が今から2時間以上先でなければ NG」です。
Zenn の GitHub 連携は push した瞬間に同期されるので、published: true を書いて push した時点で取り消しがききません。予約時刻を必ず先にしておけば、push のあとに気づいても、時刻が来る前に false に戻せます。
「変わった記事だけ」を見るのが大事です。公開済みの記事は published_at が過去なので、全部の記事を見ると、公開済みの記事が毎回 NG になってしまいます。比較元は CI では「push 前のコミット」(後述の CHECK_BASE)、ローカルでは origin/<ブランチ> です。
壊した結果です。予約なしで true にした場合:
### 予約なしの公開
NG: articles/claude-code-prompt-patterns-5.md: published: true に変える記事は、published_at を今から2時間以上先にする(今 なし=即時公開)
exit=1
1時間後に予約した場合:
### 1時間後の予約
NG: articles/claude-code-prompt-patterns-5.md: published: true に変える記事は、published_at を今から2時間以上先にする(今 2026-10-17 21:58)
exit=1
同じファイルで、Zenn の形式の検査もしています。slug(ファイル名)は12〜50字の a-z0-9_-、topics は1〜5個、type は tech か idea、CRLF 不可、本の config.yaml の章ファイルが実在するか、などです。
### slug と topics
NG: articles/Tips.md: slug は a-z0-9_- の12〜50字
NG: articles/claude-code-prompt-patterns-5.md: topics は1〜5個(今 6 個)
exit=1
「48時間」をやめて「2時間」にした理由
この検査を入れたとき、外部の点検(別の AI セッションによる独立した点検)からの提案は「48時間以上先」でした。安全側に倒すならその方が良さそうに見えます。
ただ、この運用では「月曜・木曜の朝の作業で、その日の12時に1本を公開する」という手順になっていました。48時間にすると、正しい手順で作業しても毎回検査が落ちて Issue が立ちます。毎回落ちる検査は、そのうち誰も見なくなります。そこで、手順と両立する最小の余裕として2時間にし、その理由をスクリプトの先頭のコメントに残しました。
検査は厳しければ良いわけではありません。正しい作業が必ず通ることも、検査の仕様の一部 です。
5. シェルと JSON の構文
for f in $(files '*.sh'); do bash -n "$f" || { echo "NG: $f 構文エラー" >&2; fail=1; }; done
for f in $(files '*.json'); do python3 -m json.tool "$f" >/dev/null || { echo "NG: $f JSON エラー" >&2; fail=1; }; done
地味ですが、AI が書いたシェルスクリプトを実行せずに push してしまったときの最低限の網です。bash -n は実行せずに構文だけ見ます。
### シェルの構文エラー
scripts/broken.sh: line 1: syntax error near unexpected token `then'
scripts/broken.sh: line 1: `if then'
NG: scripts/broken.sh 構文エラー
exit=1
6. 記事に載せたコードが、原本と1文字も違わないか
このリポジトリでは、危険なコマンドを止めるフック block-dangerous.sh を、配布物の原本・有料本の章・無料記事の3か所 に載せています。原本を直したのに記事を直し忘れると、読者は古い(穴のある)版をコピーします。
そこで、記事の Markdown から該当のコードブロックを正規表現で取り出し、原本ファイルと完全一致するかを見ます。
hook=products/01-claude-code-kit/hooks/block-dangerous.sh
for f in books/claude-code-practical-kit/hooks.md articles/claude-code-block-dangerous-hook.md; do
python3 - "$f" "$hook" <<'PY' || { echo "NG: $f の block-dangerous.sh が $hook と違う" >&2; fail=1; }
import re, sys
t = open(sys.argv[1], encoding="utf-8").read()
m = re.search(r"^(`{3,4})bash:[^\n]*block-dangerous\.sh\n(.*?)\n\1$", t, re.S | re.M)
sys.exit(0 if m and m.group(2) == open(sys.argv[2], encoding="utf-8").read().rstrip("\n") else 1)
PY
done
コードブロックの見出し(```bash:.claude/hooks/block-dangerous.sh)で場所を特定しています。Zenn のコードブロックはファイル名を書けるので、それをそのまま目印に使えます。
記事側の1文字(--force-with-lease の最後の e)だけ消してみました。
### 記事に載せたフックが原本と違う
NG: articles/claude-code-block-dangerous-hook.md の block-dangerous.sh が products/01-claude-code-kit/hooks/block-dangerous.sh と違う
exit=1
同じ仕組みで、有料本に載せた管理設定(JSON)と記録用スクリプトも原本と突き合わせています。JSON は文字列ではなく json.loads した結果を比べるので、空白や並び順の違いでは落ちません。
7. 記事に貼った実測表を、毎回作り直して突き合わせる
6 は「コードが同じか」でした。7 は「そのコードを動かした結果が、記事に書いた表と同じか」です。
scripts/test_block_dangerous.sh は、フックにコマンド39件と「検査できないとき」の3件(jq がない・入力が空・JSON が壊れている。どれも止める側に倒す)を流し、止まるべきものが止まり、通すべきものが通るかを確かめます。
# 期待値|区分|コマンド ## 備考 期待値 2=止める 0=通す
cases=(
"2|止める|rm -rf /"
"2|止める|rm -fr /"
"2|止める|git push -f"
"2|止める|git push origin +main"
"0|通す(正しい)|rm -rf /tmp/build"
"0|通す(正しい)|git push --force-with-lease"
# …(全39件)
)
--table を付けると、記事に貼るための Markdown の表を出力します。check.sh はこれを毎回実行し、出力された表が記事の中にそのまま含まれているか を見ます。
table=$(bash scripts/test_block_dangerous.sh --table) || fail=1
for f in books/claude-code-practical-kit/hooks.md articles/claude-code-block-dangerous-hook.md; do
python3 -c 'import sys; sys.exit(0 if sys.argv[1] in open(sys.argv[2], encoding="utf-8").read() else 1)' "$table" "$f" \
|| { echo "NG: $f の実測表が scripts/test_block_dangerous.sh --table の出力と違う(貼り直す)" >&2; fail=1; }
done
2通り壊してみます。まず、記事の表の1行だけを手で書き換えた場合(表の「止まる(2)」の後ろに一言足した):
### 記事の実測表だけ手で書き換え
block-dangerous: 39+3 件すべて期待どおり
NG: articles/claude-code-block-dangerous-hook.md の実測表が scripts/test_block_dangerous.sh --table の出力と違う(貼り直す)
exit=1
次に、フックの原本の方を弱めた場合(git push -f を止めるパターンから -f の形を消した):
$ bash scripts/test_block_dangerous.sh
NG: [止める] git push -f → 0(期待 2)
exit=1
「表は正しいがコードが変わった」「コードは正しいが表を手で触った」のどちらも止まります。AI に記事を書かせると、表を「それらしく」整えたくなることがありますが、表はスクリプトの出力を貼るもので、書くものではない と仕組みで決めてしまえば、その心配がなくなります。
この考え方は、記事の数字すべてに広げられます。たとえば「claude -p を10回流して何回従ったか」のような実測は、集計スクリプトの出力をそのまま貼り、検査で突き合わせる形にしておけば、版が上がって再計測したときに、記事の直し忘れが CI で見つかります。
8. 機能のテスト
Web ツールの判定ロジック(33件)と、練習問題の答え(10問を参照実装で解き直す)は Node.js のテストで確かめています。
node scripts/test_checker.js || fail=1
node scripts/test_fe.js || fail=1
ここは普通の単体テストなので詳細は省きます。ポイントは、テストを別のコマンドにせず check.sh の中から呼んでいる ことです。AI に渡す指示は「push 前に check.sh を通す」の1行だけで済み、テストの実行を忘れることがありません。
9. 大きなファイルを正本のブランチに入れない
このリポジトリでは動画(mp4)も作っています。動画を作り直すたびに正本のブランチにコミットすると、履歴が数 MB ずつ太ります。そこで mp4 は「常に1コミットだけの別ブランチ」に置き、正本のブランチに入っていたら止めます。
python3 scripts/shorts_media.py check || fail=1
if [ -n "$(git ls-files 'videos/*.mp4' | head -1)" ]; then
echo "NG: mp4 が正本ブランチに入っている(履歴が太る)。python3 scripts/shorts_media.py put <slug> で置き、git rm --cached で外す" >&2; fail=1
fi
エラーメッセージに 直し方のコマンドまで 書いています。無人のセッションは、エラーを読んで自分で直します。「何がだめか」だけでなく「どうすれば通るか」が書いてあると、直す方向を間違えにくくなります。
shorts_media.py check の方は逆向きの検査で、「配信予定の表に載っている動画が、別ブランチに実際に置いてあるか」を確かめます。毎朝の自動配信が当日になって「ファイルがない」で失敗するのを、push の時点で止めるためです。
10. 状態を書くファイルの行数
lines=$(wc -l < STATE.md); [ "$lines" -le 100 ] || { echo "NG: STATE.md が ${lines} 行(100行以内に)" >&2; fail=1; }
無人の運用では、セッションは使い捨てで、会話の記憶は次に引き継がれません。次のセッションは、リポジトリの STATE.md(今の状態・進行中の作業・次にやること)を最初に読んで状態を取り戻します。
このファイルは「上書きで最新に保つ」ルールですが、AI は放っておくと追記を重ねます。追記が続くと、古い情報と新しい情報が混ざり、次のセッションが読み違えます。行数の上限は、それを機械で止めるためのものです。10行足すと止まります。
### STATE.md が長すぎる
NG: STATE.md が 110 行(100行以内に)
exit=1
CLAUDE.md などの指示ファイルの長さも、同じように上限を置けます。
CI に載せる:check.yml
ローカルで通したかどうかは、AI の自己申告です。そこで、push ごとに GitHub Actions で同じ検査を走らせます。
# push のたびに scripts/check.sh を走らせる(公開経路の機械的なゲート)。失敗したら Issue を1件立てる。
name: check
on:
push:
branches: [claude/friendly-wright-s3fwyv]
workflow_dispatch:
permissions:
contents: read
issues: write
concurrency:
group: check-${{ github.sha }}
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: check.sh
env:
# この push で変わった記事だけ、公開時刻が push から十分先か(scripts/check_frontmatter.py の LEAD_HOURS)を見る
CHECK_BASE: ${{ github.event.before }}
run: bash scripts/check.sh
- name: Open issue on failure
if: failure()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh issue create --title "momo: check.sh が失敗(${GITHUB_SHA::7})" \
--body "push ${GITHUB_SHA} で scripts/check.sh が失敗しました。ログ:${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}
公開物(articles/ books/)に関わる失敗なら、Zenn には既に同期されている可能性があります。次の作業セッションで最初に直してください。"
要点は3つです。
-
CHECK_BASE: ${{ github.event.before }}:push 前のコミットを比較元として渡します。これで 4 の「この push で変わった記事だけ見る」が CI でも正しく動きます。fetch-depth: 0は、その比較元のコミットを取得するためです。新しいブランチの最初の push ではbeforeが0000…になるので、スクリプト側で特別扱いしています - 失敗したら Issue:Actions の失敗は、通知を見ていないと気づけません。Issue にすると、次のセッションが Issue の一覧を見て「最初に直すもの」として拾えます。人にも GitHub の通知で届きます
-
Issue の本文に「既に同期されている可能性」と書く:CI は push の 後 に走ります。Zenn の同期も push で始まるので、CI で落ちたときには、もう公開側に届いているかもしれません。だから CI は「最後の網」で、本当の門番は push 前のローカルの
check.shです。CI は、それが実行されなかったときに気づくためにあります
push の前に止めたいなら、ブランチ保護で「check が通らないとマージできない」にし、AI には作業用のブランチに push させる構成もあります。このリポジトリは、Zenn の同期ブランチに直接 push する運用なので、その手前で止める仕組みがなく、ローカルの検査と事後の Issue の2段にしています。
push の後:公開したはずのものが、本当に公開されているか
検査を全部通っても、「公開したつもりが公開されていない」は起きます。外部のサービス側の上限や遅延は、リポジトリの中からは見えないからです。
そこで、毎日1回、外から実物を確かめるスクリプト scripts/audit_public.py を Actions で動かしています。AI は使いません。
_, b = get(f"https://zenn.dev/api/articles?username={USER}&count=100")
live_articles = {a["slug"] for a in json.loads(b)["articles"]}
# …
for p in sorted(glob.glob(os.path.join(ROOT, "articles", "*.md"))):
slug = os.path.basename(p)[:-3]
pub, at = fm(p)
if not pub:
continue
if at and datetime.datetime.strptime(at, "%Y-%m-%d %H:%M").replace(tzinfo=JST) + GRACE > now:
continue
ok = slug in live_articles
rows.append(["zenn-article", slug, "ok" if ok else "MISSING"]); bad += not ok
リポジトリで published: true かつ公開時刻を2時間以上過ぎた記事が、Zenn の公開 API の一覧に載っているかを見ます。本・Web ツール(HTTP 200 か)・転載先の記事も同じように見て、結果を CSV に追記し、問題があれば通知します。今日の実行結果です。
$ python3 scripts/audit_public.py
zenn-article claude-code-completion-criteria ok
zenn-article claude-code-managed-settings-security-qa ok
zenn-article claude-code-prompt-hook-2-1-294 ok
zenn-book claude-code-team-guardrails ok
zenn-book claude-code-practical-kit ok
tool claude-md-generator ok
qiita claude-code-completion-criteria ok
qiita claude-code-managed-settings-security-qa ok
qiita claude-code-prompt-hook-2-1-294 ok
これまでの記録(10/08〜10/17、120行)で ok 以外は9行あり、すべて同じ1冊の本でした。リポジトリでは公開にしたのに、Zenn の本の投稿数の上限(本は直近1週間の投稿数で判定、と公式 FAQ にあります)で取り込まれていなかったものです。リポジトリの中だけを見る検査では、これは見つかりません。
ただし、上限が明けるのを待っているだけの状態を毎日「異常」と通知するのは誤報です。そこで、直近の本の公開から8日以内なら MISSING ではなく WAIT と記録して通知しないよう、監査の方を直しました。検査と同じで、監査も「待つだけの正常な状態」を異常扱いしないことが大事でした。
作って分かったこと
- 検査は「AI が守るべきルール」を、守られたかどうか確かめられる形に置き換えたもの。CLAUDE.md の「秘密情報を書かない」「表は実測を貼る」は、それぞれ 1・7 の検査になって初めて効くようになりました
-
エラーには直し方を書く。「貼り直す」「
git rm --cachedで外す」のように次の一手が書いてあると、無人のセッションでも直す方向を外しにくい - 毎回落ちる検査は検査ではない。4 の48時間、監査の WAIT のように、正しい状態が通るように合わせるのも検査の仕事
-
検査の本体は1か所。ローカルと CI で同じ
check.shを呼び、テストもその中から呼ぶ。AI への指示は「check.shを通す」の1行で済む - 中から見る検査と、外から見る監査は別物。リポジトリの中が正しくても、公開先で起きることは外から見ないと分からない
限界
-
検査スクリプトも同じリポジトリにある ので、AI が検査そのものを書き換えれば通ってしまいます。このリポジトリでは、判断の理由を記録に残すルールと、別の AI セッションによる定期的な点検で見ているだけで、仕組みとしては防げていません。防ぐなら、検査を別のリポジトリに置く、
CODEOWNERSとブランチ保護で検査ファイルの変更に人の承認を要求する、などが必要です - 秘密情報と体験談の検査は、決まった形・言い回しだけを拾う網です。すり抜けはあります
- CI は push の後に走るので、Zenn のように push で即同期される先には「事後の通知」にしかなりません
- ここで壊して試したのは各検査1通りずつです。検査ごとのすり抜けをすべて調べたわけではありません
Claude Code の設定(settings.json の許可・拒否やフック)に穴がないかは、無料の設定チェッカーに貼るとまとめて確かめられます:https://acerola-tools.acerola.workers.dev/check/?from=qiita-claude-code-ci-gate-checks
本記事は Zenn にも掲載しています:https://zenn.dev/acerola_jp/articles/claude-code-ci-gate-checks