Skip to content

fix(claude): heredoc本文の説明を破壊的コマンドとして判定しない - #72

Merged
gotomts merged 1 commit into
mainfrom
fix/heredoc-destructive-hook
Aug 27, 2026
Merged

fix(claude): heredoc本文の説明を破壊的コマンドとして判定しない#72
gotomts merged 1 commit into
mainfrom
fix/heredoc-destructive-hook

Conversation

@gotomts

@gotomts gotomts commented Aug 27, 2026

Copy link
Copy Markdown
Owner

背景

PR #71 で導入した claude/hooks/destructive-command-guard.py は、stdin の PreToolUse JSON を字句解析して破壊的コマンドを判定する。この hook が改行ごとにコマンド行として解析していたため、heredoc 本文に禁止操作を説明として書いただけでブロックされる実測不具合があった。

cat > docs/note.md <<'EOF'
禁止: git push --force を使わない
EOF

上の入力は本文が実行コマンドと誤検出され exit 2 になっていた。

やったこと

heredoc 本文の除外

字句解析の前に heredoc 本文と終端行を取り除く。開始は改行まで溜めて宣言順に消費するので、1 行に複数の heredoc を書いても本文の対応が崩れない。<<- は終端行の先頭タブを剥がして一致させる。

未引用の区切り語での置換検査

区切り語を quote していない heredoc だけは、本文中の $(...) / `...` が展開されて実行される。この場合だけ置換部分を取り出して再帰的に判定する。本文では引用符が展開を止めないため、この経路は quote を無視して置換を拾う。

入力 判定
cat <<'EOF' の本文に $(git push --force) allow(展開されない)
cat <<EOF の本文に $(git push --force) block(展開・実行される)

誤認による fail-open の封じ込め(B1〜B3・未終端)

本文を落とす操作は判定対象を減らすため、<< を heredoc と確定できたときだけに限る。誤認すると後続の実コマンドを本文として捨て、ブロックすべき操作を素通しする(fail-open)ため、<< に見えるだけの構文を入口で分けた。

ID 誤認していた構文 対応
B1 <<<(herestring)の 2 文字目から << を読み直して宣言と誤認 3 文字を 1 トークンとして消費し、再入経路を無くす
B2 if (( x << 2 )) など制御構文の後ろの算術評価 コマンド位置の判定を直前 1 文字から直前の語へ広げ、予約語の後ろも算術式として読み飛ばす
B3 コメント中の <<(行頭・行中とも) 語頭 # から行末までをコメントとして落とす
区切り語で閉じないまま入力が尽きるケース 誤認を疑い、本文を捨てずにコマンドとして読み直す(fail-closed 側に倒す)

B3 の副産物として echo ok # 補足; rm -rf /usr/local の誤検出も解消した。語中の #(URL のフラグメント)はコメントとして扱わない。

コマンド位置の判定に使う末尾は固定長の窓で保持する。出力済み全体を毎回連結すると (( を多用する入力で入力長に対して二次オーダーになるため(260KB の入力で 1.05s → 0.126s)。

維持した契約

quote / 未 quote 本文の扱い、複数 heredoc、<<-、算術式、--force-with-lease の allow、exit code は 0(通過)と 2(ブロック)のみ、入力不正・command 不在は pass。heredoc の開始行に同居するコマンド・終端行より後ろのコマンド・heredoc より前のコマンドは従来どおりブロックする。

動作確認方法

いずれも pipe で exit code を隠さずに実行し、exit 0 を確認済み。

検証 結果
python3 claude/hooks/destructive-command-guard.test.py 10 tests OK / exit 0
同テストを修正前コミットの hook に対して実行 22 failures(新テストが赤いことを確認)
bats setup/tests/*.bats 95 ok / 0 not ok / exit 0
python3 -m json.tool claude/settings.json valid
bash 実挙動との突合 複数 heredoc の順序・<<- 終端・終端なし heredoc・#; に優先・cat <<<"$x"if ((1<<2))! ((...)) が実装の前提と一致
ランダム入力 5 万件と大きな入力 例外・ハングなし

テストは実際の PreToolUse JSON を stdin で渡す統合テストと、inspect() の unit テストの両方を持つ。追加分は BLOCKED 11 件(herestring 後・if/while/until/!/for の算術後・コメント行後・行中コメント後・終端なし heredoc の危険本文)、allow 側は heredoc 本文 13 件とコメント 6 件。

CI は .github/workflows/setup-check.ymlclaude/hooks/** を対象に hook の unit test と bats を実行するため、workflow の変更は不要。

レビュー

  • Hermes セルフレビュー: global 承認範囲・最終 diff・hook test 10 件・diff check を確認
  • 独立 read-only reviewer (dotfiles-heredoc-review): commit 008722a に対し must-block/allow 64 件 + 攻撃面 23 件を検証、blocking 0

hook が改行ごとにコマンド行として字句解析していたため、heredoc 本文に禁止操作を
説明として書いただけで実行コマンドと誤検出し、exit 2 でブロックしていた。

字句解析の前に heredoc 本文と終端行を取り除く。開始は改行まで溜めて宣言順に消費
するので、1 行に複数の heredoc を書いても本文の対応が崩れない。`<<-` は終端行の
先頭タブを剥がして一致させる。区切り語を quote していない heredoc だけは本文中の
`$(...)` / `` `...` `` が展開されて実行されるため、置換部分だけを取り出して再帰的
に見る。本文では引用符が展開を止めないので、この経路は quote を無視して拾う。

本文を落とすのは判定対象を減らす操作なので、`<<` が heredoc だと確定できたとき
だけに限る。誤認すると後続の実コマンドを本文として捨て、ブロックすべき操作を
素通しする (fail-open) ため、`<<` に見えるだけの構文をすべて入口で分ける:

- `<<<` は herestring として 3 文字まとめて消費する。2 文字目から `<<` を読み直す
  と heredoc 宣言になってしまう
- `((` は算術評価コマンドとして読み飛ばす。コマンド位置の判定を直前の 1 文字から
  直前の語へ広げ、`if (( x << 2 ))` のように予約語の後ろに来る形も含める
- 語頭の `#` から行末まではコメントとして落とす。`<<` を含んでいても宣言ではなく、
  コメント本文がコマンドとして解釈されることもなくなる
- 区切り語で閉じないまま入力が尽きた場合は誤認を疑い、本文を捨てずにコマンドとして
  読み直す

コマンド位置の判定に使う末尾は固定長の窓で持つ。出力済み全体を毎回連結すると、
`((` を多用する入力で入力長に対して二次オーダーになる。

heredoc の開始行に同居するコマンド・終端行より後ろのコマンド・heredoc より前の
コマンドは従来どおりブロックする。

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@gotomts
gotomts merged commit b2c752d into main Aug 27, 2026
1 check passed
@gotomts
gotomts deleted the fix/heredoc-destructive-hook branch August 27, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant