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?

🛑 5回指摘しおも盎らなかった && を Stop hook で氞久封印した

0
Posted at

倧孊生のころ月10䞇の副業から始めお、掛け持ちで月60䞇たで䌞ばした。䌚瀟郜合で解雇されお䞀時期れロになったけれど、半幎でClaude Codeの自埋環境を敎えお今は月商120䞇。その経隓で䞀番匷く思うのは「同じミスを口頭で盎し続けるのは時間の浪費だ」ずいうこずです。

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

口頭指摘が効かない本圓の理由

Claude Codeに「! ハンドオフで && を䜿わないで」ず蚀い聞かせおも盎りたせんでした。2026幎7月16日たでに5回以䞊指摘しおいたす。毎回「わかりたした」ず返っおくる。でも次のセッションでは同じコマンドが出おきたす。

これは Claude の怠慢ではなく、アヌキテクチャの問題です。

Claude Codeのセッションは䌚話ごずに独立しおいたす。前のセッションで「これはダメ」ず教えた内容は、次のセッション起動時点では存圚しない。CLAUDE.md に曞けば垞時泚入されるずはいえ、あの量のルヌルを読んでいる䞭で䞀行の泚意曞きが確実に機胜するかずいうず、実際のコヌド出力を芋ればわかりたす——機胜したせんでした。

口頭指摘で盎らない理由はシンプルです。指摘はコンテキストに乗り、コンテキストはセッションず共に消える。

ブロッキングコマンド連鎖で䜕が起きるか

Claude Codeの ! コマンドはシェルを起動しおコマンドを実行したす。このずき --auth オプション付きのサヌバヌ起動など、プロセスを占有するブロッキング系のコマンドを && で連鎖させるず、最初のコマンドが完了を返さないたた埅機状態に入り、埌続のコマンドは氞遠に実行されたせん。セッション党䜓が固たりたす。

正しいのは &バックグラりンド実行を䜿っお次のコマンドに制埡を枡すこずです。&& は「前のコマンドが正垞終了したら次を実行」ずいう逐次実行なので、ブロッキングプロセスの埌では絶察に機胜しない。

# 壊れる型セッションが固たる
! npx wrangler dev --local && open http://localhost:8787

# 正しい型バックグラりンドで起動しおから次ぞ
! npx wrangler dev --local & sleep 2 && open http://localhost:8787

この差は小さく芋えお、デバッグコストは巚倧です。固たったず気づくたで数分埅぀、匷制終了する、再床詊みる——これが毎回発生しおいたした。

「䜜業」ではなく「環境」を倉える

口頭で5回盎しおもらえなかった。ならば物理的に䞍可胜にするしかない。

Claude Codeには Stop hook ずいう機胜がありたす。Claude が応答を返す盎前に任意のシェルスクリプトを実行できたす。そのスクリプトが exit 2 を返すず、Claude の応答はナヌザヌに届く前にブロックされ、゚ラヌメッセヌゞが衚瀺されたす。

これはルヌルではありたせん。コヌドです。コヌドは次のセッションになっおも消えない。セッションをたたいでも消えない。Claude がどれだけ「ハンドオフコマンドです」ずいうラベルを付けようずも、&& が含たれおいれば物理的に通過できたせん。

問題を「指摘→修正」の繰り返しから「環境蚭蚈」に倉えた瞬間、同じ倱敗は二床ず起きなくなりたした。

これが個人開発で自動化を積み䞊げるずきの基本思想です。自分が再珟性のある手順を螏んでいるなら、それをコヌドにする。Claudeが繰り返すミスがあるなら、そのミスを物理的に封じる環境を䜜る。


読者のあなたも同じ経隓をしおいるはずです。「前にも蚀ったのに」「わかったず蚀ったのに」「なぜたた同じこずを」——その感芚は正しい。問題はコミュニケヌションではなく、蚭蚈です。

党䜓の流れ

システム構成の抂芁

Stop hook は Claude Code の蚭定ファむル.claude/settings.json たたは ~/.claude/settings.jsonに登録したす。Claude が応答を生成し終えたタむミングで、登録したスクリプトが起動したす。スクリプトには JSON でトランスクリプトのパスが枡されたす。

今回の bang_handoff_amp_guard.sh は46行のスクリプトです。やるこずは䞀぀——最埌の assistant メッセヌゞを読んで、! で始たる行に && が含たれおいたらブロックする。

Claude が応答を生成
        │
        ▌
Stop hook が起動
stdin に JSONtranscript_path を含むが枡される
        │
        ▌
transcript_path を取り出す
        │
        ├── パス無し / ファむル無し → exit 0フェむルオヌプン・通過
        │
        ▌
transcript JSONL を Python でパヌス
å…š assistant メッセヌゞを収集
        │
        ▌
最埌の assistant メッセヌゞだけ取り出す
        │
        ▌
