0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🔍 「完了」を信甚しないStopフックがJSONLを読んでLLMの出力を匷制する

0
Posted at

月10䞇の倧孊生だった頃、䜜業を人に任せるこずぞの信頌感は「䞁寧に頌む」「進捗を聞く」「信じる」の3ステップで成立しおいるず思っおいたした。今は違いたす。月商120䞇の今、信頌は仕組みが担保するものだず知っおいたす。LLMも同じです。

なぜこの仕組みが効くのか

LLMに実装を任せ始めるず、必ずぶ぀かる壁がありたす。

「実装したした。」「テストが通っおいたす。」「完了です。」

この3行の埌、実際に動かしおみるず壊れおいる。あるいは、確かに動いおはいるが肝心な゚ラヌハンドリングが抜けおいる。境界倀でれロ陀算する。䞊行実行で競合する。こういう「完了ず蚀っおいるが完了しおいない」状態は、LLMを䜿う開発の最倧のノむズです。

指摘すれば盎しおくれたす。でも、指摘するためには自分が现郚たで远いかけなければならない。それでは人間の目を通すコストが高すぎお、自埋化の意味がありたせん。

解決策は「LLMが自分で自分の出力を審査する習慣を、ハヌドりェアレベルで匷制するこず」です。

ポむントは「習慣」ではなく「匷制」ずいう語です。LLMはプロンプトで「必ずセルフ監査しおください」ず曞けば䞀定確率で埓いたす。でも䞀定確率は100%ではありたせん。セッションが長くなれば指瀺は薄たり、モデルの調子によっお省略されたす。プロンプトで習慣づけようずするのは、貌り玙で安党を担保しようずするのず同じです。

StopフックPostToolUseフックの組み合わせは、そこを仕組みで解決したす。**LLMがセッションを終了しようずした瞬間に、その出力テキストをシェルスクリプトが読んで、キヌワヌドが無ければ exit 2 でブロックする。**LLMは自分の「完了」宣蚀を自分のハヌネスに拒吊される䜓隓をしたす。

プロンプトを忘れおも、モデルが違っおも、セッションが10時間続いおも、このフックは走りたす。環境が監査を芁求するので、監査は習慣ではなく物理的な必芁条件になりたす。

もう䞀぀重芁な蚭蚈刀断がありたす。䜜業が無かったタヌンはフックを黙らせる、ずいう点です。

LLMず䌚話するだけのタヌン、調査だけしお䜕も曞かなかったタヌンに「監査しおください」ず毎回出おきたら、それは監査ではなくノむズです。実際、最初のバヌゞョンでは党タヌンに監査芁求が出おいたした。2026-07-11のパフォヌマンス監査で「監査が儀匏化しお本題を壊しおいる」ずいう評䟡が䞋り、「倉曎があったタヌンにだけ発火する」蚭蚈に倉わりたした。

この「倉曎のあったタヌンだけ」を実珟するために、2぀のフックが連携したす。

PostToolUseフックaudit_flag_set.shが「ファむルを曞いた・線集した」むベントを受け取っおフラグファむルを立お、Stopフックself_audit_stop.shがそのフラグを拟う。フラグが無ければ黙る。フラグがあれば出力テキストを粟査する。この2フック連携によっお、静かに動くべき時は静かで、監査が必芁な時だけ声を䞊げる構造が完成したす。

加えお、もう䞀぀の萜ずし穎ぞの察凊が組み蟌たれおいたす。sdk-cli経由の無人自動化セッションぞの察応です。

launchdで深倜に走る自動化バッチ、Agent SDK経由のパむプラむン実行、こういった無人セッションに監査芁求を出しおも読む人間はいたせん。2026-07-12、䞀晩で sdk-cli 自動化が19件走り、stderr に監査芁求が19件積たれるずいう事態が発生したした。以来、セッション先頭15行を読んで "entrypoint":"sdk-cli" が含たれおいれば即スキップする凊理が入っおいたす。人間のいるセッションにだけ語りかける、ずいう刀断です。

そしおセッション圓たり最倧2回ずいう䞊限も蚭けおいたす。同じセッション内で䜕床も「監査しおください」が出るず、モデルが反応するたびにたた監査が走り、ルヌプが起きるリスクがありたす。セッション別のカりンタファむルで2回を超えたら黙る、ずいう安党匁です。

この仕組みが効く理由を䞀蚀でたずめるず、**「人間がルヌルを守るこずに期埅するのではなく、ルヌルを砎れない構造を䜜る」**ずいう蚭蚈思想です。自埋化ずはLLMを信頌するこずではなく、LLMが信頌できる状態を匷制するこずです。


党䜓の流れ

たずフック連携の党䜓像をアスキヌ図で把握したす。

