← 기사 목록
日本語https://zenn.dev/topics/ai/feed

Claude Code hook で AI coding assistant の規律を補強する — 個人運用での設計パターン参考

추출된 키워드

30
AI coding assistant·5hook·5Claude Code·5memory rule·4PreToolUse·4PostToolUse·4PostToolUseFailure·4Stop·4deploy polling·3stdin·3transcript_path·3settings.json·3secret token·3git push·3Bash·3dev workflow·3LLM coding assistant·3jq·2JSONL·2gh api·2awk·2ECONNRESET·2TimeoutError·2MCP error·2env variable·2JWT·2pip install·2git clone·2npm run build·2context window·2

원문

10,016
Claude Code hook で AI coding assistant の規律を補強する — 個人運用での設計パターン参考

Claude Code hook で AI coding assistant の規律を補強する — 個人運用での設計パターン参考

はじめに

LLM coding assistant (Claude Code 等) に dev workflow を任せていると、 memory file の「注意書き」 だけだと同じハマりを繰り返すと感じることがあります。

Claude Code には Stop / PreToolUse / PostToolUse / PostToolUseFailure の hook 機構があり、 これを使うと「LLM が忘れても hook が exit 2 で block + stderr message を流し込む」 という形で規律を補強できます。

本稿は私が個人で運用している hook の 設計パターン + 一般的に有用そうな例 をまとめたものです。 環境や好みで合う合わないがあるので、 あくまで参考レベルとして読んでください。

私が memory rule だけだと不十分だと感じた経緯

LLM assistant に build / deploy / git push 等を任せると、 こういう anti-pattern が起きました:

  • git push
    した直後に end-turn して deploy fail に気付かない
  • Tool が internal error で fail しても silent retry してしまう
  • Bash command を
    &&
    で連結すると permission prompt 暴発で silent-stall するのに、 つい連結してしまう
  • secret token を含む値を cmd に書き込んでしまう

memory file の「注意書き」 を入れても、 別 turn / 別 session で普通に忘れる感覚がありました。 LLM は context window と注意のばらつきで規律を維持しにくいので、 強制可能なものは hook で補強した方が私は楽だと感じています。

Claude Code hook の四層

eventtiming用途
PreToolUse tool 実行前危険 cmd の予防
PostToolUse tool 実行後 (成功)出力の後処理・notification
PostToolUseFailure tool 実行後 (失敗)tool error の silent retry 防止
Stop turn 終了前end-turn 規律の最終 check

私が便宜的に分けている役割:

  • PreToolUse = 火事を起こす前に止める(予防)
  • PostToolUseFailure = 火事の煙を user から隠さない(transparency)
  • Stop = 火事の鎮火を確認してから帰る(closure)

実例 (一般的に有用そうな 7 個)

PreToolUse 系

1. Bash 連結 cmd 警告

&&
/
;
/
||
の連結を block。 連結すると permission prompt 暴発で silent-stall が起きやすいので私は分けるようにしました。
#!/usr/bin/env bash
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')

if echo "$cmd" | grep -qE ' (&&|;|\|\|) '; then
    echo "[hook:no-bash-compound] cmd 連結は分けて実行を推奨" >&2
    exit 2
fi
exit 0

2. 長時間 cmd の timeout 明示

npm run build
/
git clone
等で timeout 指定が無い場合 block。 silent-stall 予防。
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
timeout_ms=$(printf '%s' "$input" | jq -r '.tool_input.timeout // 0')

if echo "$cmd" | grep -qE 'npm (run|install|ci|build)|git clone|pip install'; then
    if [ "$timeout_ms" = "0" ] || [ "$timeout_ms" = "null" ]; then
        echo "[hook:require-timeout] 長時間 cmd は timeout 明示推奨 (例: 600000ms = 10 分)" >&2
        exit 2
    fi
fi
exit 0

3. secret token literal の cmd 書き込み

sk-
,
gho_
,
AKIA
, JWT などのパターンを cmd 内で検出 → block。 env variable 経由を推奨。
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')

if echo "$cmd" | grep -qE '\b(sk-[a-zA-Z0-9]{20,}|gho_[a-zA-Z0-9]{20,}|AKIA[A-Z0-9]{16}|eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\b'; then
    echo "[hook:no-token-literal] secret literal を cmd に直書きは避ける、 env variable 推奨" >&2
    exit 2
fi
exit 0

4. 巨大 file の offset 無し Read — 1MB 超 file を

offset
/
limit
無しで Read tool に渡すと context が bloat するため block。
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""')
offset=$(printf '%s' "$input" | jq -r '.tool_input.offset // 0')

if [ -f "$file_path" ]; then
    size=$(stat -c%s "$file_path" 2>/dev/null || echo 0)
    if [ "$size" -gt 1048576 ] && [ "$offset" = "0" ]; then
        echo "[hook:no-huge-read] 1MB 超 file は offset+limit 指定推奨" >&2
        exit 2
    fi
fi
exit 0

PostToolUseFailure 系

5. Tool failure を surface — tool が runtime error で fail した直後、 silent retry を block。

