Claude Code の使い方、初心者が貼る deny は読まれない

目次
  1. 貼った deny が参照されていないことは、起動時に分かる
  2. 参照される形と参照されない形を1件ずつ分ける
  3. allow も ask も同じ、コマンドラインだけ別だった
  4. claude doctor はこの書き間違いを拾わない
  5. ツール名だけで判定して外した
  6. 直した設定が静かになるまで見る
  7. 設定を貼った直後に確かめられること・確かめられないこと

Claude Code の使い方を調べると、初心者向けの記事はたいてい .claude/settings.jsondeny をコピペ形で載せている。貼ると安心する。だがそのルールが「権限チェックから参照されない形」だった場合、.envsecret/ も、守ったつもりの状態が成立していない。JSON としては正しいので、見た目には何も起きない。

私は deny に書ける形を1件ずつ分けて読ませ、どれが参照されどれが参照されないかを Claude Code 本体の出力で判定した。そのうえで、参照されないルールを自動で洗い出す短いスクリプトを書いて実測と突き合わせた。

貼った deny が参照されていないことは、起動時に分かる

検証環境はこれだけである。

$ sh env.sh
26.6.1
v22.21.1
2.1.267 (Claude Code)
Python 3.14.7
# exit=0 (204 ms)

日本語の入門記事によくある形をそのまま1ファイルにまとめた。.env を守り、secret/ を守り、node_modules/ を見せず、.git/ を触らせず、rm を止める、という並びである。

{
  "permissions": {
    "allow": [
      "Bash(git status)",
      "Read(src/**)"
    ],
    "deny": [
      "Write(.env)",
      "Write(secret/**)",
      "Glob(node_modules/**)",
      "Edit(.git/**)",
      "Bash(rm:*)"
    ]
  }
}

これを Claude Code に読ませる。ユーザ設定やローカル設定の警告が混ざると自分の書いた行を見つけにくいので、--setting-sources "" で読み込み元を切り、--settings で対象のファイルだけを渡した。

claude -p ping --settings settings-mixed.json --setting-sources ""

結果はこうなった。