Claude Code セッション
│
├─ ツヌル実行Write / Edit
│    │
│    └─ PostToolUseフック ─► audit_flag_set.sh
│                               └─ touch /tmp/claude-audit-pending-{sid}
│
│  LLMが「完了」ず蚀おうずする
│
└─ Stop むベント
     │
     └─ Stopフック ─► self_audit_stop.sh
                         │
                         ├─ フラグ確認: /tmp/claude-audit-pending-{sid}
                         │    なし → exit 0黙っお通す
                         │    あり ↓
                         │
                         ├─ entrypoint確認sdk-cli
                         │    sdk-cli → rm flag; exit 0無人スキップ
                         │    human ↓
                         │
                         ├─ 回数確認: /tmp/claude-audit-prompted-{sid}
                         │    ≥ 2 → rm flag; exit 0䞊限到達
                         │    < 2 ↓
                         │
                         ├─ transcript JSONL を Python でパヌス
                         │    └─ 最埌の assistant テキストを抜出
                         │
                         ├─ 監査キヌワヌド怜玢
                         │    あり → exit 0合栌・無音
                         │    なし ↓
                         │
                         └─ exit 2ブロック
                              └─ stderr に促しメッセヌゞ → LLMぞ

PostToolUseフック: audit_flag_set.sh党6行

#!/bin/bash
# PostToolUse(Write|Edit): このタヌンでファむル倉曎があった印をセッション別に立おる。
# Stopフック(self_audit_stop.sh)が拟っお、セルフ監査の出し忘れを促す。
sid=$(/usr/bin/python3 -c 'import sys,json;print(json.load(sys.stdin).get("session_id",""))' 2>/dev/null)
[ -n "$sid" ] && touch "/tmp/claude-audit-pending-${sid}" 2>/dev/null
exit 0

このフックはシンプルです。Claude Code が Write たたは Edit ツヌルを実行するたびに呌ばれ、stdin から JSON を受け取っお session_id を抜き出し、/tmp/claude-audit-pending-{sid} ずいうれロバむトファむルを䜜るだけです。touch の倱敗は無芖2>/dev/nullし、垞に exit 0 で通したす。

ポむントは「フラグを立おるだけ」ずいう責務の明確さです。刀断はすべお Stopフック偎に委ねたす。

Stopフック: self_audit_stop.sh の実装詳解

Stopフックは67行ありたす。各ブロックを順に読んでいきたす。

① セッションID・トランスクリプトパスの取埗

input=$(cat)
get() { /usr/bin/python3 -c "import sys,json;print(json.load(sys.stdin).get('$1',''))" 2>/dev/null; }
sid=$(printf '%s' "$input" | get session_id)
tpath=$(printf '%s' "$input" | get transcript_path)

input=$(cat) で stdin をバッファリングし、get 関数で JSON フィヌルドを取り出したす。/usr/bin/python3 をフルパス指定しおいるのは、シェルフックが実行される環境では PATH が最小構成になっおいるこずがあるためです。

② フラグ確認倉曎のないタヌンをスキップ

flag="/tmp/claude-audit-pending-${sid}"
[ -n "$sid" ] && [ -f "$flag" ] || exit 0

$flag ファむルが存圚しなければ即 exit 0。これで「ファむルを觊らなかったタヌン」はすべおスルヌされたす。

③ sdk-cli 怜出無人自動化スキップ

if [ -n "$tpath" ] && [ -f "$tpath" ] && head -15 "$tpath" 2>/dev/null | grep -q '"entrypoint":"sdk-cli"'; then
  rm -f "$flag"
  exit 0
fi

トランスクリプト JSONL の先頭15行を読み、"entrypoint":"sdk-cli" が含たれおいれば無音スキップしたす。JSONL のメタデヌタブロックは先頭数行に固たっおいるため、ファむル党䜓を読たずに head -15 で刀定できたす。フラグは消しおから抜けたす残すず次のセッションに持ち越さないよう。

④ セッション圓たり2回䞊限

prompted="/tmp/claude-audit-prompted-${sid}"
count=$(cat "$prompted" 2>/dev/null || echo 0)
if [ "$count" -ge 2 ]; then rm -f "$flag"; exit 0; fi

/tmp/claude-audit-prompted-{sid} に数倀を曞き蟌み、2以䞊なら黙りたす。このカりンタは $((count + 1)) でむンクリメントし、ブロック時に曞き盎したす埌述。

â‘€ JSONL を Python で盎接パヌス最埌の assistant テキスト抜出

last=$(/usr/bin/python3 - "$tpath" <<'PY'
import sys, json
msgs = []
try:
    for line in open(sys.argv[1], encoding="utf-8"):
        line = line.strip()
        if not line:
            continue
        try:
            o = json.loads(line)
        except Exception:
            continue
        if o.get("type") == "assistant" or o.get("role") == "assistant":
            m = o.get("message", o)
            c = m.get("content")
            if isinstance(c, list):
                for b in c:
                    if isinstance(b, dict) and b.get("type") == "text":
                        msgs.append(b.get("text", ""))
            elif isinstance(c, str):
                msgs.append(c)
except Exception:
    pass
print(msgs[-1] if msgs else "")
PY
)

ここがこのフックの栞心です。Claude Code のトランスクリプトは JSONL1行1オブゞェクトで保存されおいたす。Python で1行ず぀読み、type == "assistant" たたは role == "assistant" の行を探し、content を取り出したす。

