対象
- Claude Codeを日常的に使っている人
- パーミッション確認ダイアログに疲れている人
- HookやSkillの実用例を知りたい人
1. 「Always allow」を押し続けた末路
Claude Codeを使い込んでいると、頻繁にこんなダイアログが出てきます。
「このコマンドを実行してもよいですか?」
最初は丁寧に確認していたのですが、慣れてくるとつい「Always allow」を連打しがちになります。その結果、気づいたら settings.local.json がこんな状態になっていました。
{
"permissions": {
"allow": [
"Bash(done)",
"Bash(then mv \"$file\" \"99_archive/\")",
"Bash(\"/Users/xxx/project/xxx/00_inbox/xxx.md\" )",
// ... 228件続く
]
}
}
231件。しかも中身を見ると Bash(done) や Bash(fi) といったシェルスクリプトの制御構文の断片が永続許可として登録されていました。
「さすがにこれはまずい」と感じ、自動で整理・レビューする仕組みを作りました。
2. 何が問題か
-
プロジェクトローカルにしか効かない —
settings.local.jsonはそのリポジトリ専用です。別のプロジェクトで同じgit addを実行してもまた確認ダイアログが出ます。 -
ゴミルールが大量蓄積 —
Bash(done)やBash(fi)のようなシェルスクリプトの断片、特定ファイルパスのワンタイム的な操作が永続許可として残り続けます。 -
本来グローバルで許可すべきものが埋もれる —
Bash(git add:*)やReadのような汎用操作も個別のコマンド文字列として記録されてしまい、グローバル設定に昇格されることなく眠り続けます。
3. Claude Codeのパーミッションシステムを理解する
パーミッションファイルには3層構造があります。
| ファイル | スコープ | 役割 |
|---|---|---|
~/.claude/settings.json |
グローバル(全プロジェクト共通) | allow / hooks / env など |
~/.claude/setting.json |
グローバル(deny専用) | 絶対に許可しないルール |
<project>/.claude/settings.local.json |
プロジェクトローカル | gitignore対象のallow蓄積場所 |
ルールの書き方はツール名とオプションの組み合わせです。
{
"permissions": {
"allow": [
"Write", // ツール全体を許可
"Bash(git add:*)", // Bashの特定コマンドをワイルドカード許可
"WebFetch(domain:qiita.com)", // 特定ドメインのみ許可
"Skill(obsidian-workflow-manager)" // 特定スキルのみ許可
],
"deny": [
"Bash(git push:*)",
"Bash(rm:*)"
]
}
}
denyはallowより優先される点が重要です。グローバルでallowしていても、denyに記載があれば必ずブロックされます。
4. 作ったもの:全体アーキテクチャ
[ツール使用]
│
▼ (PreToolUse hook)
permission-logger.sh
│
▼
~/.claude/logs/permissions/YYYY-MM-DD.jsonl (蓄積)
│
├──── [Stop hook] ──► permission-review-check.sh
│ │
│ ▼ (20件以上で通知)
│ 「📋 パーミッションログがN件溜まっています」
│ │
│ ▼ (CLAUDE.mdの指示で自動実行)
│
▼ (/permission-review)
SKILL.md の7ステップ処理
│
▼
settings.json / settings.local.json を更新
│
▼
.last_review_ts マーカー更新 (次回カウントのリセット)
| コンポーネント | 役割 | 実行タイミング |
|---|---|---|
| permission-logger.sh | 全ツール呼び出しをJSONLに記録 | PreToolUse(自動) |
| permission-review-check.sh | ログ件数をカウントしリマインド | Stop(自動) |
| permission-review スキル | ログ分析→整理提案→適用 | /permission-review(手動 or 自動) |
5. セットアップ手順
GitHubリポジトリにスクリプト・スキル一式を置いています。
https://github.com/daikin555/permission-review
前提条件として jq コマンドが必要です。
brew install jq
ステップ1 スクリプトの配置
permission-logger.sh と permission-review-check.sh を ~/.claude/scripts/ に配置して実行権限を付与します。
mkdir -p ~/.claude/scripts
cp permission-logger.sh ~/.claude/scripts/
cp permission-review-check.sh ~/.claude/scripts/
chmod +x ~/.claude/scripts/*.sh
ステップ2 スキルの配置
SKILL.md を ~/.claude/skills/permission-review/ に配置します。グローバルに置くことで、どのリポジトリからでも /permission-review として呼び出せます。
mkdir -p ~/.claude/skills/permission-review
cp SKILL.md ~/.claude/skills/permission-review/
ステップ3 フックの登録
~/.claude/settings.json にPreToolUseとStopのフックを追加します。
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/permission-logger.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/permission-review-check.sh"
}
]
}
]
}
}
さらに全自動にしたい場合は、CLAUDE.md に以下の一文を追加します。
## Permission Review
Stopフックから「📋 パーミッションログが〇件溜まっています」というメッセージが届いたら、
自動的に `/permission-review` スキルを実行する
これだけで、セッション終了のたびにClaudeが自分でパーミッションを整理してくれるようになります。
6. 仕組みの詳細
permission-logger.sh(ログ収集)
PreToolUseフックで呼び出され、全ツール呼び出しをJSONL形式で記録します。
#!/bin/bash
LOG_DIR="$HOME/.claude/logs/permissions"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$(date +%Y-%m-%d).jsonl"
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
SUMMARY=$(echo "$INPUT" | jq -c '.tool_input // {} | {
command: .command,
file_path: .file_path,
url: .url,
skill: .skill
} | with_entries(select(.value != null))' 2>/dev/null || echo "{}")
echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"tool\":\"$TOOL_NAME\",\"summary\":$SUMMARY}" >> "$LOG_FILE"
exit 0
設計のポイントは2点あります。
- 必ず
exit 0で終了する — ブロックせずツール実行を継続させるため - 重要フィールドだけ保存する — ファイルサイズの肥大化を防ぐため
記録されるログはこのようになります。
{"ts":"2026-02-18T20:59:07Z","tool":"Bash","summary":{"command":"git add 02_atomic/foo.md"}}
{"ts":"2026-02-18T20:59:09Z","tool":"Read","summary":{"file_path":"/Users/itoudaichi/.claude/settings.json"}}
{"ts":"2026-02-18T20:59:13Z","tool":"Skill","summary":{"skill":"obsidian-workflow-manager"}}
パターン正規化(スキルの核心)
スキルの中核処理は、実際のコマンド文字列をパーミッションルール形式に変換するパターン正規化です。
| 実際のコマンド | 正規化後のパターン |
|---|---|
git add 02_xxx/foo.md |
Bash(git add:*) |
git commit -m "fix: ..." |
Bash(git commit:*) |
python3 << 'EOF' ... |
Bash(python3:*) |
https://qiita.com/article/123 |
WebFetch(domain:qiita.com) |
Read |
Read(そのまま) |
| skill名あり | Skill(スキル名) |
コマンドの先頭語を抽出して Bash(コマンド:*) 形式にまとめることで、個別のファイルパスや引数を気にせず汎用的なルールを生成できます。
7. 実際に動かした結果
実際に1日分のログを分析した結果を示します。
分析済みログ 1ファイル(2026-02-19.jsonl)/ 総ツール呼び出し数 165回
グローバルへの追加推奨として以下が提案されました。
| パターン | 回数 |
|---|---|
Read |
約50回 |
Edit |
約18回 |
Bash(python3:*) |
約10回 |
Bash(grep:*) |
約8回 |
Bash(ls:*) |
約6回 |
Bash(find:*) |
約5回 |
ローカルから削除候補として40件以上のシェル制御構文の断片が挙がりました。
Bash(done)
Bash(then)
Bash(else)
Bash(fi)
Bash(do if [ -f "$file" ])
...(40件以上)
適用後の結果です。
グローバルへ追加: 12件
+ Read, Edit, Write, Glob
+ Bash(ls:*), Bash(grep:*), Bash(echo:*)
+ Bash(find:*), Bash(mkdir:*), Bash(head:*)
+ Bash(wc:*), Bash(chmod:*)
ローカルから削除: 48件(シェル断片 + グローバル重複)
ローカル残存ルール: 182件(プロジェクト固有のルールが残る)
231件 → 182件(ローカル)+ 12件(グローバル)という形でスッキリしました。
8. 設計で学んだこと
PreToolUseはPermission判定の後に発火する
フックの発火タイミングを最初勘違いしていました。実際のフローはこうなっています。
Claudeがツールを呼び出そうとする
↓
Permissionチェック(settings.json / settings.local.json を参照)
↓ allowの場合のみ
PreToolUse Hook 発火 ← ログを取れるのはここ
↓
ツール実行
↓
PostToolUse Hook 発火
「拒否」されたログは現在のClaude Codeでは取得できません。ただし、hookに届いたものはすべて許可された操作という保証でもあります。今回の目的は「許可した操作の整理」なので、実用上はまったく問題ありません。
exit codeの意味
| exit code | 効果 |
|---|---|
0 |
通常実行を継続(stdoutはClaudeへのフィードバック) |
2 |
ツール実行をブロック(stderrが表示される) |
| その他 | hook自体の失敗として扱われる |
ログ収集用のhookは必ず exit 0 で終わらせてください。誤って exit 2 にするとあらゆるツール実行がブロックされます。
グローバル/ローカルの使い分け原則
| 配置先 | 何を置くか | 例 |
|---|---|---|
グローバル (settings.json) |
全プロジェクト共通の汎用操作 |
Read, Edit, Bash(ls:*)
|
ローカル (settings.local.json) |
プロジェクト固有の操作 | Skill(obsidian-workflow-manager) |
グローバルdeny (setting.json) |
絶対に禁止する操作 |
Bash(git push:*), Bash(rm:*)
|
スキルはグローバルに配置する
最初はスキルをプロジェクト内の .claude/skills/ に置いていました。この場合、そのリポジトリ内でしか使えません。~/.claude/skills/ に置くことで、どのリポジトリからでも /permission-review として呼び出せるようになります。スキルは基本的にグローバルに置くのが正解です。
9. まとめ
- PreToolUse hookで全ツール呼び出しをログ収集 — JSONLで日付別に蓄積
- Stopフックで自動リマインド — セッション終了時に件数を通知
- スキルがログを分析して整理を提案 — グローバル移行 / ゴミ削除を半自動化
- 結果として231件 → 182件(ローカル)+ 12件(グローバル)でスッキリした
GitHubリポジトリ: https://github.com/daikin555/permission-review
今後の改善余地
- 複数プロジェクトのローカル設定を横断的に比較・統合する機能
- ログローテーション(30日以上前のファイルを自動削除)
- Stopフックの通知しきい値をプロジェクト単位で設定できるようにする