$ claude -p ping --settings settings-mixed.json --setting-sources ''
Failed to authenticate. API Error: 403 Connection blocked by network allowlist
Permission deny rule (settings-mixed.json): Write(.env) is not matched by file permission checks — only Edit(path) rules are. Use Edit(.env) instead (Edit rules cover all file-editing tools).
Permission deny rule (settings-mixed.json): Write(secret/**) is not matched by file permission checks — only Edit(path) rules are. Use Edit(secret/**) instead (Edit rules cover all file-editing tools).
Permission deny rule (settings-mixed.json): Glob(node_modules/**) is not matched by file permission checks — only Read(path) rules are. Use Read(node_modules/**) instead (Read rules cover all file-reading tools).
# exit=1 (6428 ms)

2行目の認証エラーと最終行の終了コードは、この検証機が外向きの通信を止めていることによる。プロンプトへの応答は返ってこない。都合が良いのは、設定ファイルの検証が認証より手前で終わっていることだ。つまり通信できない状態でも、設定が参照される形かどうかだけは確かめられる。以降のログで終了コードが 1 になっているのも同じ理由で、設定の読み込みが失敗したわけではない。

守りたかった5件のうち3件が「参照されない」と名指しされている。残る Edit(.git/**)Bash(rm:*) は無言、つまり通っている。厄介なのは、この3件が「設定として無効」ではないことだ。JSON としては正しく、読み込みも通り、起動も止まらない。エラーではなく警告として1行流れるだけである。今回測ったのは -p を付けた非対話実行だけで、対話モードで同じ行がどう見えるかは確かめていない。

参照される形と参照されない形を1件ずつ分ける

まとめて読ませると因果が混ざるので、1ルールだけの設定ファイルを3つ作って分けた。違いはツール名とパスの有無だけにしてある。

まず Write(secret/**)

$ claude -p ping --settings settings-write.json --setting-sources ''
Failed to authenticate. API Error: 403 Connection blocked by network allowlist
Permission deny rule (settings-write.json): Write(secret/**) is not matched by file permission checks — only Edit(path) rules are. Use Edit(secret/**) instead (Edit rules cover all file-editing tools).
# exit=1 (1131 ms)

次に、同じパスを Edit で書いたもの。

$ claude -p ping --settings settings-edit.json --setting-sources ''
Failed to authenticate. API Error: 403 Connection blocked by network allowlist
# exit=1 (1896 ms)

警告は出ない。認証エラーだけが残る。

最後に、パスを持たない Write だけのルール。

$ claude -p ping --settings settings-bare.json --setting-sources ''
Failed to authenticate. API Error: 403 Connection blocked by network allowlist
# exit=1 (1141 ms)

これも警告が出ない。ここは私の予想と違った。Write という語が出たら警告されると思っていたが、警告の対象になるのはパスを書いたときだけだった。公式ドキュメントの権限ページにも、パスの無いツール名ルールは警告しないと書かれている。ただし私が測ったのは警告の有無までである。パス無し Write が実際にすべての書き込みを止めるかどうかは確かめていない。

allow も ask も同じ、コマンドラインだけ別だった

ここまでは deny の話である。初心者向けの設定例は allow のほうが行数が多いので、そちらも測った。ついでに、Edit 以外のファイル編集系ツール名と、フラグで直接渡した場合も並べた。

NotebookEditMultiEdit を1ファイルに入れる。

$ claude -p ping --settings settings-notebook.json --setting-sources ''
Failed to authenticate. API Error: 403 Connection blocked by network allowlist
Permission deny rule "MultiEdit(docs/**)" matches no known tool — check for typos.
Permission deny rule (settings-notebook.json): NotebookEdit(docs/**) is not matched by file permission checks — only Edit(path) rules are. Use Edit(docs/**) instead (Edit rules cover all file-editing tools).
Permission deny rule (settings-notebook.json): MultiEdit(docs/**) is not matched by file permission checks — only Edit(path) rules are. Use Edit(docs/**) instead (Edit rules cover all file-editing tools).
# exit=1 (772 ms)

MultiEdit だけ警告が2行出ている。1行目は「そんなツールは無い」、3行目は「パスルールとしても参照しない」。古い記事の設定をそのまま持ってくると、この二重の警告に当たる。

allowGlob を書いた場合。

$ claude -p ping --settings settings-allow-glob.json --setting-sources ''
Failed to authenticate. API Error: 403 Connection blocked by network allowlist
Permission allow rule (settings-allow-glob.json): Glob(docs/**) is not matched by file permission checks — only Read(path) rules are. Use Read(docs/**) instead (Read rules cover all file-reading tools).
# exit=1 (394 ms)

askWrite を書いた場合。

$ claude -p ping --settings settings-ask-write.json --setting-sources ''
Failed to authenticate. API Error: 403 Connection blocked by network allowlist
Permission ask rule (settings-ask-write.json): Write(docs/**) is not matched by file permission checks — only Edit(path) rules are. Use Edit(docs/**) instead (Edit rules cover all file-editing tools).
# exit=1 (603 ms)

行頭が Permission allow rule / Permission ask rule に変わるだけで、扱いは deny と同じだった。3セクションとも同じ検査を通っている。

最後に、同じ Glob(docs/**) をフラグで渡す。

$ claude -p ping --allowedTools 'Glob(docs/**)' --setting-sources ''
Failed to authenticate. API Error: 403 Connection blocked by network allowlist
# exit=1 (397 ms)

これだけ警告が出ない。設定ファイルに書けば警告され、--allowedTools で渡せば黙る。設定ファイルだけを検査しても、起動スクリプトやエイリアスに書いたルールは素通りするということである。チームでラッパースクリプトを配っているなら、検査の対象はファイルだけでは足りない。

ここまでで測った8通りを並べるとこうなる。「出る」は権限チェックから参照されないという意味で、貼った本人の意図が通っていない側である。

書いた場所 ルール 警告
deny Write(secret/**) 出る
deny Edit(secret/**) 出ない
deny Write(パス無し) 出ない
deny NotebookEdit(docs/**) 出る
deny MultiEdit(docs/**) 出る(2行)
allow Glob(docs/**) 出る
ask Write(docs/**) 出る
--allowedTools Glob(docs/**) 出ない

この表から言えることは3つある。

  • 警告は deny 専用ではなく、allowask にも同じ検査が掛かる
  • ツール名にパスを付けたかどうかが分かれ目で、ツール名だけのルールは別扱いになる
  • 設定ファイル経由かコマンドライン経由かで扱いが変わる

覚えることは少ない。ファイルを触らせたくないなら Edit、読ませたくないなら Read、この2つにパスを書く。それ以外のツール名にパスを付けたら、その行は飾りになる。逆にパスを付けないツール名だけのルールは別の扱いなので、混ぜて覚えないほうがいい。

claude doctor はこの書き間違いを拾わない

設定を疑ったとき、最初に打ちたくなるのは claude doctor だと思う。初心者向けの記事でも、困ったらこれ、という位置づけで紹介されている。先ほどと同じ内容を普通のプロジェクトと同じ位置、.claude/settings.json に置いて実行した。長いので ... で中略した。省いたのはインストール方式・更新チャネルなどの行である。

$ claude doctor
Claude Code doctor
...
1 warning found
- macOS Keychain is not writable (security: SecKeychainItemCreateFromContent (<default>): UNIX[Operation not permitted]; add-generic-password: returned 100001). Console login will fail to save your API key.
...
# exit=0 (1098 ms)

警告は1件出ているが、中身は Keychain が書き込み可能かという話で、permissions とは無関係だった。終了コードは 0 である。念のため、出力から該当する行だけを数えた。

$ sh -c 'claude doctor 2>&1 | grep -c '"'"'Permission deny rule'"'"'; exit 0'
0
# exit=0 (1260 ms)

一方、同じディレクトリで同じファイルをプロジェクト設定として読ませると、行は出る。--settings で渡したときだけの挙動ではない、という確認である。

$ sh -c 'claude -p ping --setting-sources project 2>&1 | grep -c '"'"'Permission deny rule'"'"'; exit 0'
3
# exit=0 (1294 ms)

中略した部分も含めて、doctor が並べていたのは次の種類の情報だった。

  • インストール方式と版、プラットフォーム
  • 自動更新の可否と更新チャネル、最後の更新結果
  • 組織ポリシーや管理設定を取得できるか
  • 認証情報を保存できるか(今回はここが警告になった)

どれも導入まわりの話で、設定ファイルの書式は入っていない。同じファイルを置いた同じディレクトリで、doctor は 0、起動は 3 になった。どういう実装でそうなっているかは分からないが、少なくとも同じ設定ファイルを前にして結果が割れることは確かめられた。「困ったら doctor」は導入まわりの確認には効くもので、permissions の書式の確認には使えない、と覚えておけばいい。

ツール名だけで判定して外した

毎回目で読むのは続かないので、設定ファイルを渡すと参照されないルールを列挙するスクリプトを書いた。最初の版(check_v1.py)は単純で、ツール名が Write / NotebookEdit / MultiEdit / Glob のどれかなら参照されない、と判定した。

$ python3 check_v1.py settings-write.json settings-edit.json settings-bare.json settings-mixed.json
settings-write.json: 1
    deny: Write(secret/**)
settings-edit.json: 0
settings-bare.json: 1
    deny: Write
settings-mixed.json: 3
    deny: Write(.env)
    deny: Write(secret/**)
    deny: Glob(node_modules/**)
total: 5
# exit=0 (51 ms)

この4ファイルの実測は4件だったのに、スクリプトは5件と言っている。差は settings-bare.jsonWrite で、パスを持たないルールを巻き込んでいた。ここで直すべきはスクリプトのほうである。

判定の基準をどこに置くかは先に決めておいたほうがいい。私は「Claude Code 本体が警告を出すかどうか」を正とし、食い違ったらスクリプトを直す、と決めてから書いた。逆にしていたら、パス無し Write を無効だと書いた記事ができていたと思う。ドキュメントを読んで書いたロジックと、実際に動いているバイナリの判定は別物として扱う。

別ファイル(check_permissions.py)として書き直したのが次である。パスの有無を必須にし、allow / ask / deny の3セクションを見て、MultiEdit には印を付ける。

#!/usr/bin/env python3
"""permissions のうち、ファイル権限チェックから参照されないルールを洗い出す。

判定の基準は Claude Code 本体の起動時警告。allow / ask / deny の3セクションすべてが対象。
パス付きの Write / NotebookEdit / MultiEdit は Edit に、Glob は Read に置き換える。
パスを持たないツール名だけのルール(例: "Write")はツールレベルで効くので対象外。
MultiEdit はこのバージョンでは存在しないツールとしても警告されるので、印を付ける。
"""
import json
import re
import sys

REPLACEMENT = {
    "Write": "Edit",
    "NotebookEdit": "Edit",
    "MultiEdit": "Edit",
    "Glob": "Read",
}
UNKNOWN_TOOLS = {"MultiEdit"}
RULE = re.compile(r"^([A-Za-z]+)\((.+)\)$")


def inert_rules(path):
    with open(path, encoding="utf-8") as f:
        perms = json.load(f).get("permissions", {})
    found = []
    for section in ("allow", "ask", "deny"):
        for rule in perms.get(section, []):
            m = RULE.match(rule)
            if not m:
                continue
            tool, arg = m.group(1), m.group(2)
            if tool not in REPLACEMENT:
                continue
            fix = f"{REPLACEMENT[tool]}({arg})"
            if tool in UNKNOWN_TOOLS:
                fix += " [unknown tool]"
            found.append((section, rule, fix))
    return found


def main(paths):
    total = 0
    for path in paths:
        found = inert_rules(path)
        total += len(found)
        print(f"{path}: {len(found)}")
        for section, rule, fix in found:
            print(f"    {section}: {rule} -> {fix}")
    print(f"total: {total}")
    return 1 if total else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

この記事で作った設定ファイル8本を全部渡す。最後の settings-fixed.json は、置き換え先を当てたあとの版である。

$ python3 check_permissions.py settings-write.json settings-edit.json settings-bare.json settings-mixed.json settings-notebook.json settings-allow-glob.json settings-ask-write.json settings-fixed.json
settings-write.json: 1
    deny: Write(secret/**) -> Edit(secret/**)
settings-edit.json: 0
settings-bare.json: 0
settings-mixed.json: 3
    deny: Write(.env) -> Edit(.env)
    deny: Write(secret/**) -> Edit(secret/**)
    deny: Glob(node_modules/**) -> Read(node_modules/**)
settings-notebook.json: 2
    deny: NotebookEdit(docs/**) -> Edit(docs/**)
    deny: MultiEdit(docs/**) -> Edit(docs/**) [unknown tool]
settings-allow-glob.json: 1
    allow: Glob(docs/**) -> Read(docs/**)
settings-ask-write.json: 1
    ask: Write(docs/**) -> Edit(docs/**)
settings-fixed.json: 0
total: 8
# exit=1 (32 ms)
1件settings-write.json0件settings-edit.json0件settings-bare.json3件settings-mixed.json2件settings-notebook.json1件settings-allow-glob.json1件settings-ask-write.json0件settings-fixed.json
参照されないルールの件数

ファイル別の内訳も合計も、Claude Code 本体が出した警告と一致した。無効なルールが1件でもあれば終了コードを 1 にしてあるので、そのまま CI に置ける。上の最終行が 1 になっているのはそのためで、スクリプトが落ちたわけではない。

このスクリプトは本体の判定を写しただけのもので、将来ツール名が増えたら追随しない。それでも手元に置く価値はあると考えている。起動時の警告は他の出力に紛れるうえ、読み飛ばしても何も起きないからだ。終了コードにしてしまえば、読むかどうかの問題ではなくなる。同じ理由で、私は REPLACEMENT の表を本体の警告文からそのまま作った。自分で対応表を考えると、その時点で本体の判定から離れる。

直した設定が静かになるまで見る

チェッカーが出した置き換え先をそのまま当て、読み取りも止めたかった .env には Read を足した。Edit は書き込み側しか見ないので、読ませたくないなら別に書く必要がある。

{
  "permissions": {
    "allow": [
      "Bash(git status)",
      "Read(src/**)"
    ],
    "deny": [
      "Read(.env)",
      "Edit(.env)",
      "Edit(secret/**)",
      "Read(node_modules/**)",
      "Edit(.git/**)",
      "Bash(rm:*)"
    ]
  }
}

同じ手順で読ませる。

$ claude -p ping --settings settings-fixed.json --setting-sources ''
Failed to authenticate. API Error: 403 Connection blocked by network allowlist
# exit=1 (1153 ms)

警告は消えた。ここまで来て初めて「書いた deny が参照される形になった」と言える。逆に言えば、警告が消えたことが示すのは形の正しさだけである。そのルールが意図した操作を実際に止めるかどうかは別の話で、通信を止めた状態では測れないので私は確かめていない。

設定を貼った直後に確かめられること・確かめられないこと

確かめられるのは「そのルールが権限チェックから参照される形か」である。作業ディレクトリで claude -p ping --setting-sources project を一度打って、Permission deny rule の行が出るかを見るだけでいい。数秒で終わるし、認証が通らない環境でも判定できる。初心者がやることとして、/init を打つのと同じくらいの手間しかかからない。

--setting-sources project を付けているのは、プロジェクトの設定だけを読ませて他の警告を混ぜないためである。付けずに打てばユーザ設定やローカル設定の分もまとめて出るので、自分のマシン全体を点検したいときはそちらでいい。どちらにしても、見るのは Permission deny rule で始まる行があるかどうかの1点だけである。

チェッカーを使うなら、設定ファイルを引数に渡して終了コードを見ればいい。無効なルールが残っていれば 1 が返るので、リポジトリに設定を置いているならコミット前に通せる。起動時の警告と違って、読み飛ばしようがないのが利点である。ただし前述のとおり、--allowedTools のようにコマンドラインで渡したルールはファイル検査では拾えない。

確かめられないのは「そのルールが狙った操作を止めるか」である。パスの書き方には別の落とし穴がある。公式ドキュメントによれば Edit(src/**) は作業ディレクトリ直下の src にしかマッチせず、任意の深さを対象にするには Edit(**/src/**) と書く必要がある。これは警告が出ない領域なので、起動時の出力を見ても分からない。私もこの深さの違いは測っていない。

この方法が向かないケースも書いておく。私が測ったのは 2.1.267 の1点だけで、どのバージョンからこの警告が出るようになったのかは確かめていない。古い環境では何も出ない可能性がある。Bash(rm:*) のようなコマンドのルールは今回の判定の対象外で、ファイル権限チェックの話とは別経路である。そして doctor は、少なくとも私が測った範囲では、この種の書き間違いを報告しない。

まとめると、設定を貼った直後にやることは次の4つになる。

  • 設定を書き換えたら claude -p ping --setting-sources project を一度打つ
  • Permission deny rule で始まる行が出たら、その行末の Use ... instead にそのまま従う
  • ファイルを触らせたくないなら Edit、読ませたくないなら Read にパスを書く
  • ラッパースクリプトで --allowedTools を渡しているなら、そちらは別に目で見る

初心者向けの記事が悪いというより、確認の工程がどこにも書かれていないのが問題だと思う。設定の書き方を教える記事は多いのに、書いた設定が読まれたかを確かめる行は載っていない。コピペした設定を貼ったままにするのと、起動を1回挟んで警告の有無を見るのとでは、後者のほうが確実に安い。私は deny を書き換えたら必ずこれを打つことにした。

関連書籍