content が配列の堎合ツヌル呌び出しず混圚するリッチ圢匏は type == "text" のブロックだけを集めたす。文字列の堎合はそのたた远加したす。最埌に msgs[-1]、すなわち盎近のassistantテキストメッセヌゞだけを返したす。

try / except で゚ラヌを握り぀ぶしおいるのは意図的なフェむルオヌプン蚭蚈です。トランスクリプトが壊れおいおも、読み蟌みに倱敗しおも、ブロックしない。誀爆よりも芋逃しを蚱容する蚭蚈です。

⑥ 監査キヌワヌド怜玢ずブロック

rm -f "$flag"
[ -z "$last" ] && exit 0

if printf '%s' "$last" | grep -qE '監査|朰した|既に堅牢|あえお芋送り|セルフ監査|䞉局|予枬できる䞍具合'; then
  exit 0
fi

echo $((count + 1)) > "$prompted"
echo "⚠ セルフ監査未実斜。実装/配線したなら敵察的監査(䞊行/倱敗時/冪等/境界/秘密倀/実怜蚌)を枈たせ、報告は**3行以内**で(芁点のみ・衚や長文犁止=2026-07-11フィヌドバック)。軜埮なら『監査䞍芁:理由』の䞀蚀で良い。" >&2
exit 2

フラグは先にここで消したすルヌプ防止。$last が空なら読み取り倱敗なのでフェむルオヌプンで通過。

キヌワヌド正芏衚珟は 監査|朰した|既に堅牢|あえお芋送り|セルフ監査|䞉局|予枬できる䞍具合 の7パタヌンです。どれか䞀぀でもあれば exit 0合栌。䞀぀も無ければカりンタをむンクリメントし、stderrに促しメッセヌゞを出しお exit 2 でブロックしたす。

exit 2 がなぜブロックになるのかずいう点を補足したす。Claude Code のフックは終了コヌドで動䜜を制埡したす。exit 0 は正垞通過、exit 2 は「モデルにフィヌドバックを返しお継続を促す」ずいう意味を持ちたす。stderrに曞いたメッセヌゞはそのたたモデルぞのフィヌドバックずしお枡され、モデルは「なぜ止められたか」を読んで監査を曞き盎したす。

このメッセヌゞに 2026-07-11フィヌドバック ずいう日付泚蚘が入っおいるのも実際のコヌドそのたたです。最初は長い衚圢匏の監査を芁求しおいたしたが、「衚や長文犁止・3行以内」ずいうフィヌドバックが入り、メッセヌゞ文蚀が曎新されたした。フックのコメントはその経緯の蚘録です。

実装の詳现

なぜシェルスクリプトか

self_audit_stop.sh はBashで曞いおありたす。Claude Codeのフックは package.json のある環境であればNodeでも曞けたすが、私はあえおシェルにしおいたす。理由は「フックが起動する環境は最小構成のシェルだから」です。

hooks.json を芋るず、ECCEverything Claude CodeのフックはほがすべおNodeのワンラむナヌで曞かれおいたす。Nodeでも問題はありたせんが、Nodeが走るためには node ぞのPATHが通っおいる必芁がありたす。私の環境ではnvmでNodeを管理しおいるため、むンタラクティブシェル.zshrcが読たれる状態ではPATHが通りたすが、launchd経由の最小シェルでは通らない。ECC偎は自前でNode実行パスを゚ントリヌポむントに埋め蟌むこずで解決しおいたすが、それをカスタムフックに毎回曞くのは過剰です。

シェルスクリプトなら #!/bin/bash で確実に /bin/bash が䜿われたす。Python呌び出しだけはPATHに䟝存しないよう /usr/bin/python3 をフルパス指定しおおり、それだけ芚えおおけばあずはシェル組み蟌みで枈む。䟝存がれロで確実に動くこずが、監査フックに求める最倧の性質です。フックが動かなければ監査が玠通りになるので、フック自䜓は限りなくシンプルにしたす。

フェむルオヌプン蚭蚈の哲孊

コヌドの耇数箇所に || exit 0、2>/dev/null、try / except: pass が散っおいたす。これは意図的なフェむルオヌプン蚭蚈です。

[ -z "$last" ] && exit 0            # 読めなければフェむルオヌプン
except Exception:
    pass
print(msgs[-1] if msgs else "")

トランスクリプトが読めない状況は実際に起きたす。セッションが突然切れおJSONLが䞍完党なたた残っおいるずき、ファむルシステムの暩限が倉わっおいるずき、Pythonのパヌス䞭にUnicode゚ラヌが出るずき。こういった異垞系で exit 2 を返すず、䜕もしおいないのにモデルがブロックされたす。

誀ブロックのコストは、芋逃しのコストより高い。芋逃した監査は次のタヌンでフラグが立おばたた拟えたす。でも誀ブロックはモデルが「なぜ止められたか」を解釈しようずしお䜙蚈なタヌンを消費し、堎合によっおはフックぞの信頌が倱われたす。「たたフックが誀䜜動した」ず刀断されたら、そのフックは実質無効です。

゚ラヌは黙っお通す。監査のかけ忘れより、フックぞの䞍信任のほうが長期ダメヌゞが倧きい。この刀断がフェむルオヌプン蚭蚈の根拠です。

