0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「設定したのになぜ?」Claude Codeのパーミッションが効かない理由と、今すぐできる対策

0
Posted at

はじめに

Claude Codeを使い始めてしばらく経つと、ほぼ全員がぶつかる壁がある。

settings.json にパーミッションを設定したのに、毎回こいつが来る。

Allow this bash command?
  ls -d /Users/xxx/Project/*/

  1  Yes
  2  Yes, allow for this session
  3  No

設定したやん。

Bash(ls *) も書いた。Bash(git status) も書いた。なのに毎回聞いてくる。
「書き方が間違ってるのかな?」と記法を変えても変えても、結果は同じ。

実はこれ、設定の書き方の問題じゃなくて、Claude Code 側のバグです。
GitHub には同じ悩みの Issue が 30 件以上積み上がっていて、世界中の Claude Code ユーザーが同じ壁にぶつかっている笑

この記事では、バグの正体と「今すぐできる対策」をまとめます。


そもそも settings.json のパーミッションはこう書く(おさらい)

{
  "permissions": {
    "allow": [
      "Bash(ls *)",
      "Bash(grep *)",
      "Bash(git status)"
    ]
  }
}

Bash(ls *) と書けば ls から始まるコマンドは全部許可、というのが期待する動作。
公式ドキュメントにもそう書いてある。

でも、動かない。


バグの正体:「パイプでつないだコマンドは別物扱い」される

Claude Code のパーミッションシステムは、コマンドを 文字列ごと丸ごと パターンマッチしている。

つまりこういうことだ。

# ✅ これは通る(単体コマンド)
ls -la

# ❌ これは通らない(パイプで繋いだ複合コマンド)
ls -la | grep foo | head -30

ls -laBash(ls *) にマッチする。
でも ls -la | grep foo | head -30 という文字列全体Bash(ls *) にマッチしない。

Claude Code が実際に実行するコマンドは後者のような複合コマンドがほとんど。だから設定していても「ほぼ全部のコマンドで毎回聞かれる」という事態になる。


世界中で同じ不満が上がっていた

これはローカルな問題ではなく、世界規模で報告されているバグだ。

GitHub の公式リポジトリには 30 件以上の関連 Issue が積み上がっている。

Issue 内容
#13340 パイプコマンドが設定を無視する(2025年12月〜)
#18160 settings.json の allow 設定が効かない
#18846 カスタム Hook なしでは機能しない、と結論づけた報告
#29529 Bash(curl *) を設定しても 20 回全部聞かれた
#29616 ワイルドカードが機能せず settings.json に完全一致文字列が増殖し続ける

特に #29616 の「Always Allow をクリックするたびに settings.json にそのコマンドの完全一致文字列が追記されていくだけで、次回また聞かれる」という地獄のループ報告は笑えない笑

英語圏の DEV Community でも「Permission に関するバグで 57 件の Issue に答えた。全部同じパターンだった」という記事が出るほど、グローバルな問題になっている。

公式もバグと認識しているが、2026年5月時点でまだ修正されていない。


海外コミュニティが辿り着いた結論

世界中の開発者が試行錯誤した結果、英語圏のコミュニティでこういう考え方が定着してきた。

「Permissions はリクエスト、Hooks は強制執行」

Claude Code には PreToolUse Hooks という仕組みがある。コマンドが実行される直前に割り込んで、「このコマンドは許可する / しない」をプログラムで判定できる。

バグのあるパターンマッチを使う代わりに、自前でコマンドをパイプ分割して個別に判定すれば問題は解消される。

さらに、公式ドキュメントにもこの使い方が追記された。

「すべての Bash コマンドをプロンプトなしで実行したい場合は Bash を allow リストに入れ、ブロックしたい特定コマンドを PreToolUse Hook で拒否する」

つまり**「パーミッション = 利便性のため」「Hooks = 安全確保のため」という使い分けが正解**という認識に、公式側も寄ってきている。


解決策:選択肢は 2 つ

① Python スクリプト版(個人利用・シンプルに始めたい人向け)

GitHub Issue #18846 発祥の Python 版。
クォート対応・リダイレクト除去・deny ルール対応まで含まれていて、動作も安定している。

~/.claude/hooks/smart_approve.py を作成

#!/usr/bin/env python3
import json, sys, re
from pathlib import Path

def load_patterns():
    settings_file = Path.home() / ".claude" / "settings.json"
    if not settings_file.exists():
        return [], []
    with open(settings_file) as f:
        s = json.load(f)
    perms = s.get("permissions", {})
    def extract(rules):
        result = []
        for r in rules:
            if r.startswith("Bash(") and r.endswith(")"):
                p = r[5:-1]
                p = re.sub(r':\*$', '', p)
                p = re.sub(r' \*$', '', p)
                result.append(p)
        return result
    return extract(perms.get("allow", [])), extract(perms.get("deny", []))

def matches(pattern, command):
    command = command.strip()
    return command == pattern or command.startswith(pattern + " ")

def decompose(cmd):
    # クォートを考慮してパイプ・&&・; で分割
    parts, current, in_sq, in_dq = [], [], False, False
    i = 0
    while i < len(cmd):
        c = cmd[i]
        if c == '\\' and not in_sq:
            current.append(c)
            if i + 1 < len(cmd):
                current.append(cmd[i+1]); i += 2
            continue
        if c == "'" and not in_dq:
            in_sq = not in_sq
        elif c == '"' and not in_sq:
            in_dq = not in_dq
        elif not in_sq and not in_dq:
            if c == '|' or c == ';' or (c == '&' and i+1 < len(cmd) and cmd[i+1] == '&'):
                parts.append(''.join(current).strip())
                current = []
                if c == '&': i += 2; continue
                i += 1; continue
        current.append(c); i += 1
    parts.append(''.join(current).strip())
    return [p for p in parts if p]

def main():
    try:
        data = json.load(sys.stdin)
    except Exception:
        sys.exit(0)
    command = data.get("tool_input", {}).get("command", "")
    if not command:
        sys.exit(0)
    allow_patterns, deny_patterns = load_patterns()
    if not allow_patterns:
        sys.exit(0)
    stages = decompose(command)
    for stage in stages:
        clean = re.sub(r'\d*>[>&]\d*', '', stage).strip()
        if not clean:
            continue
        if any(matches(p, clean) for p in deny_patterns):
            sys.exit(0)
        if not any(matches(p, clean) for p in allow_patterns):
            sys.exit(0)
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "allow",
            "permissionDecisionReason": "All stages matched allow patterns"
        }
    }))

if __name__ == "__main__":
    main()

実行権限を付与する。

chmod +x ~/.claude/hooks/smart_approve.py

~/.claude/settings.json に Hooks を登録

{
  "permissions": {
    "allow": [
      "Bash(ls)",
      "Bash(grep)",
      "Bash(git status)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /Users/yourname/.claude/hooks/smart_approve.py",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

command のパスはチルダ(~)展開が環境によっては効かないケースがあります。/Users/yourname/ のようにフルパスで書くのが確実です。

Claude Code を再起動すれば完了。


② Rust 製ライブラリ版(チーム運用・監査ログが必要な人向け)

海外エンジニアが公開した OSS、kornysietsma/claude-code-permissions-hook

TOML ファイルで allow/deny ルールを正規表現で定義でき、監査ログ機能まで標準搭載。
複数人で Claude Code を使っているチームや、「誰がいつどのコマンドを実行したか」を記録したい場合に特に有効。

[audit]
audit_file = "/tmp/claude-tool-use.json"
audit_level = "matched"  # off / matched / all から選択

[[allow]]
tool = "Bash"
command_regex = "^git (status|log|diff|branch)"
command_exclude_regex = "&|;|\\||`|\\$\\("  # シェルインジェクション防止

[[allow]]
tool = "Read"
file_path_regex = "^/Users/yourname/projects/.*"
file_path_exclude_regex = "\\.\\./"  # パストラバーサル防止

[[deny]]
tool = "Bash"
command_regex = "^rm .*-rf"

Python 版と比べると導入コストは上がるが、セキュリティと可視性が段違い。


どれを選べばいいか

状況 おすすめ
個人利用、手軽に始めたい ① Python 版
チーム利用、監査ログが欲しい ② Rust 製ライブラリ

「ワンコマンドでインストールできるツール」を紹介している記事も見かけますが、出所不明の npx xxx を実行するのは危険です。2026年4月、Claude Code の npm エコシステムを狙ったサプライチェーン攻撃が実際に発生しており、トロイの木馬入りパッケージが出回りました。Hook 系のツールはコマンド実行前に割り込む仕組み上、悪意あるコードが混入していると被害が大きくなります。公式リポジトリか、中身を自分で読めるスクリプトだけを使うようにしてください。


Hooks を入れても残る罠

「よし、解決!」と思ったら、もう一段罠がある笑

罠①:クォート内のパイプ

ls -la /path/ | grep -E "Knowledge|Skill"

grep -E "Knowledge|Skill" の中の | を、スクリプトがパイプと誤認識して分割してしまうケースがある。
ネットに転がっているシンプルな Hook スクリプトをそのまま使うと踏む。上記の Python 版・Rust 版はどちらもクォート対応済みなので問題ない。

罠②:git -C フラグ問題

海外の Issue(#27803#36900)で報告されていたケース。

# Bash(git status) を設定していても通らない
git -C /path/to/repo status

git コマンドを許可していても、-C フラグ付きで実行されるとパターンにマッチしない。
対策は Hook の判定ロジックを正規表現ベースに拡張すること(② Rust 版が有利)。

罠③:Edit / Write の Hook deny バグ

海外 Issue(#37210)で報告されている。
Hook で permissionDecision: "deny" を返しても、Edit/Write はファイルが変更されてしまうケースがある。
気になる人はファイルを chmod 444 で読み取り専用にする二重防御を検討してほしい。


まとめ

  • settings.json のパーミッション設定が効かないのはバグ。GitHub に 30 件以上の Issue があり、世界中の開発者が同じ問題を抱えている
  • バグの正体は「パイプで繋いだ複合コマンドがパターンマッチしない」こと
  • 解決策は PreToolUse Hooks でパーミッション判定を自前実装すること
  • 個人利用なら Python 版、チーム利用なら Rust 製ライブラリがおすすめ
  • Hooks にもクォート内パイプ・-C フラグ・Edit/Write の罠があるので注意
  • 出所不明の npx ツールは使わない

公式が修正してくれるのが一番だが、それまでの現実的な対策として参考にしてみてほしい。
バグが直ったら Hooks を外せばいいだけなので、導入コストはそこまで高くない。


おわりに

Claude Code、本当に便利なんですよ。だからこそパーミッションまわりのバグが余計に目立つ笑
「設定したのに聞いてくる」ストレスをなくして、もっと快適に使えるようになれば幸いです。

また、「こんな課題があるんだけど解決できないかな」といったことをお持ちでしたらぜひご連絡ください。可能な限り対応してみたいと思います(対応した場合には記事に投稿させていただきますのでその点はご容赦ください)。
X はこちら

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?