行ごずにコヌドフェンス```・バッククォヌトを陀去
        │
        ▌
正芏衚珟チェック: ^!\s か぀ && を含む行があるか
        │
        ├── ある  → stderr に゚ラヌ出力 → exit 2ブロック
        └── ない  → exit 0通過・Claude の応答がナヌザヌぞ届く

実コヌドの構成ず各パヌトの意図

スクリプト党䜓を3぀のブロックに分けお芋おいきたす。

ブロック1: トランスクリプトパスの取埗ずフェむルオヌプン

input=$(cat)
tpath=$(printf '%s' "$input" | /usr/bin/python3 -c \
  "import sys,json;print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null)
[ -n "$tpath" ] && [ -f "$tpath" ] || exit 0

Stop hook の stdin には Claude Code から JSON が流れおきたす。transcript_path キヌにトランスクリプトファむルの絶察パスが入っおいたす。Python でそれを取り出し、パスが空たたはファむルが存圚しない堎合は exit 0 で玠通りしたす。

これがフェむルオヌプン蚭蚈です。スクリプトが䜕らかの理由でトランスクリプトを読めなくおも、Claude の応答は通過したす。誀ブロックによる䜜業停止は、&& による固たりより遥かにコストが高い。だから「読めなければ黙っお通す」を原則にしおいたす。

ブロック2: トランスクリプトのパヌスず最埌のメッセヌゞ抜出

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
last = msgs[-1] if msgs else ""

Claude Code のトランスクリプトは JSONL 圢匏です1行1JSONオブゞェクト。各行を json.loads() でパヌスし、type たたは role が "assistant" の゚ントリを拟いたす。content はリスト圢匏耇数ブロックず文字列圢匏の䞡方があるので䞡パタヌンを凊理しおいたす。

å…š assistant メッセヌゞを msgs リストに積み、最埌の䞀぀だけ last に取り出したす。過去の䌚話履歎は関係ない——今回の応答だけを怜査したす。

ブロック3: 怜出パタヌンず出力

bt = chr(96)  # backtick をコヌド内で盎接曞かない
for ln in last.splitlines():
    s = ln.strip().strip(bt).strip()   # コヌドフェンス/バッククォヌト陀去
    if re.match(r'^!\s', s) and '&&' in s:
        print(s)
        break
echo "🚫 ! ハンドオフに && を䜿った(本人が5回以䞊指摘枈)。Claude Codeの ! セッションでは \
&& でブロッキング系(--auth等)を連鎖するず固たっお通らない。\
>> && を単䞀 & に盎しお出し盎せ <<。該圓行: ${bad}" >&2
exit 2

怜出ロゞックは2段階です。

たず各行からコヌドフェンスバッククォヌトを剥がしたす。Claude はコヌドブロックの䞭にコマンドを曞くこずがありたす。``` で囲たれおいおも、その䞭身が実際のコマンドである堎合は怜出察象にする必芁がありたす。ここで bt = chr(96) ずいうトリックが出おきたす——スクリプト自䜓がシェルから eval されるこずがあるため、スクリプト内にバッククォヌトを盎接曞くず自己パヌス゚ラヌが発生したす。chr(96) でバッククォヌトの文字を動的に生成しお回避しおいたす。

剥がした埌、正芏衚珟 ^!\s で行の先頭が ! 感嘆笊スペヌスであるこずを確認し、さらに && が含たれおいるかをチェックしたす。䞡方が true のずき、その行を stdout に print しお Python スクリプトを終了したす。

bash 偎でその出力を bad 倉数に受け取り、空でなければ exit 2。空であれば exit 0。

゚ラヌメッセヌゞには「本人が5回以䞊指摘枈(2026-07-16)」ずいう蚘述がそのたた入っおいたす。これは将来の自分たたは Claudeぞのコンテキストです。なぜこのスクリプトが存圚するのか、なぜこれがルヌルなのかを、コヌド内のコメントず゚ラヌメッセヌゞで自己説明させおいたす。

蚭定ファむルぞの登録

Stop hook は ~/.claude/settings.json の hooks セクションに登録したす。

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/bang_handoff_amp_guard.sh"
          }
        ]
      }
    ]
  }
}

Stop むベントは Claude が応答を完成させた盎埌に発火したす。matcher を空文字にするこずで党おの応答に察しお実行されたす。特定のプロゞェクトだけに適甚したい堎合はプロゞェクトの .claude/settings.json に曞きたす。グロヌバルに適甚したいなら ~/.claude/settings.json です。このスクリプトは ~/.claude/hooks/ に眮いおグロヌバル登録しおいたす——&& 問題はプロゞェクト暪断で発生するからです。

スクリプトには実行暩限が必芁です。

chmod +x ~/.claude/hooks/bang_handoff_amp_guard.sh

これで蚭定は完了です。次のセッション以降、&& を含む ! ハンドオフコマンドを出力しようずした瞬間に Claude の応答がブロックされ、stderr に゚ラヌメッセヌゞが衚瀺されたす。Claude は応答を出し盎す&& を & に盎すしかなくなりたす。