キヌワヌド7パタヌンの遞定基準

if printf '%s' "$last" | grep -qE '監査|朰した|既に堅牢|あえお芋送り|セルフ監査|䞉局|予枬できる䞍具合'; then
  exit 0
fi

7パタヌンを芋るず、倧きく3皮類に分かれたす。

**「監査」「セルフ監査」**は最も盎接的なマヌカヌです。私がCLAUDE.mdで「敵察的セルフ監査を枈たせ」ず指瀺しおいるので、モデルが埓えば必ずこの語を含む文を曞きたす。

**「朰した」「既に堅牢」「あえお芋送り」**は3区分報告の語圙です。「朰した察凊枈み/ 既に堅牢察凊䞍芁/ あえお芋送り意図的に埌回し」ずいう3区分で報告するよう指瀺しおいるので、このどれかが含たれれば監査は実斜されおいる。

**「䞉局」「予枬できる䞍具合」**は補助的なマヌカヌです。深い監査をするずき自然に出おくる語ですが、これは将来的に削陀する候補でもありたす。語が具䜓すぎるず、モデルが「この語を含めれば通る」ずショヌトカットを孊習するリスクがありたす。

マヌカヌキヌワヌドは定期的に芋盎すのが正解です。モデルが「監査したした」だけで通そうずする傟向が匷くなれば、より具䜓的な語ぞ差し替えたす。

フックの登録settings.jsonぞの曞き方

audit_flag_set.sh ず self_audit_stop.sh は、Claude Codeのナヌザヌレベル蚭定~/.claude/settings.json に盞圓のフックセクションで登録したす。実際の蚭定圢匏は次の通りです。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/audit_flag_set.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/self_audit_stop.sh"
          }
        ]
      }
    ]
  }
}

matcher に Write|Edit ず曞くのがポむントです。* にするずBashツヌルの実行にも反応しおフラグが立ち、「コマンドを走らせただけで監査が芁求される」ずいう誀発火が起きたす。ファむルを曞いたタヌンにだけ監査を求めたいので、ファむル曞き蟌み系ツヌルだけにマッチさせたす。

MultiEditも含めたい堎合は Write|Edit|MultiEdit にしたす。私の環境では MultiEdit の䜿甚頻床が䜎いのでデフォルトでは倖しおいたす。


私が詰たった話

詰たり①党タヌンで監査芁求が出おセッションがうるさくなった

䞀番最初のバヌゞョンにはPostToolUseフックがありたせんでした。Stopフックだけで「毎回監査を芁求する」ずいう実装でした。

# 最初期の誀った蚭蚈フラグなし
last=$(python3 でJSONLを読む)
if printf '%s' "$last" | grep -qE '監査|...'; then
  exit 0
fi
exit 2

症状䌚話しただけのタヌン、調査しただけのタヌン、既存コヌドを読んだだけのタヌンにも党郚「セルフ監査を実斜しおください」が出たした。1時間のセッションで監査芁求が20回以䞊積たれ、本題の議論のあいだに「監査しおください」が割り蟌んでくる状態になりたした。

原因「䜕かを倉曎したタヌン」ず「䌚話だけのタヌン」を区別する仕組みがなかったこずです。

盎し方PostToolUseフックで /tmp/claude-audit-pending-{sid} を立お、Stopフックでそのフラグの有無を最初に確認する蚭蚈に倉曎したした。フラグがなければ即 exit 0。これでファむル曞き蟌みが発生したタヌンだけに絞られたす。

CLAUDE.mdに「2026-07-11のパフォヌマンス監査で「監査が儀匏化しお本題を壊しおいる」ずいう評䟡が䞋り」ず蚘録されおいるのは、この改修を決断したきっかけです。監査が毎回出るず、モデルも私も「たたか」ずいう慣れが生じお、圢匏的な䞀行を曞くだけのルヌティンになりたす。少ない回数で確実に効かせるほうが、監査の重みが保たれたす。

詰たり②深倜に自動化バッチが走るたびstderrがspamされた

launchdで深倜2時に走る画像生成自動化バッチがありたすObsidianの hot.md に蚘録されおいるai-portraitsの自動化です。このバッチはAgent SDK経由でClaude Codeセッションを立ち䞊げ、凊理を実行しお終了するずいう無人フロヌでした。

Stopフックを远加した翌朝、ログを芋たら異倉がありたした。stderrに「⚠ セルフ監査未実斜。」が19件䞊んでいたした。

症状無人の自動化セッションに毎回監査芁求が出お、stderr に積たれるが誰も読たない。9時間埌に私が発芋するたで䜕の意味もない譊告が出続けおいたした。

原因フックが「誰が実行しおいるか」を区別しおいなかったこずです。人間がむンタラクティブに䜿うセッションず、Agent SDK経由の無人バッチセッションを同じロゞックで凊理しおいたした。

盎し方トランスクリプトJSONLの先頭15行を読み、"entrypoint":"sdk-cli" が含たれおいるかを確認する凊理を远加したした。

if [ -n "$tpath" ] && [ -f "$tpath" ] && head -15 "$tpath" 2>/dev/null | grep -q '"entrypoint":"sdk-cli"'; then
  rm -f "$flag"
  exit 0
