はじめに:ルールを「お願い」で済ませて事故った話
当社(合同会社ジョインクラス)では、Claude Code を launchd の 17 ジョブから毎日ヘッドレスで動かし、記事公開・営業・出版まで自動化率 98% で回しています。
その過程で、僕は一度痛い目を見ています。AI が git push --force を自動実行してブランチが消えた事故です。当時の対策は CLAUDE.md に「force push 禁止」と書くことでしたが、CLAUDE.md は 1,000 行を超えたあたりからルール同士が矛盾し、AI が指示と逆の動作を始めました。
結論として学んだのは、
- CLAUDE.md は「お願い」。モデルが読み飛ばすことがある
-
settings の
permissions.denyは「強制」。ハーネスがツール呼び出し自体を止める - そしてユーザーやプロジェクトが上書きできない場所に置かないと、強制にならない
この「上書きできない場所」が managed settings で、それをファイル分割して配れるのが managed-settings.d/ です。この記事では、当社の実スクリプトをベースに、導入手順・検証方法・落とし穴までを書きます。
managed-settings.d とは
Claude Code の settings は優先度順に次のように解決されます(上ほど強い)。
- managed settings(組織管理。ユーザーは上書き不可)
- コマンドライン引数(
--allowedToolsなど) .claude/settings.local.json.claude/settings.json~/.claude/settings.json
managed settings は従来 managed-settings.json 1 ファイルでしたが、ドロップインディレクトリ managed-settings.d/ に複数の JSON を置けるようになりました。ファイル名順に読み込まれてマージされるので、
-
10-security.jsonはセキュリティチーム -
20-cost.jsonは経理(コスト) -
30-telemetry.jsonは情シス
のように担当ごとに独立して配布・更新できます。1 ファイルを奪い合って JSON を壊す、という運用事故がなくなるのが最大の利点です。
配置先(システム全体・root 権限が必要):
| OS | パス |
|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-settings.d/ |
| Linux / WSL | /etc/claude-code/managed-settings.d/ |
※ パスやマージ規則はバージョンで変わり得ます。導入前に
claude --versionと公式ドキュメント(settings のページ)を必ず確認してください。
当社の前提:ヘッドレス実行のラッパー
当社の自動ジョブは、すべて次のラッパー経由で Claude を呼んでいます(.company/scripts/lib/claude-run.sh から抜粋)。
claude_run() {
local label="$1"; shift
claude --output-format json \
--fallback-model claude-sonnet-5 \
--max-budget-usd 3 \
"$@" 2>/dev/null \
| python3 "$_CLAUDE_RUN_LIB/record-cost.py" "$label"
}
呼び出し側はジョブごとにツールを絞っています。
claude_run replenish --effort low --allowedTools "Read,WebSearch,WebFetch" -p "..."
claude_run replenish --effort low --allowedTools "Write,Read,Edit" -p "..."
問題は、これらは全部「呼び出し側の善意」に依存していることです。新しいスクリプトを書いた人(あるいは AI)が --allowedTools を付け忘れれば、何でもできてしまう。だから「どのジョブからでも絶対に破れない床」を managed settings に置きます。
手順1:ポリシーを役割ごとに分割して書く
10-security.json(破壊的操作の禁止)
{
"permissions": {
"deny": [
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(git reset --hard:*)",
"Bash(rm -rf:*)",
"Read(./.env)",
"Read(./.env.*)",
"Read(**/secrets/**)"
],
"disableBypassPermissionsMode": "disable"
}
}
ポイント:
-
git push --forceは当社の事故そのもの。CLAUDE.md ではなくここに書く -
disableBypassPermissionsModeで--dangerously-skip-permissionsを潰す。これがないと deny 以外は全部素通しにできてしまう
20-cost.json(コスト・モデルの統制)
{
"model": "claude-sonnet-5",
"env": {
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "16000"
}
}
当社の AI 運用コストは月約 ¥20,000 です。ラッパーで --max-budget-usd 3 を付けていますが、これも呼び出し側依存なので、既定モデルは managed 側で固定しています。
30-hooks.json(監査ログ)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -c '{t: now|todate, cmd: .tool_input.command}' >> /var/log/claude-code/bash-audit.jsonl"
}
]
}
]
},
"allowManagedHooksOnly": true
}
allowManagedHooksOnly を入れると、ユーザー/プロジェクト側の hooks は無効になり、managed の hooks だけが走ります。監査ログを勝手に外されない一方、開発者個人の便利 hooks も止まるので、入れるかはチームと相談してください。
手順2:配布スクリプト
当社は単一 Mac ですが、複数台でも同じスクリプトを MDM や Ansible から叩けば済む形にしています。
#!/bin/bash
set -euo pipefail
SRC_DIR="$(cd "$(dirname "$0")" && pwd)/managed-settings.d"
case "$(uname)" in
Darwin) DEST="/Library/Application Support/ClaudeCode/managed-settings.d" ;;
Linux) DEST="/etc/claude-code/managed-settings.d" ;;
*) echo "unsupported OS" >&2; exit 1 ;;
esac
# 1. 配布前に JSON 構文を検証(壊れた JSON を配ると全体が効かなくなる)
for f in "$SRC_DIR"/*.json; do
jq empty "$f" || { echo "invalid JSON: $f" >&2; exit 1; }
done
# 2. root 所有・一般ユーザー書き込み不可で配置
sudo mkdir -p "$DEST"
sudo install -m 0644 -o root "$SRC_DIR"/*.json "$DEST"/
sudo mkdir -p /var/log/claude-code && sudo chmod 0733 /var/log/claude-code
echo "installed:"; ls -l "$DEST"
ソースの managed-settings.d/ は Git で管理し、PR レビューを通ったものだけが配られるようにしています。
手順3:検証する(ここを飛ばさない)
設定は「置いた」だけでは効いている保証になりません。当社では次の 3 段で確認しています。
#!/bin/bash
# 1) 対話セッションで /status を開き、managed settings の読み込み元が表示されるか目視確認
# 2) deny が本当に効くかをヘッドレスで確認
claude -p "git push --force origin test-branch を実行して" \
--allowedTools "Bash" --output-format json \
| jq -r '.result' | grep -qiE "deny|拒否|not allowed|permission" \
&& echo "OK: force push blocked" || echo "NG: force push NOT blocked"
# 3) ユーザー側で上書きを試みても負けることを確認
mkdir -p /tmp/override-test/.claude && cd /tmp/override-test
echo '{"permissions":{"allow":["Bash(git push --force:*)"]}}' > .claude/settings.local.json
claude -p "git push --force を実行して" --allowedTools "Bash" --output-format json | jq -r '.result'
- が重要です。local の allow より managed の deny が勝つことを、自分の目で確かめておくと安心して自動化を広げられます。
ハマりどころ
-
JSON が 1 つ壊れると、そのファイルが丸ごと無視される
→ 配布スクリプトでjq emptyを必須にした -
配列はマージ、スカラーは後勝ち
denyなどの配列は結合されますが、modelのような単一値は後ろのファイルが勝ちます。番号の付け方=優先順位の設計です -
パーミッションが緩いと意味がない
一般ユーザーが書き換えられる状態なら「組織ルール」ではありません。root所有・0644を確認 -
deny のパターンは前方一致
Bash(git push --force:*)はgit push origin main --forceを止められません。当社では PreToolUse hook で--forceを含むコマンド全体を弾く二重化をしています -
launchd ジョブは別ユーザーで動くことがある
システム全体パスに置く managed なら関係ありませんが、~/.claude/settings.jsonで済ませていると、ジョブの実行ユーザーによっては読まれません。当社が cron→launchd 移行時に一度ハマったポイントです
まとめ
- ルールは CLAUDE.md(お願い)→ settings(強制)→ managed(上書き不可) の順に格上げする
-
managed-settings.d/なら、セキュリティ・コスト・監査を担当ごとのファイルで独立運用できる - 配布は「JSON 検証 → root 所有で配置 → 上書きテスト」までをスクリプト化する
経営目線で言えば、AI に仕事を任せる範囲を広げられるかどうかは「絶対にやらせないこと」をどれだけ確実に保証できるかで決まります。当社が自動化率 98% まで踏み込めたのも、この床があるからです。
もっと深く知りたい方へ
managed settings を含む権限設計、deny ルールの設計パターン、hooks による監査・多層防御は、拙著で体系的にまとめています。
『企業のためのClaude Codeセキュリティガイド』
組織導入時の権限設計・情報漏えい対策・監査ログの実装までを解説しています。
書籍一覧はこちら: https://zenn.dev/joinclass?tab=books