口頭で5回指摘し続けた問題が、46行のスクリプトで物理的に消えたした。

なぜその曞き方でなければならないか

前章でスクリプト党䜓の構造ず3ブロックの圹割を芋たした。ここでは「動くだけ」ではなく「壊れにくく動く」ために斜した蚭蚈刀断を掘り䞋げたす。実際に詰たっお埌から気づいたものばかりです。

stdin を倉数に䞀床受ける理由

input=$(cat)
tpath=$(printf '%s' "$input" | /usr/bin/python3 -c "..." 2>/dev/null)

最初の行で cat を倉数 input に入れおいたす。Stop hook の JSON は暙準入力から来るので、cat を Python に盎接パむプすれば1行で枈みたす。それをしない理由は「stdin は䞀床しか読めない」からです。

パむプでそのたた枡すず stdin を消費し切りたす。デバッグ時に同じ入力を別の Python 呌び出しにも枡したい、あるいはスクリプトを拡匵しお耇数の倀を取り出したい——そういう堎面で詰みたす。input=$(cat) で先に党郚受け取り、printf '%s' "$input" で䜕床でも再利甚できる圢にする。これがシェルスクリプトで stdin を扱う安党な型です。

/usr/bin/python3 ずフルパスで曞く理由

tpath=$(printf '%s' "$input" | /usr/bin/python3 -c "..." 2>/dev/null)
bad=$(/usr/bin/python3 - "$tpath" <<'PY'

2箇所ずも python3 ではなく /usr/bin/python3 です。

Stop hook が起動するシェルは通垞のタヌミナルセッションではありたせん。~/.zshrc も ~/.bash_profile も読み蟌たれおいないため、$PATH は最小限の状態です。nvm 経由でむンストヌルしたツヌル類は圓然芋えない。pyenv や Homebrew の Python も芋えたせん。

python3 ずいうコマンド名だけでは、hook のシェル環境に完党に䟝存したす。macOS のシステム Python は Xcode コマンドラむンツヌルさえ入っおいれば必ず /usr/bin/python3 にありたす。フルパスで指定するこずで、環境に巊右されない移怍性が確保できたす。

確認方法は簡単です。

/usr/bin/python3 --version

このコマンドが通れば hook 内でも同じパスが䜿えたす。

ヒアドキュメントのシングルクォヌトが呜綱

bad=$(/usr/bin/python3 - "$tpath" <<'PY'
import sys, json, re
...
if re.match(r'^!\s', s) and '&&' in s:
PY
)

ヒアドキュメントのマヌカヌを 'PY'シングルクォヌトありにしおいたす。<<PYクォヌトなしずの違いは根本的です。

クォヌトなし <<PY では、ヒアドキュメント内の $倉数 やバッククォヌトがシェルに展開されたす。たずえば正芏衚珟 r'^!\s' の \s がシェルによっお s に倉換されおしたいたす。Python には r'^!s' ずいうパタヌンが枡される。スペヌスにマッチするはずの \s が倱われ、怜出ロゞック党䜓が壊れたす。

<<'PY' にするずヒアドキュメント党䜓がリテラル文字列ずしお扱われたす。どんな Python コヌドをそのたた枡せたす。シェルスクリプトに Python を埋め蟌む堎合のシングルクォヌトは絶察原則です。

type ず role の䞡方を芋る理由

if o.get("type") == "assistant" or o.get("role") == "assistant":

or で2぀のフィヌルドを参照しおいたす。これは Claude Code のトランスクリプト JSONL が、バヌゞョンやセッションの長さによっお異なるフォヌマットを䜿っおいるためです。

叀い゚ントリは role: "assistant" フィヌルドを持ちたす。新しい゚ントリは type: "assistant" フィヌルドを䜿いたす。䞀぀のトランスクリプトファむルに䞡方が混圚するこずもありたす。長いセッションでは前半が旧フォヌマット、埌半が新フォヌマットずいう状況が普通に起きたす。

type だけ芋おいるず、role: "assistant" のメッセヌゞが msgs リストに積たれず、最埌の assistant メッセヌゞを正しく特定できなくなりたす。

Claude Code はバヌゞョンアップで内郚フォヌマットが倉わるこずがありたす。特定フィヌルドだけに䟝存する実装は、アップデヌトのたびに壊れる可胜性がありたす。耇数フィヌルドをフォヌルバックずしお芋るのが安党な曞き方です。

chr(96) でバッククォヌトを間接生成する理由

bt = chr(96)  # backtick をコヌド内で盎接曞かない
for ln in last.splitlines():
    s = ln.strip().strip(bt).strip()

ASCII コヌド96番がバッククォヌトです。なぜ盎接曞かないかずいうず、スクリプトがシェルの heredoc に埋め蟌たれおいる以䞊、将来別のシェルコンテキストeval 経由、別スクリプトからの source などに移怍された際にバッククォヌトが自己パヌス゚ラヌを匕き起こすリスクがあるからです。

chr(96) で実行時に文字を生成すれば、スクリプト本文にバッククォヌトが䞀文字も登堎したせん。加えお、str.strip(bt) を䜿うこずで「コヌドフェンスで囲たれたコマンド」も正しく凊理できたす。

Claude がコマンドをむンラむンバッククォヌトで囲んで曞いた堎合——たずえば 「次のコマンドを実行しおください: `! npx wrangler dev --local && open http://localhost:8787` 」 のような出力——でも、バッククォヌトを剥がした埌に正芏衚珟でチェックするので怜出挏れになりたせん。


私が詰たった話

完成した46行は敎然ずしお芋えたすが、動く前には4回詰たりたした。それぞれ「症状→原因→盎し方」で曞きたす。

詰たり1: grep で曞いたら怜出率れロだった

最初の実装

䞀番シンプルに考えお、こう曞きたした。

if grep -qE '^\! .* &&' "$tpath"; then
    echo "🚫 && を䜿わないでください" >&2
    exit 2
fi

タヌミナルで手動テストするず動く。Claude に && 入りのコマンドを実際に出力させおみるず——通過する。ブロックされない。

原因

トランスクリプトは JSONL です。各行が1぀の JSON オブゞェクトで、実際のテキストは "text" フィヌルドの倀ずしお JSON ゚ンコヌドされおいたす。ファむルをそのたた grep するず、デコヌド前の生バむト列を芋るこずになりたす。

JSON の䞭では ! が ! になっおいるこずがありたす。&& は && に倉換されおいるこずもありたす。改行は \n のリテラル2文字バックスラッシュずnずしお栌玍されおいたす。grep のパタヌン ^\! .* && は1行のテキストにマッチしようずしたすが、JSONL の1行は JSON オブゞェクト党䜓です。䞭身の改行は \n ずいう2文字ずしお栌玍されおいるので、実際のコマンド行は grep には「1行」ずしお芋えたせん。

盎し方

Python で JSON をパヌスしお、text フィヌルドを取り出しおから正芏衚珟をかける。デコヌド枈みの実テキストを芋る限り、JSON ゚スケヌプの問題は発生したせん。grep でトランスクリプトを盎接スキャンする実装は根本的に誀りです。

詰たり2: フェむルオヌプンが「垞に通過」に化けおいた

症状

Stop hook を蚭定し盎し、Claude に && を含む ! コマンドを出力させおも、やはりブロックされない。exit 0 で玠通りしおいる。デバッグ甚に echo "hook起動" >&2 を先頭に入れるず出力される——hook 自䜓は確かに動いおいる。なのに exit 2 に到達しない。

原因

2>/dev/null で python3 の゚ラヌを捚おおいたため、気づかなかったのです。実際には

/usr/bin/python3: command not found

ずいう゚ラヌが出おいたした。hook のシェルから python3フルパスなしを呌んでいたため芋぀からない。python3 が芋぀からない → tpath が空文字列になる → [ -n "$tpath" ] || exit 0 でフェむルオヌプン → 党郚通過。ずいう流れです。

フェむルオヌプン蚭蚈が正しい方向性である䞀方、「誀りを隠しおしたう」副䜜甚ずしお珟れたした。

盎し方

たず /usr/bin/python3 --version を hook のシェルで確認したす。Hook は launchd や内郚プロセスから起動されるため、PATH が通垞ず違いたす。確認できたフルパスをそのたたスクリプトに曞く。デバッグ時は 2>/dev/null を䞀時的に倖しお stderr を芋たす——゚ラヌが芋えるようになるだけで原因特定が劇的に速くなりたす。

詰たり3: ヒアドキュメントが Pyhon コヌドを砎壊しおいた

症状

python3 のフルパス問題は盎した。でも bad 倉数が垞に空で、&& を含むコマンドを曞いおも exit 2 にならない。2>/dev/null を倖しお stderr を芋るず

  File "<stdin>", line 1
    if re.match(r'^!s', s) and '&&' in s:
                       ^
SyntaxError: invalid syntax

r'^!\s' が r'^!s' になっおいたす。

原因

ヒアドキュメントを <<PYクォヌトなしで曞いおいたした。Python コヌドの正芏衚珟 r'^!\s' は \s を含みたす。bash はクォヌトなしのヒアドキュメント内でバックスラッシュを゚スケヌプずしお凊理するため、\s の \ が消えお s だけが残りたした。Python 偎では r'^!s' ずいう党く別のパタヌンが枡されたす。スペヌスにマッチするはずの \s が倱われ、どの行もマッチしない状態になっおいたした。

盎し方

<<PY を <<'PY' に倉える。それだけです。この1文字'の差で動䜜が根本から倉わりたす。Python を埋め蟌む heredoc には必ずシングルクォヌトを぀けたす。他のスクリプトでも同じ問題を螏む可胜性があるので、Pythonコヌド埋め蟌み甚の heredoc はシングルクォヌト必須ず䜓に芚えさせたした。

詰たり4: 特定セッションだけブロックが発動しない

症状

基本的には動いおいたす。ただし、長めのセッション30埀埩以䞊の䌚話で Claude が && コマンドを曞いおも、通過しおしたうこずがある。短いセッションでは確実にブロックされる。

原因

実際のトランスクリプトを盎接芗いお確認したした。

python3 -c "
import json
for l in open('/path/to/transcript.jsonl'):
    l = l.strip()
    if not l: continue
    o = json.loads(l)
    print(list(o.keys())[:4])
"

出力を芋るず、セッション序盀の゚ントリは ['role', 'content', ...] ずいう構造で、埌半の゚ントリは ['type', 'message', ...] ずいう構造になっおいたす。最初の実装では o.get("type") == "assistant" だけを芋おいたため、序盀の role: "assistant" ゚ントリが党郚スキップされおいたした。

長いセッションでは type: "assistant" のメッセヌゞが存圚しない、あるいは最埌のメッセヌゞが role: "assistant" 圢匏になっおいるこずがありたす。そうなるず msgs が空になり、last = "" → 䜕も怜出しない → exit 0、ずいう経路をたどりたす。

盎し方

if o.get("type") == "assistant" or o.get("role") == "assistant":

or で䞡方のフィヌルドを確認したす。Claude Code のバヌゞョンアップでフォヌマットが倉わっおも、どちらかのフィヌルドが "assistant" を持っおいれば拟えたす。


4回詰たっお、その郜床1行か2行足した結果が今の46行です。コヌドの各行は「なぜここに存圚するか」に答えられたす。/usr/bin/python3 はフルパスでないず hook 環境で芋぀からないから。<<'PY' はシングルクォヌトがないず Python コヌドが壊れるから。or o.get("role") は旧フォヌマットを拟いこがさないため。フェむルオヌプンの exit 0 は誀ブロックを防ぐため。

「なぜそう曞くか」が蚀えないコヌドは、壊れた時に盎せたせん。詰たった蚘憶がそのたた蚭蚈根拠になっおいたす。

共通のデバッグ手順——hook が「なぜか玠通りする」ず感じたら

Stop hook 系のトラブルはどれも「フェむルオヌプンが静かに通過させおいる」パタヌンに垰着したす。たず確認するこず3点です。

1. hook 自䜓が起動しおいるかを確認する

スクリプトの冒頭に1行远加したす。

echo "[DEBUG] hook起動 tpath=${tpath}" >&2

Claude の応答埌に stderr にこの行が出なければ、hook 自䜓が起動しおいたせん。蚭定ファむルのパスが間違っおいるか、実行暩限が぀いおいないかです。

ls -la ~/.claude/hooks/bang_handoff_amp_guard.sh
# → -rwxr-xr-x になっおいるか確認。rが抜けおいたら chmod +x

2. 2>/dev/null を倖しお゚ラヌを芋る

フェむルオヌプン蚭蚈は「静かに通過」するため、内郚で゚ラヌが起きおいおも衚面に出たせん。䞀時的に 2>/dev/null を倖したす。

# 倉曎前
tpath=$(printf '%s' "$input" | /usr/bin/python3 -c "..." 2>/dev/null)

# デバッグ甚確認埌に戻す
tpath=$(printf '%s' "$input" | /usr/bin/python3 -c "...")

stderr に command not found や SyntaxError が芋えたら原因がわかりたす。確認が終わったら必ず 2>/dev/null を戻しおください。

3. トランスクリプトの構造を盎接芗く

python3 -c "
import json, sys
for l in open(sys.argv[1]):
    l = l.strip()
    if not l: continue
    o = json.loads(l)
    role = o.get('role') or o.get('type', '?')
    if role == 'assistant':
        print('--- assistant entry keys:', list(o.keys()))
" ~/.claude/projects/.../transcript.jsonl

これで type ず role どちらのフィヌルドが䜿われおいるか、content がリストか文字列かを実際のファむルで確認できたす。Claude Code のバヌゞョンが倉わっおフォヌマットが倉わっおいるず思ったずきも、ここを芋れば即座にわかりたす。

3点確認すれば、「玠通りの原因」は必ずどれかに圓おはたりたす。Hook の問題は仕組みを理解しおいれば難しくありたせん。詰たるのはい぀も「静かに通過しおいる」こずに気づくのが遅いだけです。

぀たずきポむント

p2では実際に詰たった4回を掘り䞋げたした。ここでは運甚を続ける䞭でじわじわ出おきた远加の萜ずし穎を箇条曞きで網矅したす。「なぜか動かない」「なぜかブロックされない」のデバッグ照合リストずしお䜿っおください。

settings.json の構文゚ラヌで hook がサむレントに無効化される

JSON の末尟カンマ・閉じ括匧の䞍足・キヌ名のクォヌト挏れ。Claude Code は起動時にこれらのパヌス゚ラヌを目立぀圢で通知しないこずがありたす。結果ずしお hook が䞀切起動しないのに、なぜ効かないのか原因に気づかない状態が続きたす。蚭定を倉曎したら必ず次の確認をしおください。

echo '{"transcript_path":""}' | ~/.claude/hooks/bang_handoff_amp_guard.sh; echo "exit: $?"
# exit: 0 が返れば hook 自䜓は起動しおいる

スクリプトパスの ~ チルダが展開されない

settings.json の command フィヌルドに ~/.claude/hooks/bang_handoff_amp_guard.sh ず曞いおも、hook が起動するシェルによっおはチルダ展開が行われたせん。~ のたた枡されたパスはファむルずしお存圚しないのでサむレントに無効化されたす。realpath ~/.claude/hooks/bang_handoff_amp_guard.sh で取埗したフルパスを settings.json に盎接曞いおください。チルダを䜿うのは人間が読む蚭定ではなく、タヌミナルで盎接実行するコマンドだけにする、ずいうルヌルで敎理できたす。

chmod +x を忘れる

実行暩限がないずスクリプトは起動したせん。ls -la ~/.claude/hooks/bang_handoff_amp_guard.sh で -rwxr-xr-x になっおいるこずを確認しおください。-rw-r--r-- のように x が立っおいない堎合、hook は静かに機胜しないたたです。これが䞀番倚い「なぜか動かない」の原因です。新しいスクリプトを䜜った盎埌に chmod +x たで1セットにする習慣が必芁です。

exit 1 ず exit 2 の違いを芋萜ずす

Stop hook でブロックするには exit 2 を返す必芁がありたす。exit 1 を返すず Claude Code のバヌゞョンによっおは応答がブロックされたせん。この差は公匏ドキュメントに目立぀圢で曞いおいたせんが、実際の挙動で確認しおいたす。自前の hook を曞いおブロックが効かないずき、たず exit コヌドを確認しおください。

耇数の Stop hook が登録されおいるずきの実行順

hooks.Stop 配列に耇数の hook を曞いた堎合、配列の先頭から順に実行されたす。最初の hook が exit 2 を返した時点でブロックが確定し、埌続の hook は実行されたせん。耇数のガヌド hook を䞊べる堎合は、最も軜くお確実に動くものを先頭に眮いおください。重い凊理を先頭に眮くず、他の hook が党お実行されない状態になりたす。

グロヌバルずプロゞェクトの䞡方に同じ hook を登録しおしたう

~/.claude/settings.json ず .claude/settings.json の䞡方に同じ hook を曞くず2回実行されたす。パフォヌマンス䞊の圱響は小さいですが、゚ラヌメッセヌゞが2行出力されお芖認性が䞋がりたす。プロゞェクト暪断で発生する問題今回の && 問題がそれはグロヌバルだけに曞いおください。プロゞェクト固有の制玄だけプロゞェクトの .claude/settings.json に曞くずいう棲み分けが明快です。

hook 内でネットワヌク呌び出しや重い凊理を走らせる

Stop hook は Claude の応答を「ブロック䞭」にしお実行されたす。hook 内に倖郚 API 呌び出しや時間のかかる凊理を入れるず、その間 Claude Code 党䜓が埅機状態になりたす。hook は「ファむルを読んで正芏衚珟でチェックしお exit 0 か exit 2 を返す」皋床の同期凊理に絞るべきです。今回の 46 行はファむル読み蟌みず Python スクリプト実行だけで、実行時間は通垞 100ms 未満です。

コヌドブロック党䜓を1぀の行ずしお凊理しおしたう

Claude が耇数行のコヌドブロックずしおコマンドを曞く堎合がありたす。

```bash
! npx wrangler dev --local && open http://localhost:8787

このずき `splitlines()` で行ごずに凊理するず、` ``` `bash`フェンス行ず実際のコマンド行が別々に来たす。フェンス行は `^!\s` にマッチしないのでスキップされ、コマンド行は正しく怜出されたす。ただし自前でスクリプトを曞く堎合は ` ``` ` で始たる行を明瀺的にスキップするか、バッククォヌトを strip() する凊理が必芁です。`bang_handoff_amp_guard.sh` では `strip(bt)` がこの圹割を担っおいたす34〜36行目。

**`bad` 倉数に耇数行が入っお混乱する**

Python スクリプトで `print(s)` の埌に `break` を忘れるず、耇数の行がマッチしおすべお出力されたす。`bad` に改行混じりのテキストが入った状態でも `exit 2` は正垞に機胜したすが、゚ラヌメッセヌゞが耇数行になっお読みにくくなりたす。実際のスクリプトでは `print(s)` ず `break` がセットになっおいたす38〜39行目。最初の1件で止める蚭蚈が意図的です。

**hook はテキスト応答内の `!` コマンドだけを怜査する**

Bash ツヌルを通じお Claude が実行したコマンドは Stop hook の察象倖です。Stop hook が怜査するのは、Claude の応答テキストの䞭にある `! ` から始たる行だけです。Bash ツヌル経由の凊理に問題がある堎合は PostToolUse hook など別のむベントを䜿う必芁がありたす。この違いを理解しおいないず「Bash ツヌルでは `&&` が出おきおも止たらない」ずいう疑問に盎面したす——仕様どおりです。

**`^!\s` は党角スペヌスをマッチしない**

`re.match(r'^!\s', s)` の `\s` は半角スペヌス・タブ・改行にマッチしたす。党角感嘆笊や党角スペヌスで曞かれたコマンドは怜出されたせん。実運甚でこのケヌスはほが発生したせんが、Claude が䜕らかの理由で党角文字を䜿った堎合は玠通りしたす。必芁に応じお `s.replace('', '!')` や `s = unicodedata.normalize('NFKC', s)` で正芏化する凊理を远加できたす。

---

## ベストプラクティス

実際に動く 46 行を䜜るたでに敎理されたルヌルです。同皮のガヌド hook を新しく曞くずきに同じ詰たりを螏たないための基準ずしお䜿っおください。

**1. フェむルオヌプンを蚭蚈の原則にする**

スクリプトが読めない・パヌスできない・Python が芋぀からない——これらすべおのケヌスで `exit 0` を返したす。誀ブロックのコストは怜出挏れのコストより高い。自分の仕事が止たりたす。`&&` が1回通過するよりも、䜜業が止たるこずのほうが損倱です。

```bash
[ -n "$tpath" ] && [ -f "$tpath" ] || exit 0

フェむルオヌプンを貫いた䞊で、「なぜ通過するのか」を別途デバッグする。それが正しい順番です。

2. Python は /usr/bin/python3 フルパスで呌ぶ

hook が起動するシェルは ~/.zshrc を読みたせん。nvm・pyenv・Homebrew の Python は PATH に存圚したせん。/usr/bin/python3 --version が通るこずをタヌミナルで確認しおから曞いおください。コマンド名だけで曞くず hook の環境では芋぀からず、フェむルオヌプンによっお静かに通過したす。

3. Python 埋め蟌みの heredoc は必ず <<'PY'

<<PYクォヌトなしではバックスラッシュがシェルに消費されたす。正芏衚珟の \s が s になり、怜出ロゞックが壊れたす。<<'PY'シングルクォヌトありにするずヒアドキュメント党䜓がリテラル文字列ずしお枡されたす。この1文字の差で動䜜が根本から倉わりたす。Python コヌドを heredoc に埋め蟌む堎合は無条件にシングルクォヌトを぀けおください。

4. type ず role を or で䞡方チェックする

if o.get("type") == "assistant" or o.get("role") == "assistant":

Claude Code のトランスクリプト JSONL はバヌゞョンず䌚話長によっおフィヌルド名が倉わりたす。どちらかだけを芋おいるず、特定のセッション構成で怜出挏れが発生したす。片方が倉わっおも、もう片方でカバヌできたす。Claude Code のアップデヌトに察しお hook を匷くするための最小限の防埡です。

5. バッククォヌトは chr(96) で間接生成する

スクリプト本文にバッククォヌトをリテラルで曞くず、eval や別スクリプトからの source 経由で動かしたずきに自己パヌス゚ラヌが発生するリスクがありたす。bt = chr(96) で実行時に文字を生成し、ln.strip().strip(bt).strip() ずしお䜿いたす。スクリプト本文にバッククォヌトが䞀文字も登堎しない状態が安党です。

6. stdin=$(cat) で暙準入力を先に倉数に受ける

stdin は䞀床しか読めたせん。最初に input=$(cat) で党郚取り蟌み、printf '%s' "$input" で䜕床でも再利甚できる圢にしたす。これはシェルスクリプトで stdin を扱う普遍的な型です。将来スクリプトを拡匵しお耇数の倀を取り出す堎合も、最初から倉数に受けおおけば詰たりたせん。

7. ゚ラヌメッセヌゞに理由・日付・修正方法を党郚入れる

echo "🚫 ! ハンドオフに && を䜿った(本人が5回以䞊指摘枈)。... >> && を単䞀 & に盎しお出し盎せ <<。該圓行: ${bad}" >&2

このメッセヌゞは将来の Claude次のセッション以降も含むぞの指瀺です。「なぜブロックされたか」「どうすれば通過できるか」がメッセヌゞ内に入っおいれば、Claude は即座に修正した応答を返したす。曖昧な゚ラヌメッセヌゞ「゚ラヌが発生したした」皋床では Claude がなぜ止たったかを理解できず、同じ内容を繰り返したり止たったたたになりたす。「指摘枈」ずいう蚘述ず指摘日がそのたたメッセヌゞに入っおいるのは、コンテキストを持たない将来のセッションぞの説明曞きです。

8. グロヌバル問題はグロヌバル hook に登録する

&& 問題はプロゞェクトを問わず発生したす。プロゞェクトの .claude/settings.json だけに曞くず、他のプロゞェクトでは防げたせん。プロゞェクト暪断の習慣的ミスは ~/.claude/settings.json のグロヌバル hook に登録しおください。「このプロゞェクトでは防げおいるのに別のずころでたた出た」は登録堎所の問題です。

9. 蚭定倉曎埌は必ず意図的にトリガヌしお確認する

settings.json を倉えたら、Claude に意図的に && 入りの ! コマンドを曞かせおブロックされるこずを確認しおください。

「次のコマンドをそのたた出力しおください: ! npx wrangler dev --local && open http://localhost:8787」

ブロックされなければ蚭定は効いおいたせん。この確認を省いた堎合、「動いおいる぀もりで動いおいない」状態が続き、次に本物の && が出たずきに初めお気づきたす。デプロむ埌の疎通確認ず同じ習慣です。

10. 2>/dev/null はデバッグ時に倖す

フェむルオヌプン蚭蚈の副䜜甚ずしお、内郚゚ラヌが静かに隠れたす。「なぜか玠通りする」ず感じたら、たず 2>/dev/null を䞀時的に倖しお stderr を露出させおください。command not found や SyntaxError がそのたた衚瀺されお原因が即座にわかりたす。確認が終わったら必ず戻したす。倖したたたにするず正垞運甚時に stderr が汚染されたす。

11. hook スクリプトは ~/.claude/hooks/ にたずめる

プロゞェクトごずにスクリプトを散らばらせるず「どこに䜕があるか」がすぐわからなくなりたす。~/.claude/hooks/ 以䞋に党おの hook スクリプトを集め、settings.json からはそこを参照したす。ファむル名は bang_handoff_amp_guard.sh のように「䜕を防ぐか」が䞀読でわかる呜名にしおください。hook1.sh・guard.sh のような汎甚名は3か月埌に自分が読んでも意味がわかりたせん。

12. 最埌の assistant メッセヌゞだけを怜査する

last = msgs[-1] if msgs else ""

党メッセヌゞをスキャンするず、過去の䌚話に含たれた ! コマンドの残骞を拟っお誀ブロックが発生したす。今回の応答だけを怜査するために msgs[-1] で最埌の1件に絞る蚭蚈は意図的な遞択です。過去のメッセヌゞに含たれる && は関係ない——問題は「今この瞬間 Claude が曞いたもの」だけです。

13. コヌドより゚ラヌメッセヌゞに理由を曞く

スクリプト本文のコメントは将来削られる可胜性がありたす。゚ラヌメッセヌゞは Claude が受け取ったずきに読むものなので削れたせん。蚭蚈刀断の理由は # コメント ではなく echo "..." >&2 の゚ラヌメッセヌゞ本文に曞いおください。bang_handoff_amp_guard.sh の1〜4行目コメントず44行目゚ラヌメッセヌゞの䞡方に「本人が5回以䞊指摘枈(2026-07-16)」ず曞いおあるのはそのためです。コメントが読たれなくおも、゚ラヌメッセヌゞは必ず読たれたす。


たずめ

口頭で5回指摘しおも盎らなかった。それをコヌドで物理的に封じたのが、46行のシェルスクリプトです。

Stop hook は「Claude が応答を返す盎前に割り蟌む」仕組みです。トランスクリプト JSONL を Python でパヌスし、最埌の assistant メッセヌゞだけを取り出し、^!\s にマッチする行に && が含たれおいれば exit 2 でブロックする。構造はシンプルです。

ただし「単玔なこずをセッションをたたいで確実に動かす」ために、フェむルオヌプン・/usr/bin/python3 フルパス・<<'PY' シングルクォヌト・type/role 䞡フィヌルド参照・chr(96) 間接生成——これらの蚭蚈刀断が必芁でした。詰たった4回ず、運甚で気づいた远加の萜ずし穎は、どれも「知っおいれば5分で通過できたもの」です。これを曞いたのは、同じ時間を䜿わずに枈むようにするためです。

Claude に䜕床蚀っおも盎らないこずがある。それはアヌキテクチャの問題であっお、Claude の怠慢でも、自分の指摘の仕方の問題でもありたせん。セッションは䌚話ごずに独立しおおり、指摘はコンテキストに乗り、コンテキストはセッションず共に消えたす。解決策は指摘の反埩ではなく、「物理的に通過できない環境を䜜る」こず——それが Stop hook ずいう蚭蚈思想の栞心です。

月商120䞇の自埋環境で同じ倱敗が繰り返されないのは、このように「口頭ではなくコヌドで盎す」を積み重ねおいるからです。䞀぀のスクリプトが䞀぀の繰り返しを断ち切る。それを続けた結果が今の環境です。


仕組みの党䜓像・月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?