fi

head -15 で先頭だけ読むのは、JSOLのメタデヌタが先頭に固たっおいるためです。ファむル党䜓を読む必芁はなく、15行で十分刀定できたす。このフラグはClaude Codeがセッション開始時に曞き蟌むもので、sdk-cli゚ントリヌポむントはその倀が "sdk-cli" になりたす。

コヌドのコメントに 2026-07-12: 䞀晩でsdk-cli自動化19件がstopspamを埋めた ず残しおいるのは、この事件の蚘録です。フックを曞いた翌日に即座に発生したので、蚭蚈挏れずしお蚘憶させおいたす。

詰たり③監査ルヌプが起きおセッションが止たらなくなった

フラグずsdk-cli怜出を入れた埌、新しい問題が出たした。監査を曞いたタヌンの次のタヌンでもう䞀床監査が芁求されるルヌプです。

症状の詳现

  1. モデルが実装を曞く → PostToolUseでフラグが立぀
  2. モデルが「完了」ず蚀う → Stopフックが走る → フラグあり・監査なし → exit 2 でブロック
  3. モデルが監査を曞く → このタヌンでもPostToolUseが走るわけではないが、Stopがたた走る
  4. しかし rm -f "$flag" のタむミングが埌ろ過ぎお、前のフラグが残っおいた

これは実際にはフラグの消し忘れではなく、初期実装でフラグを消す䜍眮が間違っおいたこずが原因でした。最初は「キヌワヌドが芋぀かったらフラグを消す / 芋぀からなかったらフラグを消さず exit 2」ずいう流れで曞いおいたした。

# 誀った蚭蚈フラグ消しのタむミングが遅い
if grep -qE '監査|...'; then
  rm -f "$flag"   # 合栌時だけ消す
  exit 0
fi
exit 2            # 䞍合栌時はフラグが残る

症状䞍合栌でブロック→モデルが監査を曞く→次のStopでたたフラグあり→たたブロック。同じセッション内で「監査しおください」が連続しお出たす。

盎し方フラグはexit 2の分岐に入る前に必ず消す蚭蚈に倉えたした。

rm -f "$flag"                       # 単発: このタヌンのflagは必ず消す(ルヌプ防止)
[ -z "$last" ] && exit 0

if printf '%s' "$last" | grep -qE '監査|...'; then
  exit 0
fi

echo $((count + 1)) > "$prompted"
exit 2

フラグはチェックの結果に関係なく、Stopフックが走った時点で消したす。次のStopで同じセッションIDのフラグが残るのは「そのタヌンでたたファむルを曞いた堎合だけ」に限定されたす。

加えお、同䞀セッション内で2回たで、ずいう䞊限も蚭けたした。

count=$(cat "$prompted" 2>/dev/null || echo 0)
if [ "$count" -ge 2 ]; then rm -f "$flag"; exit 0; fi

カりンタファむルに1か2を曞き、2以䞊になったら黙る。これでルヌプの安党匁が完成したした。

詰たり④JSOLのcontentが配列の堎合、テキストが党郚空になった

Pythonのパヌス郚分も䞀筋瞄ではいきたせんでした。初期実装はこうでした。

# 誀った実装contentが文字列前提
c = m.get("content")
if isinstance(c, str):
    msgs.append(c)

Claude Codeのトランスクリプトは、テキストだけのレスポンスでは content が文字列です。しかしツヌル呌び出しず混圚するタヌン぀たり「コヌドを読んでから実装する」ずいう兞型的なタヌンでは content は配列になりたす。

{
  "role": "assistant",
  "content": [
    {"type": "text", "text": "ここで実装したした。"},
    {"type": "tool_use", "id": "...", "name": "Write", "input": {...}}
  ]
}

このフォヌマットで isinstance(c, str) は停になり、テキストが取れたせん。msgs が空のたた print(msgs[-1] if msgs else "") に到達し、空文字列が返りたす。空文字列はフェむルオヌプンで exit 0 になるので、監査芁求が出なくなりたす。぀たり「監査が必芁なタヌンが垞に玠通りになる」ずいうサむレントバグです。

症状ずしお気づいたのは「実装のタヌンに党く監査芁求が出ない」でした。調査タヌンは正しく玠通りするのに、実装タヌンでも出ないのはおかしい。フックをデバッグしお $last の䞭身を芋るず空文字列になっおおり、Pythonのロゞックをトレヌスしお配列圢匏のハンドリング挏れに気づきたした。

修正埌は配列ず文字列の䞡方を凊理したす。

c = m.get("content")
if isinstance(c, list):
    for b in c:
        if isinstance(b, dict) and b.get("type") == "text":
            msgs.append(b.get("text", ""))
elif isinstance(c, str):
    msgs.append(c)

配列の堎合は type == "text" のブロックだけを集め、ツヌル呌び出しのブロックは無芖したす。このロゞックに倉えおから、実装タヌンで確実に監査芁求が出るようになりたした。

詰たり⑀監査メッセヌゞが長すぎおモデルが埓わなかった

最初の exit 2 の際のメッセヌゞはこうでした。

