「あ、そういえば API キーが console に出てた」
Claude Code をバックグラウンド実行や長時間セッションで動かすとき、こういう瞬間、ありませんか。エージェントが自動で動いてる間に、予期しない操作(git push --force、rm -rf、API キー露出)をしてしまって、気付くのが数分後。
従来は「このコマンドは危ないから avoid リスト」という属人的な判断に頼っていたわけですが、本番環境では仕組みレベルで防御を多層化する必要があります。設定ファイルで宣言的に deny する。実行時に動的に判定する。実行後に秘匿情報をフィルターする。OS レベルで通信を隔離する。
そこで活躍するのが Claude Code の hooks と permissions deny リスト です。本記事では、この 4 層の防御を組み合わせた、安全なエージェント設計パターンを解説します。
この記事で学べること
- Claude Code の
PreToolUse/PostToolUsehooks の設計パターンと使い分け - Bash sandbox egress 隔離 + secret-mask hook での多層防御
- git ブランチ保護 hook による push 制御の実装
- リソース監視 hook によるハング検知
- settings.json deny ルールの分類戦略と判断基準
前提条件
- Claude Code v2.1.136 以上(PostToolUse の
updatedToolOutputサポート) - git, gh CLI がインストール済み
- Python 3 で hook scripts が実行可能な環境
多層防御の構図:Hooks + Deny Rules の役割分担
防御は 4 層に分かれます。
- PreToolUse hooks — ユーザー入力段階での ask / deny。環境変数検知(
$PROD_*)、ファイルパス確認(.env.production)。「止めるなら今」の一番上流の砦 - Deny rules (settings.json) — コマンドパターンによる機械的フィルター。
git push --force、rm -rf/*、chmod -R/*等を自動拒否。hooks を抜け出た悪意に対する静的な防火壁 - PostToolUse hooks + Bash sandbox — 実行後のアウトプット処理。API キー、トークン、database credentials の自動マスク + egress 隔離。逃げられたデータを回収する最後の機会
- OS レベル sandbox — macOS / Linux のシステムコール制限。network、filesystem へのアクセスを permit list で制限。ツール自体が信頼できなくても環境を守る
各層が異なる責務を持つため、どれか一つ失敗しても他が補完します。
層1: PreToolUse Hooks — 実行前の credential 検知
使用例:Bash credential guard
「本番の API キーなんて読むわけない」と思うでしょ。でもエージェントは条件を見ない。.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 検知中..."
}
]
}
このスクリプトは以下を検知したら permissionDecision: "ask" で stop-and-ask を返します。
| 検知対象 | 正規表現 | 例 |
|---|---|---|
| 環境変数参照 | $PROD_* / ${PRODUCTION_*} / $LIVE_* | echo $PROD_API_KEY |
| ファイル参照 | .env.production / .env.prod | cat .env.prod |
| AWS profile | --profile <name> で prod 含む | aws --profile prod-account |
完全ブロック(deny)ではなく ask にすることで、誤検知時の逃げ道を確保しています。「ちょっと待って、これ本当に本番?」という確認を強制させる。開発環境の環境変数名が PRODUCTION_FALLBACK のようなパターンでも、_ サフィックス必須にすることで $PRODUCER は除外されます(= false positive を最小化)。
実装例:Bash credential guard hook
以下は、PreToolUse で Bash コマンドを検査し、本番環境への credential アクセスを検知する実装例です。~/.claude/hooks/ に保存して、上記の hook 設定から参照します。
#!/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 アクセスはプロンプト段階で検出し、ユーザーに確認させる。ただし、完全防御には不十分なため、後段の PostToolUse で実行結果をフィルターする多層設計にします。
層2: Deny Rules — 宣言的防御ルールの分類戦略
「まぁ、そんなコマンドは実行されないでしょ」と思うのが人間の心理。実行されるんです。settings.json の permissions.deny リストは、以下のような categories で整理することで、予期しない操作をブロックします。
Git 操作(強制push・保護ブランチ操作)
{
"type": "Bash",
"command": "git push --force*",
"decision": "deny"
}
--force-with-lease のような「安全な強制push」もブロックする理由:long-running agent の push 事故は、「誰かが push した」という履歴を見つけるまで気付かないことが多い。3 時間後、ログで気付く(遅い)。だから、力で上書きするパターンはすべて eliminate します。push 自体は deny しないが、force の二文字が付いたら full stop。
Git 破壊的操作の判断基準
| コマンド | 判断 | 理由 |
|---|---|---|
git reset --hard | 許可推奨 | reflog で復旧可能(90日程度) |
git branch -D | 許可推奨 | ref ジャーナルで復旧可能 |
git clean -f | deny | ワーキングツリーファイル削除は復旧不可 |
git checkout -- . / git restore . | 許可推奨 | staged changes のみ影響 |
rm -rf /* | deny | ファイルシステム削除は復旧困難 |
chmod -R /* / chown -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 ルール設計の実践的なポイント
deny ルールを整理するとき、以下の分類軸で整理すると保守性が高まります:
- Git: 強制push、reflog 非対応の破壊操作
- System: root 権限操作(sudo)、ファイルシステム削除(rm -rf)
- Network: curl・wget への secret 埋め込み疑い(
curl ... -H "Authorization: Bearer ...") - 機密ファイル read:
.envの direct read(Bash sandbox のdenyReadで補完)
層3: PostToolUse Hooks — 実行後の秘匿情報フィルター
Bash output には API キー、OAuth token、database URL が埋め込まれて返ることがあります。「コマンド実行したら結果が来ただけ」と思い、そのまま Claude の会話履歴に流れてしまうと、過去ログから秘匿情報を抽出できてしまう(= つまり、バックアップを取られたのと同じ)。
そこで活躍するのが 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 を generic fallback より先に処理することで、AKIA... が *_KEY fallback に誤被検される可能性を排除しています(= つまり、「当たるべき」prefix が「外される」という検知ミスを防ぐ)。
Secret Masking の設計原則
PostToolUse での秘匿情報フィルターは、実運用で以下の課題に直面することが多いです:
- URL-embedded credentials:
postgresql://user:password@host/dbは環境変数形式では detect できない(URL syntax を理解する必要) - Lowercase / camelCase 環境変数:
PRODUCTION_API_KEYは detect しやすいが、productionApiKeyは fallback regex でも漏れやすい - JSON inline format:
{"api_key": "value"}の value 部を selective に mask する必要がある
テストコーパス(real .env files)を使って検知精度を検証し、改善前後で「detect 率」を測定することが重要です。「理屈では完全に見えるはず」ではなく「実際のデータセットで検証」することで、実装信頼度が高まります。
実装パターン: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 を使って PR の 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 秒を超えることがある → timeout 延長検討
- Hang 判定のタイミング: 本来の timeout 前に memory / CPU を check し、早期に hang を検知する hook を入れることで、ユーザーが止める判断を提供できる
- Threshold の設定: 環境固有なので、測定してから設定(例:512MB、72時間稼働の環境では 1GB まで許容する、など)
層4: Bash Sandbox — OS レベルの egress 隔離
PreToolUse / PostToolUse 以上に強力な防御が、Bash 実行時の OS-level sandbox です。「エージェントは信頼できるだろう」と思うのではなく、エージェント本人も信頼できない という前提で、macOS Seatbelt を活用して untrusted code の 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, crates.io など、開発ツールチェーンの endpoint のみ許可。その他への通信は prompt で ask します。
許可例: github.com, registry.npmjs.org, pypi.org
禁止例: attacker.com, malicious-logging-service.net
2. denyRead は credential dir のみ
.env は deliberately NOT included。理由:.env の秘匿情報は PostToolUse hook(層3)で処理するのが正しい層だから。Bash sandbox で読み取り禁止にすると、legitimate な設定読み込みまでブロックされて、使いにくくなる。つまり、「禁止する強度」と「使いやすさ」のバランスを取っている。
deny read: ~/.aws, ~/.ssh, ~/.gnupg, ~/.config/gh
allow read: .env(PostToolUse で mask する)
3. excludedCommands は interpreter 非依存の path ベース
gh / git のような trusted tools は除外するが、generic python3:* / bash:* 全除外は危険(任意 script が非 sandbox 化)。実際のスキル script ファイルパスで個別除外する粒度を取ります。
Trusted Tools の除外判定
gh / git を除外する理由:
- gh: Go-based ツール、TLS 検証時に macOS keychain(Mach service)アクセスが必要 → Seatbelt ブロック
- git: credential helper(gh / osxkeychain)が keychain token 読み込み → Seatbelt ブロック
つまり、ツール設計上 sandbox 内での動作が技術的に困難なため、除外リストに入れます。
Hook パターン:Auto-Approve と State 管理
「何もできない制限」は困る。だから、「やることは限定するけど、セットアップを自動化して安全性を担保」という hook の活用パターンです。
使用例:.worktreeinclude を自動生成する
git worktree を作ると、.env や .claude/settings.local.json のような gitignore されたファイルは新しい worktree にコピーされません。毎回手でコピーするのは面倒で、忘れると worktree が動かない。地味だが効くタイプのストレスです。
これを解決する仕組みが .worktreeinclude です。Claude Code(および git-worktreeinclude CLI)は、作業ツリーのルートに置いた .worktreeinclude を読み、そこに .gitignore 構文で列挙されたファイルを新しい worktree へコピーします。安全のため、.worktreeinclude と .gitignore の両方にマッチするファイルだけがコピー対象になります(tracked なファイルを二重化しない設計)。
つまり、worktree 間のファイル引き継ぎ自体は「.worktreeinclude を置く」だけで解決します。残る手間は、その .worktreeinclude を毎回書くこと。そこで価値が出るのが「.worktreeinclude を自動生成する hook」です。git worktree add の PreToolUse(実行直前)で、リポジトリ内の .env* をスキャンして .worktreeinclude を用意しておきます。
#!/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"
ポイントは2つ。生成先はリポジトリルートであって新しい worktree 側ではありません(.worktreeinclude は「コピー元」のツリーに置くもの)。そして PreToolUse で git worktree add の前に用意しておくこと(作成後では間に合わない)。すでに .worktreeinclude があれば触らないので、何度実行しても安全です。
実際のコピーは Claude Code 側が .worktreeinclude を見て行うため、この hook 自体は「コピー対象を宣言するファイルを用意する」だけ。「worktree を作るな」ではなく「worktree を作る前に、引き継ぐファイルを自動で宣言しておく」。ブロック + 手動セットアップではなく、allow + 自動化で UX を損なわない hook の使い方です。
Auto-Approve の使い分け
| Hook | 判定 | 用途 |
|---|---|---|
| PreToolUse | ask / deny / auto-setup | 実行前に intent をチェック。本番環境接触・強制push 等は stop-and-ask。git worktree add 前の .worktreeinclude 生成などの先回りセットアップもここ |
| PostToolUse | allow&filter | 実行後に output を sanitize。秘匿情報 mask、リソース警告 |
| SessionStart | context-load | セッション開始時にプロジェクト状態・ガイドラインを読み込む |
deny 推奨:本番環境接触(PreToolUse ask)、git refspec 検証(PreToolUse ask)、秘匿情報露出リスク(deny)
allow&filter 推奨:実行後の output sanitize(PostToolUse)、リソース監視警告(PostToolUse)。セットアップ自動化は実行「前」に用意する必要があるため PreToolUse
実装ガイド:層別の設計判断フロー
本記事で扱った topic を実装判断フローで整理すると:
エージェントの長時間実行安全性
├─ 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 | PreToolUse credential guard | ask |
| git 強制push による履歴破損 | 2 | deny rules + PreToolUse refspec check | deny + ask |
| API key ログ漏洩 | 3 | PostToolUse secret mask | mask output |
| 長時間実行の hang / OOM | 3 | PostToolUse memory-monitor | alert + recommend stop |
| 未承認の外部通信 | 4 | Bash sandbox allowedDomains | deny + prompt |
まとめ:多層防御で信頼できるエージェント運用
Claude Code をバックグラウンド実行や long-running agent として本番で使うとき、「あ、API キー出てた」という瞬間を避けるには、防御は「どれか一つ」ではなく、Application (hooks) → File (deny rules) → OS (sandbox) の 4 層を組み合わせた深層防御を実装します。
各層の責務
| 層 | 実装 | 脅威モデル | 復旧可能性 |
|---|---|---|---|
| PreToolUse | credential 検知、refspec 確認 | 無意識的な本番接触 | ユーザー確認で prevent |
| Deny Rules | コマンドパターンフィルター | 自動実行による誤った破壊 | 設定で deterministic に block |
| PostToolUse | 秘匿情報 mask、リソース警告 | ログ漏洩、hang 見落とし | Output を後処理で sanitize |
| Bash Sandbox | Network / filesystem 隔離 | 未承認の egress、credential 盗聴 | OS-level で物理的に block |
設計の実践的ポイント
- 完全ブロックより ask を多用: 誤検知時の逃げ道確保(credential guard では ask、refspec では ask)
- 復旧可能性で判定: reflog で復旧できる操作は allow、ファイル削除のように復旧不可な操作は deny
- 実測ベース の threshold: 理屈ではなく、テストデータセット(.env ファイル集合等)で検証して設定
- Trusted tools の個別除外: regex で「全除外」は危険。gh / git は path ベースで限定的に除外
同じ課題に直面しているチームは、この 4 層の考え方を自分たちの環境に適応させることで、予期しないエージェント動作から本番環境を守ることができます。「まさかのとき」に備える仕組みを作る。そうすれば、エージェントを信頼して仕事を任せられる環境が作れます。