echo "[hook:tool-failure] Tool failed at runtime." >&2
echo "Report the failure and propose a retry path before continuing." >&2
exit 2

Stop 系

6. Unhandled tool error で end-turn block — transcript の tail から直近の tool error envelope を検出、 後続 assistant text に retry/error/失敗 vocabulary が無ければ block。

#!/usr/bin/env bash
input=$(cat)
transcript=$(printf '%s' "$input" | jq -r '.transcript_path // ""')
[ -z "$transcript" ] && exit 0
[ ! -f "$transcript" ] && exit 0

tail_json=$(tail -100 "$transcript")

err_idx=$(printf '%s\n' "$tail_json" | awk '
  /"type":"assistant"/ { next }
  /\[hook:/ { next }
  /Tool result missing/ ||
  /MCP error/ ||
  /TimeoutError/ ||
  /ECONNRESET/ { last=NR }
  END { if (last) print last }
')

[ -z "$err_idx" ] && exit 0

later=$(printf '%s\n' "$tail_json" | tail -n +"$((err_idx + 1))")

if ! printf '%s' "$later" | grep -qiE 'retry|失敗|エラー|error|timeout|タイムアウト'; then
    echo "[hook:unhandled-tool-error] Tool error 後に ack 無しで end-turn しないこと" >&2
    exit 2
fi
exit 0

7. push 後の deploy polling

git push origin main
検出後、 後続 assistant text に deploy polling vocabulary が無ければ block。 これは私が今回作って一番効いた hook です。
tail_json=$(tail -200 "$transcript")

push_idx=$(printf '%s\n' "$tail_json" | awk '
  /"type":"assistant"/ { next }
  /\[hook:/ { next }
  /main -> main/ { last=NR }
  END { if (last) print last }
')
[ -z "$push_idx" ] && exit 0

later=$(printf '%s\n' "$tail_json" | tail -n +"$((push_idx + 1))")

if ! printf '%s' "$later" | grep -qiE 'deploy[_ ]?state|polling|=success|=failure|gh api.*deployments'; then
    echo "[hook:deploy-not-polled] push 後 deploy state polling 推奨" >&2
    echo "対処: gh api deployments?sha=<full> --jq .[0].id → deployments/<id>/statuses --jq .[0].state" >&2
    exit 2
fi
exit 0

設計パターン

stdin で context 受け取り

Claude Code hook は stdin に JSON を渡してきます。 主要 field:

  • .tool_input.command
    (Bash tool の場合)
  • .tool_input.file_path
    (Read/Write/Edit の場合)
  • .transcript_path
    (Stop hook の場合、 当 session の JSONL transcript path)
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')

transcript scan で文脈確認

Stop hook では

transcript_path
の JSONL を tail scan して直近の context を確認できます:
tail_json=$(tail -200 "$transcript")

assistant 自身の説明文を scan から除外しないと、 誤検出します (assistant が「git push しました」 と書いた text を「push が行われた」 と読んで block してしまう):

awk '
  /"type":"assistant"/ { next }
  /\[hook:/ { next }
  /main -> main/ { last=NR }
  END { if (last) print last }
'

exit code 規約

  • exit 0
    = passthrough (規律違反なし)
  • exit 2
    = block + stderr が LLM の次 turn input に reminder として挿入される

stderr に書く message は LLM が次 turn で読むので、 「何が違反か + 対処方法」 を 2-3 行で書く と効きやすい印象です。

settings.json への登録

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {"type": "command", "command": "bash '~/.claude/hooks/no-bash-compound.sh'"},
          {"type": "command", "command": "bash '~/.claude/hooks/no-token-literal.sh'"}
        ]
      },
      {
        "matcher": "Read",
        "hooks": [
          {"type": "command", "command": "bash '~/.claude/hooks/no-huge-read.sh'"}
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {"type": "command", "command": "bash '~/.claude/hooks/unhandled-tool-error.sh'"},
          {"type": "command", "command": "bash '~/.claude/hooks/deploy-not-polled.sh'"}
        ]
      }
    ]
  }
}

私の運用 = 漸進的に拡張

最初から全部揃える必要はないと感じていて、 ハマるたびに 1 個追加しています。 判断の目安:

  • memory rule で済む: context-dependent な judgement (revert すべきか、 commit を細粒化すべきか等)
  • hook 化したい: 強制可能な pattern (連結 cmd、 token literal、 push 後 polling 等)

「強制可能な規律は強制する、 memory rule で誤魔化さない」 を私は自分の指針にしていますが、 環境によって判断は変わると思います。

まとめ

memory rule (注意書き) だけだと私は規律を維持しにくく、 強制可能な規律は Claude Code hook で補強しています:

  • PreToolUse: 危険 cmd を起こす前に止める
  • PostToolUseFailure: tool error を user から隠さない
  • Stop: end-turn 前の規律 violation を最終 check

hook 開発は漸進的でいいと感じています。 memory rule + hook の二段階設計が私には合っていました。

あくまで個人の運用例ですので、 参考になりそうな部分だけ拾っていただければ。

GitHubで編集を提案