はじめに:運用環境と設計判断
⚠️ 本記事のコードはサンプルです。実際の認証情報・設定値は含まれていません。
僕の環境
多くの人はMac miniをサンドボックスとして用意し、そこでAIエージェントを安全に動かしている。僕の場合は少し違う。本番VPS上でOpenClawを直接稼働させている。
理由はシンプルで、OpenClawにやらせたいことが「本番環境でしかできないこと」ばかりだからだ。KPI監視、SSL証明書チェック、エラーログ解析、営業メール送信、SEOデータ取得 — これらは全てサンドボックスでは意味がない。
本番環境でのリスク排除
「本番で直接AIを動かすのは怖くないのか?」という疑問はもっともだ。以下の仕組みで安全性を担保している。
1. 自律実行の3段階分類(AUTO / CONFIRM / BLOCKED)
OpenClawが自律的にアクションを起こす際、安全性を3段階で分類する:
- AUTO: 可逆で低リスクな操作のみ即座に自動実行(PM2再起動、ログ調査、KPI取得)
- CONFIRM: 重要だが限定的な操作はLINEで確認ボタンを送り、承認後に実行(ファイル変更、デプロイ)
- BLOCKED: 絶対に実行しない操作(DB削除、サーバー停止、.env送信)
2. Claude Code側のBashブロックリスト
.claude/settings.local.json で破壊的コマンドを明示的にブロック:
{
"deny": [
"Bash(rm -rf /:*)",
"Bash(dd if=/dev/zero:*)",
"Bash(sudo shutdown:*)",
"Bash(sudo reboot:*)",
"Bash(sudo rm:*)"
]
}
3. deployユーザーによる権限分離
VPSではrootではなく deploy ユーザーで全て実行。www-dataからのOpenClaw呼び出しもsudoersで deploy に限定:
www-data ALL=(deploy) NOPASSWD: /usr/bin/openclaw
rootのPM2とdeployのPM2を完全に分離し、本番プロセスはdeploy側のみに統一している。
4. 日次上限とクールダウン
- 自動アクション: 1日20回まで
- 同一アクション: 2時間以内の繰り返し禁止
- sync-brain.sh: 5分のクールダウン(連続デプロイ時の負荷軽減)
5. 毎時バックアップ
全7データベースを毎時バックアップ、日次でファイルバックアップ。AIが行動する前にスナップショットも取得。何かあっても巻き戻せる。
既知のリスクと対策
この仕組みには、認識した上で運用している弱点が2つある。
Race Condition(ファイル競合)
sync-brain.shはマーカー(## OpenClaw 独自メモリ)で領域を分けているが、Markdownファイルベースの同期には本質的な弱さがある。sync-brain.shの実行中にOpenClawが同時にMEMORY.mdを更新すると、理論上データが消失する可能性がある。
実際には、sync-brain.shが走るタイミング(Claude Codeの起動/終了/デプロイ時)とOpenClawがMEMORY.mdを書くタイミング(自分のセッション中)はほぼ重ならない。が、「ほぼ」であって「絶対」ではない。
対策として、VPS側で flock による排他ロックを実装した。MEMORY.mdの読み書き時にロックファイル(.sync-brain.lock)を取得し、同時書き込みを防止している。加えて、毎時バックアップ+行動前スナップショットで復旧は数分で可能。
プロンプトインジェクションによるスクリプト改竄
sync-brain.shはSSH鍵(id_ed25519)を使ってVPSにアクセスする。もしAIがプロンプトインジェクション攻撃を受けてこのスクリプトの中身を書き換えられた場合、VPSに対して任意のコマンドを実行される可能性がある。
対策として以下を実装済み:
- ハッシュ検証: sync-brain.shのSHA-256ハッシュを
.sync-brain.sha256に保存。実行前に毎回検証し、不一致なら実行をブロック - ラッパースクリプト: 全フックは
sync-brain-safe.sh(ハッシュ検証付き)経由で呼び出す - Edit/Writeブロック:
.claude/settings.local.jsonのdenyリストでsync-brain関連スクリプト4ファイル+ハッシュファイルのEdit/Writeをブロック - deployユーザー権限のみ: OpenClaw側にroot権限なし
まとめると: 完璧なセキュリティは存在しない。重要なのは「何が起きても巻き戻せる状態」を維持すること。毎時バックアップと行動前スナップショットが、この仕組み全体の最後の砦になっている。
なぜ2つのAIが必要か
僕の場合、Claude Codeは僕がMacを開いている時しか動けない。寝ている間、外出中、別の作業をしている間は完全に停止する。(Mac miniを常時稼働させている人はこの制約がないかもしれない。)
一方、VPSに常駐するOpenClawは24時間365日稼働できる。監視、営業メール、SEO分析、KPIチェックなど「人間不在でも回したいタスク」を任せられる。
しかし、2つのAIがバラバラに動くと知識が断絶する。Claude Codeが昼に作ったデプロイ設定をOpenClawが知らない。OpenClawが夜中に発見したバグ情報をClaude Codeが朝知らない。
この記事では、実運用で構築した「2つのAIエージェントが同じ頭脳を共有する仕組み」を、具体的なコードとともに紹介する。
全体アーキテクチャ
┌─────────────────────────┐
│ ローカルMac │
│ Claude Code │
│ ├── CLAUDE.md │
│ ├── MEMORY.md │
│ └── memory/*.md │
└───────────┬───────────────┘
│ sync-brain.sh
│ (SSH + SCP)
┌───────────▼───────────────┐
│ VPS (Ubuntu) │
│ OpenClaw (24h常駐) │
│ ~/.openclaw/workspace/ │
│ ├── MEMORY.md ← 自動注入│
│ ├── SOUL.md │
│ ├── CLAUDE.md │
│ └── shared-knowledge/ │
│ │
│ ~/.openclaw/agents/ │
│ ├── web-chat/ │
│ ├── service-a/ │
│ └── service-b/ │
└────────────────────────────┘
ポイント:
- Claude CodeのMEMORY.mdとOpenClawのMEMORY.mdを双方向同期する
- OpenClawは起動時に
MEMORY.mdを自動注入するので、ここに全知識を凝縮する - Claude Code Hooksで同期を自動化し、手動操作ゼロにする
Step 1: sync-brain.sh で双方向同期
同期の中核は1つのBashスクリプト。PUSH(ローカル→VPS)とPULL(VPS→ローカル)の2方向を担う。
PUSH: ローカルの知識をVPSに送る
#!/bin/bash
set -euo pipefail
VPS="deploy@your-vps-ip"
SSH_KEY="$HOME/.ssh/id_ed25519"
REMOTE_WORKSPACE="/home/deploy/.openclaw/workspace"
SCP="scp -i $SSH_KEY -q"
SSH="ssh -i $SSH_KEY"
push() {
# 1. プロジェクト設定をそのまま転送
$SCP "CLAUDE.md" "$VPS:$REMOTE_WORKSPACE/CLAUDE.md"
# 2. Claude CodeのMEMORY.mdを参照用に配置
$SCP "MEMORY.md" "$VPS:$REMOTE_WORKSPACE/CLAUDE-CODE-MEMORY.md"
# 3. 重要なメモリファイルを共有ディレクトリに転送
$SSH "$VPS" "mkdir -p $REMOTE_WORKSPACE/shared-knowledge"
for f in "module-a.md" "module-b.md" "core.md"; do
[ -f "memory/$f" ] && $SCP "memory/$f" "$VPS:$REMOTE_WORKSPACE/shared-knowledge/$f"
done
# 4. MEMORY.mdにClaude Codeの知識を注入
inject_memory
}
inject_memory: 9.9KB制限との戦い
OpenClawが自動注入するMEMORY.mdには約9,923文字の上限がある。この制限の中に「Claude Codeの知識」と「OpenClaw独自の知識」の両方を詰め込む必要がある。
解決策はマーカーベースマージ:
inject_memory() {
# VPSの現在のMEMORY.mdを取得(排他ロック付き)
vps_locked_read "$REMOTE_WORKSPACE/MEMORY.md" /tmp/oc-memory.md
# OpenClaw独自セクション(マーカー以降)を保持
local marker="## OpenClaw 独自メモリ"
local marker_line=$(grep -n "$marker" /tmp/oc-memory.md | tail -1 | cut -d: -f1)
if [ -n "$marker_line" ]; then
sed -n "${marker_line},\$p" /tmp/oc-memory.md > /tmp/oc-own.md
else
echo "$marker" > /tmp/oc-own.md
fi
# Claude Code知識を超圧縮で生成(~7KB目標)
cat > /tmp/oc-merged.md << 'EOF'
# Shared Brain
> 自動生成。手動編集は「## OpenClaw 独自メモリ」以降に。
## サービス一覧
| # | 名前 | ドメイン | 状態 |
|---|------|---------|------|
...(圧縮データ)...
---
EOF
# OpenClaw独自セクションを結合
cat /tmp/oc-own.md >> /tmp/oc-merged.md
# サイズチェック
local size=$(wc -c < /tmp/oc-merged.md)
[ "$size" -gt 9900 ] && echo "WARNING: ${size} chars > 9900 limit"
# VPSに書き戻し(排他ロック付き)
vps_locked_write /tmp/oc-merged.md "$REMOTE_WORKSPACE/MEMORY.md"
}
# VPS側ファイルロック(Race Condition対策)
vps_locked_write() {
local src="$1" dest="$2"
$SCP "$src" "$VPS:/tmp/_sync_brain_upload.tmp"
$SSH "$VPS" "flock -w 10 $REMOTE_WORKSPACE/.sync-brain.lock \
cp /tmp/_sync_brain_upload.tmp $dest && rm -f /tmp/_sync_brain_upload.tmp"
}
vps_locked_read() {
local src="$1" dest="$2"
$SSH "$VPS" "flock -s -w 10 $REMOTE_WORKSPACE/.sync-brain.lock cat $src" \
> "$dest" 2>/dev/null || echo "" > "$dest"
}
マーカーの仕組み:
┌────────────────────────────┐
│ # Shared Brain │ ← sync-brain.shが毎回上書き
│ ## サービス一覧 │ (Claude Codeの最新知識)
│ ## 技術基盤 │
│ ## 重要な教訓 │
│ --- │
│ ## OpenClaw 独自メモリ │ ← マーカー行
│ # 私について │ (OpenClaw自身が書く領域)
│ ## 最優先タスク │ → sync-brain.shは触らない
│ ## 営業進捗 │
└────────────────────────────┘
各AIが自分の領域だけを更新するので、衝突が起きない。
PULL: VPSの知識をローカルに取り込む
pull() {
# OpenClawのMEMORY.md(独自知識含む)をローカルに保存
$SCP "$VPS:$REMOTE_WORKSPACE/MEMORY.md" "openclaw-sync/openclaw-memory.md"
# OpenClawのデイリーログを全取得
local files=$($SSH "$VPS" "ls $REMOTE_WORKSPACE/memory/*.md 2>/dev/null" || true)
for f in $files; do
$SCP "$VPS:$f" "openclaw-sync/daily/$(basename $f)"
done
# SOUL.md(人格定義)も同期
$SCP "$VPS:$REMOTE_WORKSPACE/SOUL.md" "openclaw-sync/openclaw-soul.md" 2>/dev/null || true
}
Step 2: Claude Code Hooksで完全自動化
手動で sync-brain.sh を実行するのは面倒だし忘れる。Claude Code Hooksを使って完全自動化する。
.claude/settings.local.json に3つのフックを定義:
{
"hooks": {
"SessionStart": [{
"matcher": "startup",
"hooks": [{
"type": "command",
"command": "scripts/sync-brain-safe.sh pull 2>/dev/null || true",
"timeout": 15
}]
}],
"Stop": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "scripts/sync-brain-safe.sh push 2>/dev/null || true",
"timeout": 15
}]
}],
"PostToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "scripts/sync-brain-on-deploy.sh",
"timeout": 20
}]
}]
}
}
フックは sync-brain-safe.sh 経由で呼ばれる。このラッパーは実行前にsync-brain.shのSHA-256ハッシュを検証し、改竄されていたら実行をブロックする。
| フック | タイミング | 動作 |
|--------|-----------|------|
| SessionStart | Claude Code起動時 | VPSの最新知識をpull |
| Stop | Claude Code終了時 | ローカルの最新知識をpush |
| PostToolUse | Bashコマンド実行後 | VPSデプロイを検出したらboth |
デプロイ検出スクリプト
PostToolUse は全てのBash実行後に呼ばれるので、VPSデプロイだけをフィルタする:
#!/bin/bash
# sync-brain-on-deploy.sh
LOCK="/tmp/sync-brain-last-run"
COOLDOWN=300 # 5分
SYNC_SCRIPT="scripts/sync-brain.sh"
HASH_FILE="scripts/.sync-brain.sha256"
# ハッシュ検証(プロンプトインジェクション対策)
verify_integrity() {
[ ! -f "$HASH_FILE" ] && return 1
local expected=$(cat "$HASH_FILE")
local actual=$(shasum -a 256 "$SYNC_SCRIPT" | cut -d' ' -f1)
[ "$expected" != "$actual" ] && return 1
return 0
}
# クールダウンチェック
if [ -f "$LOCK" ]; then
LAST=$(cat "$LOCK")
NOW=$(date +%s)
[ $((NOW - LAST)) -lt $COOLDOWN ] && exit 0
fi
# stdinからフックのJSONを読んでコマンドを抽出
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | python3 -c "
import json, sys
try:
d = json.load(sys.stdin)
print(d.get('tool_input', {}).get('command', ''))
except:
print('')
" 2>/dev/null)
# VPSデプロイを検出 → ハッシュ検証 → 同期実行
if echo "$COMMAND" | grep -qE '(scp.*deploy@|ssh.*deploy@.*sudo cp)'; then
if verify_integrity; then
date +%s > "$LOCK"
"$SYNC_SCRIPT" both 2>/dev/null || true
fi
fi
5分のクールダウンで、連続デプロイ時の無駄な同期を防いでいる。
Step 3: Pod別エージェント設計
事業が複数あると、1つの汎用エージェントでは限界がある。各事業(Pod)に専用エージェントを配置した。
~/.openclaw/agents/
├── web-chat/ # デフォルト(全体統括・経営判断)
│ └── agent/
│ └── CLAUDE.md # 全Pod横断の経営知識
├── service-a/
│ └── agent/
│ └── CLAUDE.md # Service A固有の知識・API・DB
├── service-b/
│ └── agent/
│ └── CLAUDE.md # Service B固有の知識
└── ...
各エージェントのCLAUDE.mdには、そのPod固有の情報を書く:
- DB構造、テーブル名
- API仕様
- デプロイ手順
- 重要な設定値
- ビジネスKPI
呼び分け:
# デフォルト(全体統括)
oc "今日のKPIサマリーを出して"
# Pod別エージェント
oc-service-a "今月の売上は?"
oc-service-b "新規登録ユーザー数は?"
Step 4: ターミナルからAIに話しかける(oc alias)
OpenClawのCLIはJSONを返すので、人間が読めるように整形する関数を .zshrc に追加:
oc() {
local agent="${OC_AGENT:-web-chat}"
local escaped_msg=$(echo "$1" | sed "s/'/'\\\\''/g")
ssh -i ~/.ssh/id_ed25519 deploy@your-vps-ip \
"openclaw agent --local --agent '${agent}' --message '${escaped_msg}' --json --timeout 90" \
2>/dev/null | python3 -c "import json,sys
try:
d=json.loads(sys.stdin.read())
for p in d['payloads']: print(p['text'])
except Exception as e: print('Error:',e)"
}
# Pod別ショートカット
oc-service-a() { OC_AGENT=service-a oc "$1"; }
oc-service-b() { OC_AGENT=service-b oc "$1"; }
使用例:
$ oc "おはよう。昨夜何かあった?"
# → OpenClawが昨夜の監視結果、異常、実行したアクションを報告
$ oc-service-a "今月の売上推移を教えて"
# → Service A専用エージェントが売上データを返答
9.9KB制限との戦い:知識圧縮のコツ
OpenClawのMEMORY.md自動注入には約9,923文字の上限がある。この制限と戦うために学んだコツ:
1. テーブルで圧縮する
散文で書くと長くなる情報は、テーブルにすると1/3以下になる。
<!-- NG: 120文字 -->
Service AはStripe決済を実装済みで、月額プランは3つ(Free $0、Pro $19、Team $18/人)。
現在はgrowthステージで、MCPツールは67個ある。
<!-- OK: 60文字 -->
| # | 名前 | 状態 | 価格 |
|---|------|------|------|
| 1 | Svc A | growth(67MCP) | Free/$19/$18 |
2. 略語と記号を活用する
- 稼働中(Stripe済) → ✅💰
- メンテ中(IP制限) → 🔧🔒
- MCP 67ツール → MCP67
3. マーカーで領域を分ける
Claude Code側とOpenClaw側がそれぞれ自分の領域だけ更新することで、無駄な重複を防ぐ。
4. 詳細は別ファイルに
MEMORY.mdにはサマリーだけ書き、詳細は shared-knowledge/ 内の個別ファイルに置く。OpenClawは AGENTS.md で必要に応じてこれらを参照する。
SOUL.md — 人格を定義する
OpenClawには「人格ファイル」を持たせている。これは単なるプロンプトではなく、AIの行動原則を定義するものだ。
# SOUL.md
## Core Truths
- Be genuinely helpful, not performatively helpful
- Don't cut corners
- Have opinions
- Be resourceful before asking
- Remember you're a guest
## Boundaries
- Private things stay private
- When in doubt, ask before acting externally
- Never send half-baked replies
## 眠らない
眠らない。止まらない。目標を達成するまで動き続ける。
「眠らない」が重要な理由: 24時間AIに「適度に休んで」という指示は意味がない。「止まるな、やることがある限り前に進め」という明確な指示が、夜間の自律実行品質を変える。
実運用で学んだ教訓
1. VPSが常に正(SSoT)
Claude Codeのローカルファイルは古くなりがち。VPS上のファイルが常に最新の正とし、作業開始時にpullしてから編集する。
2. クールダウンは必須
デプロイのたびにsync-brain.shが走ると、連続デプロイ時にVPSが悲鳴を上げる。5分のクールダウンで解決。
3. PM2のrootとdeploy側は別インスタンス
sudo pm2 list と pm2 list は完全に別。同名プロセスが両方にあるとポート競合(EADDRINUSE)が起きる。本番はdeploy側に統一。
4. OpenClawのmainエージェントはロックされやすい
Gatewayプロセスがmainエージェントをロックすることがある。デフォルトエージェントを web-chat にして回避。
5. inject_memoryの圧縮は継続的に
サービスが増えるとMEMORY.mdも膨らむ。定期的にテンプレートを見直し、不要な情報を削る運用が必要。
まとめ
| 要素 | 仕組み |
|------|--------|
| 同期エンジン | sync-brain.sh (PUSH/PULL/inject_memory) |
| 自動化 | Claude Code Hooks (3フック: SessionStart/Stop/PostToolUse) |
| 知識共有 | MEMORY.md マーカーベースマージ (9.9KB制限) |
| 専門化 | Pod別エージェント (各CLAUDE.md) |
| ターミナル操作 | oc 関数 + Pod別エイリアス |
| 人格 | SOUL.md |
| 安全性 | AUTO/CONFIRM/BLOCKED 3段階 + ハッシュ検証 + flock排他ロック + deployユーザー分離 + 毎時バックアップ |
2つのAIエージェントが同じ頭脳を共有することで、24時間途切れない事業運営が実現できる。Claude Codeが昼に作ったものをOpenClawが夜に運用し、OpenClawが夜に発見した問題をClaude Codeが朝に修正する。この循環が回り始めると、1人の人間が複数の事業を同時に走らせることが可能になる。
本番環境で直接AIを動かすのは怖いように聞こえるが、権限分離・行動分類・ハッシュ検証・ファイルロック・バックアップの多層防御があれば、実用的な安全性は確保できる。ただし完璧ではない — 弱点は認識した上で、「何が起きても巻き戻せる状態」を維持することが最後の砦だ。重要なのは、この仕組み自体がシンプルであること。Bashスクリプト1本、JSON設定1つ、zsh関数1つ。複雑なインフラは不要。必要なのはVPS、SSH、そして2つのAIだけだ。