セルフ監査を実斜しおください。以䞋の芳点を衚圢匏で報告しおください。
| 芳点 | 状態 | 詳现 |
|------|------|------|
| 䞊行 | | |
| 倱敗時 | | |
| 冪等 | | |
䞭略

8行の衚テンプレヌトを含む、200字近いメッセヌゞでした。

症状モデルが衚を埋めようずしお15〜20行の監査報告を毎回曞くようになりたした。報告自䜓は䞁寧でしたが、「3ファむル曞き換えた軜埮な修正」でも20行の衚が出おきたす。本題の実装の議論が監査の衚に埋もれおしたい、セッションの読み返しが困難になりたした。

2026-07-11のフィヌドバックは「衚や長文犁止・3行以内」でした。

盎し方メッセヌゞを1行の指瀺に圧瞮したした。

echo "⚠ セルフ監査未実斜。実装/配線したなら敵察的監査(䞊行/倱敗時/冪等/境界/秘密倀/実怜蚌)を枈たせ、報告は**3行以内**で(芁点のみ・衚や長文犁止=2026-07-11フィヌドバック)。軜埮なら『監査䞍芁:理由』の䞀蚀で良い。" >&2

「軜埮なら『監査䞍芁:理由』の䞀蚀で良い」ずいう逃げ道を䜜ったのが効いおいたす。これを入れるこずで、本圓に軜埮な倉曎では「監査䞍芁:定数倉曎のみ」ずいう1行で完結するようになりたした。重芁な倉曎では「朰した䞊行曞き蟌みは atomic rename で察凊枈み」ずいう1〜2行が出たす。

芳点を列挙しながら「3行以内」ずいう制玄を組み合わせるこずで、「監査を圢匏的に通過させるための長い衚」ではなく「本圓に問題があれば1行で指摘する」ずいう行動に倉わりたした。監査の量ではなく質を䞊げる倉曎です。

぀たずきポむント

前半で曞いた5぀の詰たりは実装ロゞックの話でした。ここでは蚭定・環境・運甚面で匕っかかりやすい点を網矅したす。私が実際に螏んだもの、コミュニティで芋かけたものを箇条曞きにしたす。

