「設定したのになぜ?」Claude Codeのパーミッションが効かない理由と、今すぐできる対策
はじめに
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 ユーザーが同じ壁にぶつかっている笑
しかも、罠は Bash だけじゃない。Edit / Write 系にも同じ根っこから生えた別の罠がある。
この記事では両方の正体と「今すぐできる対策」をまとめます。
そもそも 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 -la は Bash(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 = 安全確保のため」という使い分けが正解**という認識に、公式側も寄ってきている。
解決策(Bash 編):選択肢は 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 を入れても残る罠(Bash 編)
「よし、解決!」と思ったら、もう一段罠がある笑
罠①:クォート内のパイプ
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 版が有利)。
実は Bash だけじゃなかった:Edit / Write 系にも同型の罠
ここまでが Bash パーミッションの話。
…で終わらせたかったんですが、本記事を書いている直前、僕自身が 同じ permissions システムの中で、別カテゴリの罠 にやられました。Edit / Write 系の話です笑
要件はめちゃくちゃシンプルだったんです。
「
session_log.mdを Claude Code に追記してもらうたびに毎回 yes 確認されるので、自動承認したい」
これだけ。
これだけのことに、4 段階のハマり方を経て、ようやく解決しました。
同じ4段階を踏まずに済むよう、順番に書き残しておきます。
罠①:絶対パスは / 1 つじゃ効かない、// 2 つが必須
最初に書いたのはこれ。
"Edit(/Users/yourname/project/.claude/memory/session_log.md)"
動かない。
なんで?と思って公式 docs を読んだら、Edit / Write の path 記法は Bash と違いました。
| 書き方 | 解釈 |
|---|---|
//path |
絶対パス(ファイルシステムルート) |
/path |
プロジェクトルート相対 |
path / ./path
|
cwd 相対 |
~/path |
ホームディレクトリ相対 |
つまり /Users/... は「プロジェクトルート配下の /Users/...」と解釈されて、永久にマッチしない。
正しくはこう。
"Edit(//Users/yourname/project/.claude/memory/session_log.md)"
/ を 2 つ重ねる。
Bash のノリで書くと踏む。同じ permissions システムなのに、なんで Bash と Edit/Write で構文が違うのか…笑
罠②:「Edit ルールは Write を含む」(サブエージェントが誤情報を出すことがある)
// に直したのにまだ効かない。
ここでサブエージェントに「これって Edit と Write 両方書かないとダメ?」と聞いたら、**「Edit ルールは Write を含まない、別途 Write(...) も追加せよ」**と回答が返ってきました。
追加してもまだ効かない。
おかしいなと思って公式 docs を直接 WebFetch して原文を読みに行ったら、こう書いてありました。
Edit rules apply to all built-in tools that edit files.
つまり Edit ルールは Write も自動的にカバーする。サブエージェントの回答が逆だったわけです。
ここで本記事冒頭の話とつながる教訓が出てきます。
Claude Code の permissions は、情報源によって解釈にズレが出るほど癖がある。
公式 docs を直接 WebFetch して原文を読むのが一番安全。
ググって出てきた記事も、サブエージェントの回答も、最新の正解とは限らない。
バグも仕様も流動的に変わっているので、一次ソースを当たる癖をつけるのが結局一番早い。
罠③:File modification はデフォルトで「Until session end」リセット
これが最後の、そして一番性格の悪い罠でした。
Claude Code の確認ダイアログで「Yes, don't ask again」を押したときの挙動が、Bash と Edit / Write で違うんです。
| 種類 | 「Yes, don't ask again」の挙動 |
|---|---|
| Bash | Permanent(settings.json に保存される) |
| File modification (Edit/Write) | Until session end(セッション終わるとリセット) |
つまり、
「前のセッションで Yes 押したのに、新セッションでまた聞かれる」
という現象は、設定ファイルに残らない仕様が原因。
毎回聞かれる根本理由はここでした。
「設定したやん」じゃなくて「そもそも設定として保存されていなかった」。罠としてはかなり性格悪い部類。
この挙動は 2026 年 5 月時点の仕様です。Anthropic が今後変更する可能性があるので、最新の公式 docs を確認してください。
真の解決策:defaultMode: "acceptEdits"
罠①〜③ を全部解いた結論として、settings.local.json に 1 行追加するのが一番早いです。
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
...
]
}
}
これで以下が全部自動承認になります。
- プロジェクトの working directory 配下の 全ファイルへの Edit / Write
-
additionalDirectories配下も同様 -
mkdir/touch/mv/cpなどの filesystem 系コマンド
設定の反映には Cursor / VS Code の完全再起動が必要です。公式 docs に「automatically reload」の記載はありません。Claude Code をリロードしただけだと反映されないことがあるので注意。
acceptEdits の注意点(自衛策)
便利な反面、書き換えリスクは個別 allow より広い。最低限の自衛は以下。
-
意図しないファイル書き換え対策:
git diffで必ず差分を見る運用にする -
メモリ系ファイルの異常検出:行数制限などで「気付ける仕組み」を残す(僕は
MEMORY.mdを 200 行制限にしている) -
acceptEdits自体が Bash 系と同じ既知バグ群に該当する可能性は完全には否定できない:もし将来効かなくなったら、本記事のPreToolUse Hook方式を Edit / Write にも応用するプラン B を検討(hook input のtool_nameで Edit / Write を捕まえて自前判定する。未検証だが理論上は可能)
ちなみに:Hook で deny する方向にも別バグがある
「じゃあ Hook で Edit / Write をブロックすればいいじゃん」と考えた人へ、もうひとつ罠があります。
海外 Issue(#37210)で報告されているのですが、Hook で permissionDecision: "deny" を返しても、Edit / Write はファイルが変更されてしまうケースがある。
なので Edit / Write のコントロールは Hook ではなく、acceptEdits + git diff 運用 に寄せるのが結局一番安全です。
ブロックしたい特定ファイルがあるなら、chmod 444 で読み取り専用にする二重防御がおすすめ。
Bash 系と Edit / Write 系、対応表
整理するとこうなります。
| 主題 | バグの形 | 効く解決策 |
|---|---|---|
| Bash 系 |
Bash(ls *) がパイプ複合コマンドにマッチしない |
PreToolUse Hook で自前判定 |
| Edit / Write 系 |
/ vs // 構文ハマり + サブエージェント誤情報 + Until session end リセット |
defaultMode: "acceptEdits" |
両方踏むと完成、というか、両方踏まないと完成しない笑
両方踏まなくていいように、最初から両方先回りで設定するのがおすすめです。
最後の罠:同じファイル追記でも、Claudeは2つのルートを使い分ける
「Bash 系と Edit / Write 系を両方押さえれば完璧!」
…と思ったら、もう一個だけ罠があります笑
session_log.md のような「頻繁に追記してもらいたいファイル」を運用していると、Claude は 状況によって違うツールで書きに来る。同じ「1行追記」でも、ルートが2通りあるんです。
パターン A:Edit / Write ツールで書く
Edit("//path/to/session_log.md", old_string=..., new_string=...)
→ acceptEdits のラインで自動承認 ✓
パターン B:Bash でリダイレクトする
echo "新しいログ" >> /path/to/session_log.md
→ こっちは Bash 経由 なので acceptEdits は 効かない
→ Bash hook + Bash(echo) の allow 設定が必要
つまり、acceptEdits だけ設定して安心していると、Claude が Bash redirect で来たとき毎回聞かれる。
「同じファイルを書くのに、ルートが違うだけで挙動が変わる」、これは盲点でした。
完全に黙らせる allow 設定
両方のルートをカバーするには、log/append 系で使われそうな Bash コマンドも allow リストに入れておくのがおすすめです。
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Bash(ls)",
"Bash(grep)",
"Bash(git status)",
"Bash(echo)",
"Bash(cat)",
"Bash(tee)"
]
}
}
これで、
-
Edit / Write 経由 →
acceptEditsで承認 -
Bash echo >> 経由 → Hook がリダイレクト除去 →
Bash(echo)にマッチして承認 -
Bash tee -a 経由 → 同じく
Bash(tee)にマッチして承認
どのルートで書きに来ても聞かれない構成になります。
対応表で2系統覚えればOK、と思ったらまだあった、というオチでした笑
まとめ
-
settings.jsonのパーミッション設定が効かないのはバグ。GitHub に 30 件以上の Issue があり、世界中の開発者が同じ問題を抱えている - Bash 系のバグの正体は「パイプで繋いだ複合コマンドがパターンマッチしない」こと
- Bash の解決策は
PreToolUse Hooksで自前判定。個人利用なら Python 版、チーム利用なら Rust 製ライブラリ - Hooks にもクォート内パイプ・
git -Cフラグの罠があるので注意 -
Edit / Write 系にも同じ permissions システムの中で別カテゴリの罠がある(
/vs//・Edit が Write を含む・Until session end リセット) - Edit / Write の解決策は
defaultMode: "acceptEdits"の 1 行 -
Claude は同じファイル操作でも Edit / Write ツールと Bash redirect の両方を使い分けるので、
acceptEditsだけでなく Bash の allow にもecho/tee/catなどを入れておく - 公式 docs を直接 WebFetch する癖をつける(ググり結果や AI の回答は古いことがある)
- 出所不明の
npxツールは使わない
公式が修正してくれるのが一番だが、それまでの現実的な対策として参考にしてみてほしい。
バグが直ったら Hooks も acceptEdits も外せばいいだけなので、導入コストはそこまで高くない。
おわりに
Claude Code、本当に便利なんですよ。だからこそパーミッションまわりのバグが余計に目立つ笑
「設定したのに聞いてくる」ストレスをなくして、もっと快適に使えるようになれば幸いです。
また、「こんな課題があるんだけど解決できないかな」といったことをお持ちでしたらぜひご連絡ください。可能な限り対応してみたいと思います(対応した場合には記事に投稿させていただきますのでその点はご容赦ください)。
X はこちら