◉CLAUDE LABEN
●2.1.312 — 巨大な CLAUDE.md の上書きや、cd のあとにスキルが見つからない問題を修正しました●API — Messages API の最大リクエストサイズが 32MB から 128MB に拡大されました(10/28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り31日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●API — 新しい API バージョン 2026-10-21 では temperature・top_p・top_k を渡すと 400 になります●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.312 — 巨大な CLAUDE.md の上書きや、cd のあとにスキルが見つからない問題を修正しました●API — Messages API の最大リクエストサイズが 32MB から 128MB に拡大されました(10/28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り31日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●API — 新しい API バージョン 2026-10-21 では temperature・top_p・top_k を渡すと 400 になります●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/Claude Code
⟐ Claude Code/2026-07-30上級

作業ルートを途中で足したとき、無視規則と deny はどこまで届くのか — DirectoryAdded で境界を測り直す

セッション途中で作業ルートを追加すると、無視規則も deny パターンもそのままでは新しいルートに届きません。git check-ignore と照合器の比較で境界を実測し、DirectoryAdded から呼ぶ差分監査スクリプトの実行結果まで記録しました。

Claude Code280hooks22モノレポセキュリティ23運用設計30

✦ プレミアム記事

セッションを止めずに /add-dir でもう一つのリポジトリを足したのは、iOS 側のビルド設定を並べて確認したかったからでした。

個人開発でアプリ側のリポジトリとサイト側のリポジトリを行き来していると、こういう横断はどうしても出てきます。

作業自体はすぐ終わりました。気になったのは、そのあとです。

普段どおり秘匿ファイルの棚卸しを走らせたところ、件数が変わっていませんでした。追加したルートには、設定ファイルが何本も入っているはずなのに。

数分かけて追い、原因は単純でした。棚卸しの起点が最初のルートのままだったのです。ルートを増やすという操作は、そこにぶら下がる前提まで一緒に運んではくれません。

セッションは正常。エラーもゼロ。ただ、検査の網だけが古いところに張られたままでした。

追加したルートは、既存ルートの子ではない

/add-dir や SDK の register_repo_root でルートを足すと、感覚としては作業範囲が「広がった」ように見えます。

実際に起きているのは、独立したルートがもう一本並んだ状態です。親子ではなく、並列。

この違いが効いてくるのが無視規則です。git の除外規則は、リポジトリの外側には適用されません。当たり前の仕様ですが、ルートが二本になった瞬間に、その当たり前が運用上の穴になります。

同じ構造を手元で作って確かめました。root_a には secrets/ を無視する .gitignore を置き、root_b には置いていません。中身はどちらも同じ secrets/prod.env です。

# root_a 側: .gitignore が効いている
$ git -C root_a check-ignore -v secrets/prod.env
.gitignore:1:secrets/	secrets/prod.env
 
# root_a から root_b のファイルを判定させようとすると、そもそも受け付けない
$ cd root_a && git check-ignore -v ../root_b/secrets/prod.env
fatal: ../root_b/secrets/prod.env: '../root_b/secrets/prod.env' is outside repository
 
# root_b 側では、同じパスが無視対象にならない
$ git -C root_b check-ignore -v secrets/prod.env
(出力なし・終了コード 1)

git status まで見ると、差は目に見える形で出ます。

$ git -C root_a status --porcelain
A  .gitignore
A  src/app.ts
 
$ git -C root_b status --porcelain
A  secrets/prod.env      ← 追加ルート側では staged になる
A  src/app.ts

root_a では secrets/prod.env が一覧にすら現れず、root_b では素通しで staged まで進んでいます。

つまり、追加ルートに対する判定は、必ず追加ルートの中で git を回さないと正しい答えになりません。親ルートから相対パスで覗きにいく実装は、間違った「安全」を返します。

deny パターンは、どの照合器で読まれるかで意味が変わる

無視規則の次に確認したのが permissions.deny です。

こちらは git とは別系統で、パターン文字列の照合になります。ここで前提が一つ崩れました。同じパターンでも、照合器が違えば結果が変わります。

代表的な三つ、fnmatch・Path.match・glob と同じ ** セマンティクスを並べて、同じパスに当ててみました。

import fnmatch, re
from pathlib import PurePosixPath
 
def m_glob(p, pat):
    """glob と同じ ** セマンティクス(0個以上のディレクトリ)を再現する"""
    rx = (re.escape(pat)
          .replace(r"\*\*/", "(?:[^/]+/)*")
          .replace(r"\*\*", ".*")
          .replace(r"\*", "[^/]*")
          .replace(r"\?", "[^/]"))
    return re.fullmatch(rx, p) is not None
 
for pat in ["*.env", "**/*.env", "config/*.env", "**/config/*.env"]:
    for p in ["config/prod.env", "ios/Config/secrets.env",
              ".vscode/settings.json", "scripts/build.sh"]:
        print(pat, p,
              fnmatch.fnmatch(p, pat),
              PurePosixPath(p).match(pat),
              m_glob(p, pat))

実行して出た結果を表にしたものが以下です。

パターンパスfnmatchPath.matchglob(**)
*.envconfig/prod.envTrueTrueFalse
*.envios/Config/secrets.envTrueTrueFalse
**/*.envconfig/prod.envTrueTrueTrue
**/*.envios/Config/secrets.envTrueTrueTrue
config/*.envconfig/prod.envTrueTrueTrue
config/*.envios/Config/secrets.envFalseFalseFalse
**/config/*.envconfig/prod.envFalseFalseTrue
**/config/*.envios/Config/secrets.envFalseFalseFalse

読み方が二か所あります。

一つ目は *.env の行です。fnmatch と Path.match では config/prod.env に当たっています。* がスラッシュを越えているのです。glob の感覚で「直下だけを指定したつもり」で書くと、意図より広く当たります。

二つ目は **/config/*.env の行です。glob では当たるのに、fnmatch と Path.match では外れます。**/ が「0個以上のディレクトリ」ではなく「何かしらのディレクトリが1段以上」として読まれるためで、ルート直下の config/ が抜け落ちます。

ここが、私が事前に予想していたのと逆でした。**/ を付けるほど網が広がるつもりでいたのに、照合器によっては直下を取りこぼす方向に働きます。しかも取りこぼす対象が、追加ルートの直下という、いちばん設定ファイルが集まる場所でした。

なお、上の表の値は手元の Python 3.10.12 で実行したものです。Path.match は、この版では ** を * と同じ扱いにしています。3.13 で加わった PurePath.full_match は ** を再帰として読む別の関数なので、新しい版を使っている場合は、同じコードを自分の環境で一度走らせて表を作り直してください。表をそのまま信じない、というのがこの節の持ち帰りです。

ルートが一本だった頃は、この差に気づく機会がありません。パスの階層構造が一つしかないので、どの書き方でも結果が揃ってしまうからです。ルートを足して階層の浅い木が増えたときに、初めて表面化します。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦追加ルートの無視規則が親ルートへ合成されない挙動を git check-ignore で実証(同名の secrets/ が片方は無視され、片方は staged になる)
✦同じ deny パターンが照合器ごとに割れる比較表 — `*.env` はスラッシュを越えて当たり、`**/config/*.env` はルート直下を取りこぼす
✦DirectoryAdded から呼ぶ約70行の差分監査スクリプトと、その実行出力(未カバー2件・大小文字違い1件・パス衝突2件を検出)
✦2,882ファイルの追加ルートで計測したフック所要 21.8 ミリ秒と、差分監査が再走査より速くならなかった理由(差は 1% 未満)
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⟐ Claude Code2026-10-14
onFailure を block にしても、jq が無い環境では公式例の rm -rf ブロックは素通りしました
PreToolUse の門番フックは、落ちたときに通すのか止めるのか。公式例を jq の無い環境で走らせると exit 0 で素通りしました。終了コード10通りの実測、直し方、settings.json を走査して素通りを洗い出すスクリプトをまとめます。
⟐ Claude Code2026-08-16
Todo ツールが既定オフになった Claude Code で、多段作業の進捗をどう追うか
Claude Code v2.1.233 以降、新しいモデルでは TodoWrite と Task 系ツールが既定で読み込まれません。環境変数で戻す前に確かめるべき3つの分岐と、チェックリストの代わりに置いた作業台帳の実装、並列実行で台帳が壊れた境界の実測結果をまとめます。
⟐ Claude Code2026-08-04
ファイルシステムの分離をネットワークと別に締める — 触ったパスを採り、書き込み面から絞る
Claude Code v2.1.216 でファイルシステムの分離をネットワークと切り離して指定できるようになりました。締める前にジョブが触ったパスを strace で採取し、読み取り面と書き込み面を分けて測った実務メモ。同じジョブを3回走らせたときのパス集合の安定率と、接頭辞に畳んだときの安定率の差まで実測しています。