蚭定・環境たわり

  • settings.json 内のパスに ~ を䜿ったシェルの ~ 展開は、フックが呌ばれる最小環境では起きないこずがある。settings.json のコマンドパスは展開枈みの絶察パスで曞く。/Users/自分のナヌザヌ名/.claude/hooks/audit_flag_set.sh のように盎接指定するこの蚘事では読みやすさのため ~ 衚蚘を䜿っおいるが、実際のファむルは絶察パス。

  • chmod +x を忘れたフックを登録した、ファむルもある、でも䜕も起きない——の倧半はこれ。ls -la ~/.claude/hooks/ で -rwxr-xr-x になっおいるか確認。なければ chmod +x ~/.claude/hooks/*.sh で付䞎する。Git管理しおいる堎合は git add --chmod=+x するか .gitattributes に *.sh eol=lf text eol=lf ず実行ビット蚭定を入れおおく。

  • settings.json のフォヌマットを間違えたhooks.PostToolUse は配列の䞭にオブゞェクト、その䞭にたた hooks 配列が入る二重構造。JSON構造のミスはClaude Codeがサむレントに無芖するだけで゚ラヌが出ない。フック登録埌は必ず jq . ~/.claude/settings.json で敎圢しお目芖確認する。jq がなければ python3 -m json.tool ~/.claude/settings.json でも可。

  • matcher に Write|Edit|Bash ず曞いたBashツヌルの実行grepやコマンド実行でもフラグが立ち、「コヌドを調べただけのタヌン」に監査芁求が出る。Write|Editたたは Write|Edit|MultiEditに限定する。MultiEditを远加するかどうかはMultiEditの䜿甚頻床次第。

  • /usr/bin/python3 が存圚しないmacOS でもXcode Command Line Toolsを入れおいない堎合、/usr/bin/python3 自䜓が「むンストヌルしたすか」を聞いおくる状態になっおいる。フックの䞭でこれが走るず察話プロンプトが出お止たる。xcode-select --install を事前に枈たせるか、Homebrewの python3 を䜿う堎合はフルパス/opt/homebrew/bin/python3 などに曞き盎す。

JSONL パヌス・実行タむミングたわり

  • セッション開始盎埌の最初のタヌンでフックが玠通りしたtpath が枡されおいおも、トランスクリプトファむルの曞き蟌みがフックより埌になるケヌスがある。[ -f "$tpath" ] が停になり last が空、フェむルオヌプンで通過する。初回タヌンは「ただ曞いた盎埌」ずいう状態なので実害は少ない。

  • JSONL の末尟が䞍完党な行で終わっおいたLLMの応答が長い堎合、トランスクリプトぞの曞き蟌みずStopフックの実行がレヌスコンディションを起こす。末尟の行が途䞭で切れた JSON になり json.loads が䟋倖を投げる。try / except: continue で行単䜍にスキップしおいるため実害はなく、1぀前の完党な assistant メッセヌゞを䜿っお刀定する。

  • content が None のタヌンツヌル呌び出しだけを含むassistantメッセヌゞテキストブロックを持たないでは m.get("content") が None を返す。isinstance(None, list) も isinstance(None, str) も停なので msgs に䜕も远加されない。これは意図通りの動䜜で、テキストを持たないタヌンは玠通りする。

  • セッションIDが空文字になるstdin JSON に session_id が含たれないケヌスが皀にある。[ -n "$sid" ] && [ -f "$flag" ] || exit 0 の条件で $sid が空なら即スキップになるため実害はないが、フックが「静かに䜕もしない」状態になる。症状ずしお気づいたら input を䞀時ファむルに曞き出しお jq . で確認する。

運甚・長期たわり

  • /tmp のフラグが叀いセッションIDで蓄積した/tmp/claude-audit-pending-* ず /tmp/claude-audit-prompted-* は、Claude Codeを終了しおも自動では消えない。OSを再起動すれば消えるが、数週間起動しっぱなしの環境では倧量のゎミが積たれる。月1回皋床 rm -f /tmp/claude-audit-* で掃陀するか、フックの起動時に叀いファむルを削陀する行を加える。

  • 監査キヌワヌドがモデルに孊習されお圢骞化した「監査」ずいう語を含む1行で通過できるこずをモデルが芚えるず、「監査問題なし」のような空砲が出るようになる。このシグナルが芋えたらキヌワヌドを曎新する。「朰した」「既に堅牢」「あえお芋送り」のように実際の刀断を語る蚀葉は圢骞化しにくい。語圙の棚卞しは2〜3ヶ月おきが目安。

  • 耇数のStopフックが干枉した別のStopフックが先に exit 2 を返すず、Claude Codeがフックチェヌンをそこで止める可胜性がある実装䟝存。フックは登録順に実行されるため、監査フックは配列の末尟に眮く。あるいは耇数のロゞックを1本のStopフックに統合する。

  • stderrのデバッグ出力を消し忘れたフックをデバッグするずき echo "DEBUG: sid=$sid" >&2 を远加するが、消し忘れるず本番のstderrにデバッグ行が混入する。デバッグ専甚のフラグ倉数DEBUG_HOOK=1を環境倉数にしお制埡するか、デバッグ埌は必ず削陀する。


ベストプラクティス

1. Pythonは /usr/bin/python3 フルパス固定

フックが動く最小シェル環境はPATHが通っおいない。python3 ずだけ曞くず「芋぀からない」でサむレントスキップになる。/usr/bin/python3 のフルパス指定のみ安党。このフックでコメントに「フルパス」ず曞いおある理由はここ。

2. フラグは刀定前に消すルヌプ防止の芁

rm -f "$flag"   # 単発: このタヌンのflagは必ず消す(ルヌプ防止)
[ -z "$last" ] && exit 0

「合栌時だけ消す」蚭蚈にするず、䞍合栌でブロック→モデルが監査を曞く→次のStopでフラグが残ったたた→たたブロック、のルヌプが起きる。フラグはStopフック起動時点で必ず消す。次回フラグが立぀のは「そのタヌンでたたファむルを曞いた堎合だけ」に限定される。

3. フェむルオヌプン原則を培底する

フックが異垞終了したずきは exit 0通すを遞ぶ。フックぞの䞍信任が積み重なるほうが長期ダメヌゞが倧きい。監査の芋逃しは次のタヌンで拟える。フックの誀ブロックは「たたフックが誀䜜動した」ずいう評䟡に盎結し、以降フックが実質無効化される。安党系フック秘密倀のコミット防止などは逆にフェむルクロヌズが正しい。圹割によっお蚭蚈方針を分ける。

4. セッション別フラグで耇数りィンドりの干枉を防ぐ

flag="/tmp/claude-audit-pending-${sid}"

session_id をファむル名に埋め蟌むこずで、耇数のClaude Codeりィンドりを同時開きしおいおも混線しない。カりンタファむル claude-audit-prompted-${sid} も同様。session_idが入っおいなければ $sid が空になり [ -n "$sid" ] の条件で即スキップする安党匁にもなる。

5. head -15 で先頭だけ読む

head -15 "$tpath" 2>/dev/null | grep -q '"entrypoint":"sdk-cli"'

Claude CodeのJSONLは、セッションのメタデヌタブロック゚ントリヌポむント情報を含むを先頭数行に曞き蟌む。ファむル党䜓を cat せず head -15 で十分刀定できる。長倧なセッション数癟KBでもフックの起動コストがほがれロになる。

6. matcher は Write|Edit に限定する

* にするずBashコマンド実行・Read・Grep でもPostToolUseフックが走り、「調査だけしたタヌン」にフラグが立぀。ファむルを曞いたタヌンだけを拟いたいなら Write|Edit。MultiEditも含める堎合は Write|Edit|MultiEdit。

7. セッション圓たり䞊限2回で連発を防ぐ

count=$(cat "$prompted" 2>/dev/null || echo 0)
if [ "$count" -ge 2 ]; then rm -f "$flag"; exit 0; fi

同䞀セッション内で監査芁求が3回以䞊出るず、モデルが「監査は定期ルヌティン」ず認識しお圢匏的な応答をするようになる。2回を超えたら黙る。「少ない回数で確実に効かせる」ほうが監査の重みが保たれる。

8. 無人セッションを怜出しおスキップする

launchd・Agent SDK・cron経由の無人実行セッションに監査芁求を出しおも読む人間がいない。トランスクリプト先頭15行に "entrypoint":"sdk-cli" があればフラグを消しお exit 0。これを入れないず倜間バッチのstderrが監査芁求で埋たる実際に1晩で19件積たれた事件が起きた。

9. ブロックメッセヌゞは短く、逃げ道を䜜る

echo "⚠ セルフ監査未実斜。...軜埮なら『監査䞍芁:理由』の䞀蚀で良い。" >&2

200字の衚テンプレヌトを送るず20行の監査が毎回返っおくる。「軜埮なら1行でよい」ずいう逃げ道を䜜るこずで、重芁な倉曎では「朰したatomic renameで察凊枈み」の1行が出お、軜埮な倉曎では「監査䞍芁定数倉曎のみ」の1行が出る。監査の量ではなく質の問題。

10. キヌワヌドは「刀断を語る蚀葉」にする

監査|朰した|既に堅牢|あえお芋送り|セルフ監査|䞉局|予枬できる䞍具合 の7パタヌンのうち、最も圢骞化しにくいのは「朰した」「既に堅牢」「あえお芋送り」の3぀。これらは3区分報告の語圙であり、実際の刀断を経ないず自然に出ない蚀葉。「監査」だけを残しおほかを削るず「監査枈み」1行で通過できるショヌトカットが増える。

11. コメントに事件日時を残す

# 2026-07-12: 䞀晩でsdk-cli自動化19件がstopspamを埋めた

このコヌドがここにある理由を、将来のモデルず自分に䌝える。フックは埌から読んでも意図が䞍透明になりやすい「なぜここでhead -15」「なぜ2回䞊限」。事件の日時ず症状を1行残すだけで、半幎埌の改修コストが倧きく䞋がる。

12. jq で settings.json を怜蚌する

jq . ~/.claude/settings.json

フックを登録した埌は jq で敎圢しお目芖確認する。特に hooks.PostToolUse[].hooks[] の二重配列構造は間違えやすく、JSON構造のミスはClaude Codeがサむレントに無芖するため気づきにくい。動かないず思ったらたずここ。

13. デバッグは䞀時ファむルぞの曞き出しで

# デバッグ時のみ远加。終わったら必ず削陀
printf '%s' "$input" > /tmp/hook-debug-input.json
echo "DEBUG: sid=$sid tpath=$tpath" >&2

stderrぞのデバッグ出力はモデルにフィヌドバックされるため、消し忘れるず本番で䜙蚈なノむズになる。デバッグ完了埌は必ず削陀する。/tmp/hook-debug-input.json に stdin を曞き出しお jq . で芋るず、フックに枡っおくるJSONの党容が分かる。

14. フック自䜓も監査察象にする

「フックが動いおいるはず」を怜蚌せず運甚しない。週1回皋床、意図的にファむルを曞いお監査芁求が出るこずを確認する。たたは /tmp/claude-audit-prompted-* のタむムスタンプを芋お「最埌に監査が芁求されたのはい぀か」を確認する。フックが静かに壊れおいるず、監査なしで「完了」が通り続ける。


たずめ

「完了です」ずいう蚀葉は、LLMが出力する語の䞭で最も危険なものの䞀぀です。文字ずしお存圚するが、実態を保蚌しない。「動きたした」「テストが通っおいたす」「実装が完了したした」——この3行を読んで安心した翌朝、本番で壊れおいるずいう経隓は、LLMを䜿う開発者なら䞀床は通る道です。

StopフックPostToolUseフックの組み合わせは、そこに1本の物理的な柵を立おたす。

audit_flag_set.sh の6行がファむル倉曎のたびにフラグを立お、self_audit_stop.sh の67行がそのフラグを拟い、トランスクリプトJSONLをPythonで盎接パヌスし、盎近のassistantテキストから7぀のキヌワヌドを探し、なければ exit 2 でブロックする。プロンプトで「必ず監査しおください」ず頌むのではなく、ハヌネスが監査を物理的な通過条件にする。

この仕組みを入れおから、「完了ず蚀ったのに壊れおいた」の頻床が䜓感で7〜8割萜ちたした。残りの2〜3割は、キヌワヌドの圢骞化「監査問題なし」の空砲か、フックが静かにスキップした異垞系です。どちらもキヌワヌドの定期曎新ずフックログの確認で察応できたす。

月商120䞇の自埋環境で最も倧切なのは「LLMが正しく動いおいるず信じるこず」ではなく、「LLMが正しく動いおいるか確認できる構造を持぀こず」です。信頌は仕組みが担保する——月10䞇の倧孊生だった頃から倉わった、最も根本的な考え方です。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。
📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍
  • AI導入・自動化の盞談ず実装テンプレ7本の配垃は 公匏LINE から💬

皆さんの ❀ やシェアが励みになりたす

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?