【ITニュース解説】A bug in my own branch-guard hook, caught by typing one extra space
2026年10月16日に「Dev.to」が公開したITニュース「A bug in my own branch-guard hook, caught by typing one extra space」について初心者にもわかりやすく解説しています。
ITニュース概要
開発中のGitフックで、コマンド内の余分なスペースが原因のバグが見つかった。シェルの空白文字処理の不統一が原因で、フックがブランチ名を誤検出した。徹底的なテストで発見し、AIが利用するエラーメッセージは、その正確性が極めて重要であると再認識した。
ITニュース解説
システムエンジニアを目指す皆さんにとって、日々の開発作業を効率化するツールは非常に重要だ。しかし、そのようなツールさえも完璧ではなく、時には思わぬところにバグが潜んでいることがある。今回紹介するニュースは、まさにそのような事例で、開発者が自身の作成したツールに潜んでいた小さなバグを、たった一つの余分なスペースがきっかけで発見したという話だ。この経験から、私たちがシステム開発において学ぶべき多くの教訓がある。
物語の主人公は、大規模言語モデル(LLM)エージェントがコードを生成する際に犯しやすい間違いを防ぐための「Claude Codeフック」というツール群を開発した。これは、無意味なコード変更、誤ったブランチへのコードプッシュ、プログラムの想定範囲外への書き込み、そしてLLMが事実と異なる主張をするのを防ぐための自動化ツールだ。開発者はこれらのフックが正しく機能するかを確認するため、「ドッグフーディング」と呼ばれる手法、つまり自分で作ったツールを自分で実際に使ってみることでテストを行っていた。その最中に、自身が作ったフックの中に隠れていたバグを発見したのだ。
問題となったのは「block-wrong-branch.sh」という名前のフックだった。このフックの目的は、開発者が誤って間違ったGitブランチにコードをプッシュするのを防ぐことだ。具体的には、git pushコマンドが実行される前に、そのコマンドの引数からどのブランチにプッシュしようとしているのかを抽出し、あらかじめ設定された許可リスト(.claude/branch-policy.json)と照合する。もし許可されていないブランチへのプッシュであれば、フックがそれをブロックし、プッシュを中止させる仕組みだ。これは開発現場でのヒューマンエラーを防ぐための非常に有用なツールと言えるだろう。
バグは、このフックをテストする際、偶然に一つの余分なスペースを入力してしまったことから明らかになった。開発者は新しいテストケースを記述する中で、誤ってgit push origin mainというコマンドを入力した。注目すべきは、gitとpushの間に通常より一つ多いスペースが入っていたことだ。このコマンドをフックに渡したところ、フックは正しくプッシュをブロックした。これは、テスト設定によってmainブランチが許可リストに含まれていなかったため、フックの「失敗しても安全な設計(フェイルセーフ)」が機能した結果だ。しかし、フックが報告したエラーメッセージは「pushブランチへのプッシュは許可されていません」というものだった。本来であれば、「mainブランチへのプッシュは許可されていません」と報告されるべきだったのだ。プッシュ自体は阻止されたが、エラーメッセージが事実と異なっているという、小さくても見過ごせない問題が浮上した。
なぜこのような誤ったエラーメッセージが表示されたのだろうか。その原因は、Bashシェルスクリプト内部の文字列処理の挙動が複雑に絡み合っていたことにある。このフックには、主に三つの異なる方法で文字列を扱う部分があった。
一つ目は、case文によるコマンドの検出ステップだ。これは、入力されたコマンドがgit pushを含んでいるかをチェックする部分で、*"git push"*|*"git push"*)のように記述されていた。このパターンは、git pushの後にスペースが一つでも二つでも、あるいはそれ以上あっても、コマンドを正しく検出するように作られていた。つまり、この検出ステップは余分なスペースを許容する柔軟な設計だった。
二つ目は、tail="${command#*git push}"という部分だ。これは、コマンド文字列の先頭からgit pushというパターン(ここではスペースが一つ)にマッチする部分を取り除く(削除する)ためのシェルパラメータ展開という機能だ。ここがバグの核心だった。入力されたコマンドはgit push origin mainであり、gitとpushの間にスペースが二つあった。しかし、この#によるパターンマッチは空白の連続を考慮せず、指定された厳密な文字列git push(スペース一つ)にのみマッチしようとする。結果として、パターンが入力コマンドにマッチしなかったため、tail変数には元のコマンド全体であるgit push origin mainがそのまま格納されてしまった。
三つ目は、read -r -a tokens <<< "$tail"という部分だ。これは、tail変数に格納された文字列を単語ごとに分割し、配列に格納する処理だ。Bashの単語分割機能は、連続する複数の空白を単一の空白として扱う。そのため、git push origin mainという文字列は、[git, push, origin, main]という四つの単語に分割された。ここで、フックは配列の二番目の要素(push)をブランチ名として抽出するように設計されていたため、本来のブランチ名であるmainではなく、「push」という文字列が誤ってブランチ名として認識されてしまったのだ。
このように、case文によるグロブパターンマッチは空白の連続に寛容で、シェルパラメータ展開は空白に厳密であり、そしてreadによる単語分割は空白の連続を無視するという、三つの異なる空白処理ルールが一つの短いスクリプトの中で混在していたことが、このバグの根本原因だった。検出ステップは余分なスペースを許容したが、その後の解析ステップがそれに追従できなかったのである。
この問題を解決するための修正は非常にシンプルだった。コマンドの処理の最初にたった一行を追加するだけだ。command="$(printf '%s' "$command" | tr -s ' \t')"という修正は、入力されたコマンド文字列内の連続するスペースやタブを、全て単一のスペースに正規化するというものだ。これにより、後続のどの処理ステップでも、コマンドが常に一貫した形式で扱われるようになり、ブランチ名が正しく抽出されるようになった。この修正後、開発者は新しい回帰テストを追加し、合計27のテストケースで正しく機能することを確認した。
この出来事から得られる教訓は多い。まず、「ドッグフーディング」の重要性だ。自分自身がツールのユーザーになることで、開発時には気づかなかったような、実際の使用状況に近い形でバグを発見できる可能性が高まる。次に、エラーメッセージの正確性が極めて重要であること。たとえツールが問題を正しく防いだとしても、出力されるエラーメッセージが間違っていれば、それを利用する人間だけでなく、今回のようにLLMのような他のシステムも誤った情報を元に判断を下してしまう。LLMがエラーメッセージを「なぜ失敗したのか」という推論の根拠にするため、誤ったメッセージはエージェントの次の行動を「毒する」ことになりかねない。最後に、テストの徹底だ。たった一つの余分なスペースのような、開発者がつい見落としがちな「敵対的入力」が、システム全体の挙動を狂わせることがある。そのため、単なる正常系のテストだけでなく、ありとあらゆる異常系、特に空白の有無、特殊文字の混入、不正なフォーマットなど、予測しにくい多様な入力パターンに対して自動テストを行う「テストハーネス」を構築することが不可欠だ。開発者は、「もしあなたのフックにテストハーネスがなければ、バグを抱えているだろう」と述べており、テストの重要性を強く訴えている。システムエンジニアとして、目に見えない小さなバグが大きな問題を引き起こす可能性を常に意識し、堅牢なシステムを構築するための多角的なテストと、正確なフィードバックの提供を心がける必要がある。