「あ、そういえば API キーが console に出てた」
エージェントが自動で動いてる間に予期しない操作(git push --force、rm -rf、API キー露出)をして、気付くのが数分後——という経験はありませんか。
従来は属人的な判断に頼っていましたが、本番環境では仕組みレベルで防御を多層化する必要があります。
そこで活躍するのが Claude Code の hooks と permissions deny リスト です。この 4 層の防御パターンを解説します。
この記事で学べること
PreToolUse/PostToolUsehooks の設計パターンと使い分け- Bash sandbox egress 隔離 + secret-mask hook での多層防御
- git ブランチ保護 hook による push 制御
- settings.json deny ルールの分類戦略
前提条件
- Claude Code v2.1.136 以上
- git, gh CLI がインストール済み
- Python 3 で hook scripts が実行可能な環境
多層防御の構図:Hooks + Deny Rules の役割分担
防御は 4 層に分かれます。
- PreToolUse — 環境変数検知、ファイルパス確認
- Deny rules —
git push --force、rm -rf/*等拒否 - PostToolUse + Bash sandbox — API キー等の自動マスク
- OS sandbox — network/filesystem を permit list で制限
どれか一つ失敗しても他が補完します。
層1: PreToolUse Hooks — 実行前の credential 検知
使用例:Bash credential guard
エージェントは条件を見ません。.env.production に POSTGRES_URL が書いてあると理由を聞かずに read します。無意識の参照を防ぐのがこのフックです。
{
"type": "PreToolUse",
"matcher": "Bash(.*)",
"hooks": [
{
"type": "command",
"command": "bash \"$HOME/.claude/hooks/pretool-bash-credential-guard.sh\"",
"timeout": 5,
"statusMessage": "prod credential 検知中..."
}
]
}
以下を検知したら ask を返します。
| 検知対象 | 正規表現 |
|---|---|
| 環境変数参照 | $PROD_* / ${PRODUCTION_*} / $LIVE_* |
| ファイル参照 | .env.production / .env.prod |
| AWS profile | --profile <name> で prod 含む |
完全ブロック(deny)ではなく ask にすることで、誤検知時の逃げ道を確保しています。開発環境の環境変数名が PRODUCTION_FALLBACK のようなパターンでも、_ サフィックス必須にすることで $PRODUCER は除外されます。
実装例:Bash credential guard hook
本番環境への credential アクセス検知の実装例です。~/.claude/hooks/ に保存します。
#!/usr/bin/env bash
# pretool-bash-credential-guard.sh
# PreToolUse hook for detecting production credential access in Bash commands.
# Reads the command string from stdin and returns a JSON decision.
set -e
# Read input JSON from stdin
INPUT_JSON=$(cat)
# Extract the command string from the input
COMMAND=$(echo "$INPUT_JSON" | grep -o '"command":"[^"]*"' | sed 's/"command":"//' | sed 's/"$//')
# Pattern definitions — customize these to match your environment's naming conventions
# Production environment variable prefixes
PROD_VAR_PATTERNS=(
'\$PROD_'
'\$PRODUCTION_'
'\$LIVE_'
'\${PROD_'
'\${PRODUCTION_'
'\${LIVE_'
)
# Production config files
PROD_FILE_PATTERNS=(
'\.env\.prod'
'\.env\.production'
'config/prod\..*'
'config/production\..*'
)
# AWS/Cloud credentials (profile or direct reference)
CLOUD_PATTERNS=(
'--profile.*prod'
'--profile.*production'
'--profile.*live'
'AWS_PROFILE.*prod'
)
# Function to check if command matches any pattern
check_pattern() {
local cmd="$1"
local pattern="$2"
if [[ "$cmd" =~ $pattern ]]; then
return 0
fi
return 1
}
# Check all patterns
DETECTED_PATTERN=""
for pattern in "${PROD_VAR_PATTERNS[@]}"; do
if check_pattern "$COMMAND" "$pattern"; then
DETECTED_PATTERN="Production environment variable: $pattern"
break
fi
done
if [ -z "$DETECTED_PATTERN" ]; then
for pattern in "${PROD_FILE_PATTERNS[@]}"; do
if check_pattern "$COMMAND" "$pattern"; then
DETECTED_PATTERN="Production config file: $pattern"
break
fi
done
fi
if [ -z "$DETECTED_PATTERN" ]; then
for pattern in "${CLOUD_PATTERNS[@]}"; do
if check_pattern "$COMMAND" "$pattern"; then
DETECTED_PATTERN="Cloud credential reference: $pattern"
break
fi
done
fi
# Output JSON decision
if [ -n "$DETECTED_PATTERN" ]; then
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "Production credential access detected: $DETECTED_PATTERN. Please confirm this is intentional."
}
}
EOF
else
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "No production credential patterns detected."
}
}
EOF
fi
層の使い分け:PreToolUse の役割
PreToolUse は「実行前の intent チェック」が目的で、本番環境への credential アクセスをプロンプト段階で検出しユーザーに確認させます。完全防御には不十分なため後段と多層設計にします。
層2: Deny Rules — 宣言的防御ルールの分類戦略
「まぁ実行されないでしょ」と思うのが人間の心理ですが、実行されます。permissions.deny リストは以下の categories で整理し予期しない操作をブロックします。
Git 操作(強制push・保護ブランチ操作)
{
"type": "Bash",
"command": "git push --force*",
"decision": "deny"
}
--force-with-lease のような「安全な強制push」もブロックします。push事故は3時間後にログで気付く(遅い)ことが多いため、force が付いたら full stop します。
Git 破壊的操作の判断基準
| コマンド | 判断 | 理由 |
|---|---|---|
git reset --hard | 許可推奨 | reflogで復旧可(90日) |
git branch -D | 許可推奨 | refジャーナルで復旧可 |
git clean -f | deny | ファイル削除は復旧不可 |
git checkout -- . | 許可推奨 | staged changesのみ影響 |
rm -rf /* | deny | 復旧困難 |
chmod -R /* | 許可推奨 | 再設定可能 |
判断基準:reflog で復旧可能な操作は許可、unrecoverable な操作は拒否。
System 操作(sudo、rm -rf、dd、mkfs)
{
"type": "Bash",
"command": "sudo *",
"decision": "deny"
}
{
"type": "Bash",
"command": "rm -rf /*",
"decision": "deny"
}
sudo、rm -rf、mkfs は環境を壊す危険性が高いため deny です。
Deny ルール設計の実践的なポイント
- Git: 強制push
- System: sudo, rm -rf
- Network: secret埋め込み疑い
- 機密ファイル read:
.env直接read
この4軸で整理すると保守性が高まります。
層3: PostToolUse Hooks — 実行後の秘匿情報フィルター
Bash output には API キー等が埋め込まれて返ることがあります。そのまま会話履歴に流れると過去ログから抽出できてしまうため、posttool-secret-mask.sh hook が活躍します。
Secret Mask Hook の検知ルール
# URL-embedded credentials (最大の漏洩源)
postgresql://user:pass@host:5432/db
redis://default:password@localhost:6379
# High-value tokens
AKIA...(AWS Access Key)
ghp_/gho_/ghs_/ghu_/ghr_(GitHub Personal Token)
sk-ant-(Anthropic API Key)
sk-(OpenAI互換、32文字以上)
# Fallback: 環境変数形式
*_TOKEN=... / *_KEY=... / *_SECRET=... / *_PASSWORD=...
実装は Perl -0777 slurp + 順序付き regex です。具体 prefix を先に処理することで誤検知を排除しています。
Secret Masking の設計原則
実運用の課題は3つ。URL-embedded credentialsは環境変数形式では検出不可。Lowercase/camelCaseは漏れやすい。JSON inline formatはvalue部をselective mask。real .env files で検証が重要です。
実装パターン:Protect-Branches と Memory-Monitor
protect-branches.py:PR merge の gate 判定
feature branch から保護ブランチへ誤って push・merge するのを防ぐ hook です。
#!/usr/bin/env python3
import json
import sys
import subprocess
def parse_refspec(refspec: str) -> tuple:
"""refspec をパース(git の場合)
Example: refs/heads/feature/issue-123:refs/heads/main
"""
parts = refspec.split(':')
local_ref = parts[0].replace('refs/heads/', '')
remote_ref = parts[1].replace('refs/heads/', '') if len(parts) > 1 else 'main'
return local_ref, remote_ref
# PreToolUse で受け取った JSON から指定 branch を抽出
input_json = json.loads(sys.stdin.read())
target_branch = input_json.get('tool_input', {}).get('branch')
# gh pr view で base branch を確認
result = subprocess.run(
['gh', 'pr', 'view', '--json', 'baseRefName'],
capture_output=True,
text=True
)
base_branch = json.loads(result.stdout).get('baseRefName')
# 保護ブランチへのマージは ask 判定(デフォルトは main/master/production)
protected_branches = ('main', 'master', 'production')
if base_branch in protected_branches:
print(json.dumps({
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'permissionDecision': 'ask',
'permissionDecisionReason': f'Merging {target_branch} to protected branch: {base_branch}'
}
}))
else:
print(json.dumps({
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'permissionDecision': 'allow',
'permissionDecisionReason': f'Regular branch merge: {base_branch}'
}
}))
PreToolUse(Bash(gh pr merge*)) で呼ばれます。gh api で base branch を確認し、保護ブランチへのマージはユーザーに確認を強制します。
memory-monitor.py:リソース監視と hang 検知
memory の肥大化や CPU のハング状態を PostToolUse で check します。
#!/usr/bin/env python3
import psutil
import json
import sys
import time
proc = psutil.Process()
mem = proc.memory_info()
cpu_percent = proc.cpu_percent(interval=0.1)
rss_mb = mem.rss / 1024 / 1024
# threshold 設定(環境に応じてカスタマイズ)
memory_threshold_mb = 500
cpu_threshold_percent = 80
warnings = []
# リソース監視:memory 超過
if rss_mb > memory_threshold_mb:
warnings.append({
'type': 'warning',
'category': 'memory',
'message': f'Memory usage: {rss_mb:.1f} MB (threshold: {memory_threshold_mb}MB)',
'action': 'Consider session restart or reduce workload'
})
# リソース監視:CPU hanged(固い loop か I/O 待機)
if cpu_percent > cpu_threshold_percent:
warnings.append({
'type': 'critical',
'category': 'cpu',
'message': f'CPU utilization: {cpu_percent}% (likely hung or I/O bound)',
'action': 'Check process status, consider Stop'
})
for warning in warnings:
print(json.dumps(warning), file=sys.stderr)
Timeout と I/O 待機の設計判断
「エージェントがハング状態になってた」経験はありませんか?外部API連携でI/O待機timeoutが多いです。
判断基準:
- デフォルト timeout が不足: API レイテンシ+遅延で180 秒超えることがある
- Hang 判定のタイミング: timeout前にmemory/CPUを check し早期検知する
- Threshold の設定: 測定してから設定(例:512MB、72時間稼働なら1GB許容)
層4: Bash Sandbox — OS レベルの egress 隔離
PreToolUse / PostToolUse 以上に強力な防御が、Bash 実行時の OS-level sandbox です。エージェント本人も信頼できないという前提で、macOS Seatbelt を活用し network egress を許可リストベースで制限します。
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": {
"allowedDomains": [
"github.com",
"api.github.com",
"codeload.github.com",
"registry.npmjs.org",
"*.npmjs.org",
"pypi.org",
"files.pythonhosted.org",
"crates.io"
]
},
"filesystem": {
"denyRead": [
"~/.aws",
"~/.ssh",
"~/.gnupg",
"~/.config/gh"
]
},
"excludedCommands": [
"gh:*",
"git:*",
"~/.claude/skills/*/bash/*",
"~/.claude/skills/*/python3/*"
]
}
}
Sandbox 設計の3つのポイント
1. allowedDomains は whitelist ベース
npm, GitHub, PyPI 等の endpoint のみ許可。その他は prompt で ask します。
許可例: github.com, registry.npmjs.org, pypi.org
禁止例: attacker.com, malicious-logging-service.net
2. denyRead は credential dir のみ
.env は deliberately NOT included。秘匿情報は PostToolUse hook(層3)で処理するのが正しい層で、「強度」と「使いやすさ」のバランスを取っています。
deny read: ~/.aws, ~/.ssh, ~/.gnupg, ~/.config/gh
allow read: .env(PostToolUse で mask する)
3. excludedCommands は interpreter 非依存の path ベース
gh / git は除外しますが、generic な全除外は危険です。keychain アクセスが必要で sandbox 内動作が困難なため、個別除外の粒度を取ります。
Hook パターン:Auto-Approve
「限定しつつセットアップを自動化して安全性を担保」する hook の活用パターンです。
使用例:.worktreeinclude を自動生成する
git worktree を作ると .env のような gitignore されたファイルはコピーされません。これを解決する仕組みが .worktreeinclude です。ルートに置いたこのファイルを読み、.gitignore 構文で列挙されたファイルを新しい worktree へコピーします。安全のため両方にマッチするファイルだけがコピー対象です。
git worktree add の PreToolUse で .env* をスキャンし、自動生成しておきます。
#!/usr/bin/env bash
set -euo pipefail
# PreToolUse hook for "git worktree add":
# リポジトリルートに .worktreeinclude を自動生成し、
# .env や local config が新しい worktree にコピーされるようにする。
GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
WORKTREEINCLUDE="$GIT_ROOT/.worktreeinclude"
# 既にあれば何もしない(冪等)
[ -f "$WORKTREEINCLUDE" ] && exit 0
# .env* をスキャン(node_modules や .git などは除外)
ENV_PATTERNS=$(find "$GIT_ROOT" -name '.env*' \
-not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null \
| xargs -r -I{} basename {} | sort -u)
{
echo "# Auto-generated: patterns to copy into new worktrees"
while IFS= read -r f; do
[ -n "$f" ] || continue
echo "$f"
echo "**/$f"
done <<< "$ENV_PATTERNS"
echo ".claude/settings.local.json"
} > "$WORKTREEINCLUDE"
生成先はリポジトリルートで、PreToolUse で git worktree add の前に用意します(作成後では間に合わない)。すでにあれば触らないので何度実行しても安全。ブロックでなく allow + 自動化で UX を損なわない使い方です。
Auto-Approve の使い分け
| Hook | 判定 | 用途 |
|---|---|---|
| PreToolUse | ask/deny/auto-setup | 本番接触・強制push は ask。.worktreeinclude 生成 |
| PostToolUse | allow&filter | output sanitize、秘匿情報 mask |
| SessionStart | context-load | プロジェクト状態を読み込む |
実装ガイド:層別の設計判断フロー
エージェントの長時間実行安全性
├─ Layer 1(実行前): PreToolUse
│ ├─ 本番環境 credential 検知 → ask
│ ├─ git branch/refspec 検証 → ask if main/production
│ └─ worktree 初期化 → PreToolUse で .worktreeinclude 自動生成
├─ Layer 2(設定): Deny rules
│ ├─ git 強制push → deny
│ ├─ rm -rf / sudo → deny
│ └─ reflog-recoverable ops(reset --hard 等)→ allow
├─ Layer 3(実行後): PostToolUse
│ ├─ 秘匿情報 mask(URL-embedded creds)
│ ├─ リソース監視(CPU / memory)
│ └─ Output sanitize
└─ Layer 4(実行環境): Bash sandbox
├─ Network allowedDomains(whitelist)
├─ Filesystem denyRead(credential dir)
└─ Trusted tools exclude(gh, git path-based)
| 脅威モデル | 層 | 判定 |
|---|---|---|
| 本番環境への無意識的アクセス | 1 | ask |
| git 強制push による履歴破損 | 2 | deny + ask |
| API key ログ漏洩 | 3 | mask output |
| 長時間実行の hang / OOM | 3 | alert |
| 未承認の外部通信 | 4 | deny + prompt |
まとめ:多層防御で信頼できるエージェント運用
Claude Code を本番で使うとき、「あ、API キー出てた」を避けるにはApplication → File → OSの4層防御が必要です。
各層の責務
| 層 | 実装 | 復旧可能性 |
|---|---|---|
| PreToolUse | credential 検知、refspec 確認 | ユーザー確認で prevent |
| Deny Rules | コマンドパターンフィルター | 設定で deterministic に block |
| PostToolUse | 秘匿情報 mask、リソース警告 | Output を後処理で sanitize |
| Bash Sandbox | Network / filesystem 隔離 | OS-level で物理的に block |
設計の実践的ポイント
- 完全ブロックより ask を多用: 誤検知時の逃げ道確保
- 復旧可能性で判定: reflog で復旧できる操作は allow、復旧不可な操作は deny
- 実測ベースの threshold: テストデータセットで検証して設定
- Trusted tools の個別除外: regex で「全除外」は危険。path ベースで限定的に除外
なお、実際に入れてみると「安全なコマンドまで確認を求められる」といった別の詰まりに出会うことがあります。その切り分けは「Claude Code の hook を入れたら開発が止まった」で扱っています。



