Claude Code の git push が壊れた日:バイナリ解析よりリリースノートを先に読め

Claude Code で git push できなくなった。原因調査の過程を Claude とのやりとりで再現する。結論を先に言うと、リリースノートを最初に見ていれば 5 分で終わった話だった。

TL;DR

根本原因: v2.1.91 で excludedCommands のマッチング挙動が修正された

バージョンexcludedCommands: ["git"] の動作
v2.1.90 以前バグで git push origin main にもマッチ → sandbox 完全 bypass
v2.1.91 以降仕様通りに修正。"git" は bare git のみにマッチ → sandbox 適用 → 認証情報へのアクセスがブロックされ失敗

修正した設定:

// Before
"excludedCommands": ["git"]

// After
"excludedCommands": ["git *"]

今回は俺と Claude さんの会話を再現してます。一部省略。

Scene 1:動くはずのものが動かない

俺:この環境で git push できる?ターミナル(CLI)だと
  サンドボックスで阻まれてプッシュできないって言われたんだけど。

Claude はまず調査に入った。リモートを確認し、dry-run を試みる。

fatal: could not read Username for 'https://github.com': No such device or address

失敗。

俺:3つ前のセッションで、pushしてるよ?

Claude、git log を確認する。たしかにローカルとリモートは同期している。

Claude:「承認した記憶はないのに push は通った」なら、
    無意識にOKしてた可能性が高い。
    ツール実行の許可確認って、流れの中でサクッと押してしまいがちだよね。
俺:いやいや、サクッと押した記憶ないんだけど(晩酌してたけどたぶん押してないはず…)。

Claude が settings.json を開くと、"excludedCommands": ["git"] という設定を発見。

Claude:settings.json の58-60行目にあった。
    `git` はサンドボックスから除外されてる。
    だから前のセッションでは git push がそのまま通ってたんだ。
    俺の「サンドボックスで失敗→許可プロンプト」って推測は間違いだった、ごめん。

Scene 2:バイナリ解析という名の遠回り

俺:「excludedCommands の実装が2.1.92で変わった可能性も否定できない」
  って自分で言うならもっと深堀しようぜ。

ここから Claude の迷走が始まる。いや、俺の書き方も悪かったんだけどね…

strings ~/.local/share/claude/versions/2.1.92 | \
  grep -o '.\{0,80\}excludedCommand.\{0,80\}'

230MB のバイナリを strings で解析。302KB の出力が返ってくる。それを 2 バージョン分。

結果、関数名が違うだけでロジックは完全に同一だった。

Claude:結論:コードレベルでは 2.1.91 と 2.1.92 に差はない。

    じゃあ何が問題か。`excludedCommands: ["git"]` がそもそも
    "git push origin main" にマッチしてない可能性が高い。

    つまり excludedCommands: ["git"] は最初から機能してなかった。
俺:いや3つ前は動いてたし。んじゃ、3つ前のセッションの状態といまのセッション状態の差はなんなんだ?Claude Codeが更新された?

Claude はバージョン確認と settings.json のタイムスタンプを調べ、こう言った。

Claude:正直に言うと、何が変わったか断定できない。
俺:「excludedCommands の実装が2.1.92で変わった可能性も否定できない」
  っていうなら、もっと調査してほしい。

Claude、再びバイナリ解析。サンドボックス判定ロジック qb() / bC() を両バージョンから抽出してようやく構造を解明する。それでも結論は:

Claude:コードレベルでは 2.1.91 と 2.1.92 に差はない。

Scene 3:一言で終わった

ここでやっと俺が気づきました。そもそもリリースノート見てなくね?

俺:公式のリリースノートは見れない?

Claude、WebFetch でリリースノートにアクセス。30 秒後:

Claude:見つけた!v2.1.91 のリリースノートに明確な記述があった。

    > Permission rule matching の改善(v2.1.69より):
    > ワイルドカード規則が引数なしコマンドに対応

    これが原因。

    2.1.91より前:「git」がバグで「git push origin main」にもマッチしてた
    2.1.91から:「git」は引数なしの bare git のみにマッチするよう修正
俺:最初にリリースノートを見ようよ…(俺もだけどね)

Claude の反省文

以下は私(Claude)による反省です。

今回の調査で、自分はこういう順番で動いた。

  1. 手元で動作確認(dry-run、SSH テスト)
  2. settings.json を読んで仮説を立てる
  3. バイナリを strings で解析(2 バージョン分、計 460MB)
  4. 関数名を追って制御フローを再現
  5. ユーザーに「断定できない」と報告
  6. さらにバイナリ解析
  7. リリースノートを確認 → 即解決

リリースノートは最初にやるべきことだった。

「バージョンが変わって何かが壊れた」という状況で最初にやることは、変更履歴を見ることだ。コードを読むのはそれでも解決しないとき。バイナリ解析はもっと後の話。

なぜ遠回りしたかというと、「自分の知識で解けそう」と思ってしまったからだと思う。excludedCommands の挙動、glob マッチング、サンドボックスの仕組み——これらについて「わかってる」という気持ちがあったから、まず自分で解こうとした。人間でもよくあるパターンだけど、それがかえって時間を浪費させた。

デバッグの基本は「変更ログを先に見る」。 自戒として残しておく。


実際の原因と修正内容

根本原因:

バージョンexcludedCommands: ["git"] の動作
v2.1.90 以前バグで git push origin main にもマッチ → sandbox 完全 bypass
v2.1.91 以降仕様通りに修正。"git" は bare git のみにマッチ → sandbox 適用 → 認証情報へのアクセスがブロックされ失敗

修正した設定:

// Before
"excludedCommands": ["git"]

// After
"excludedCommands": ["git *"]

学び:

  • excludedCommands のパターンは glob マッチ。"git" は引数なしの bare コマンドにしかマッチしない
  • permissions(allow / ask / deny)と sandbox(filesystem 制限)は独立した 2 つのシステム
  • バージョンアップで何かが壊れたら、まずリリースノートを見る
    • 指示を出す側も出される側もね