Agent が育てた skill は二度と公式 update を受けられない: Hermes Agent の skill ecosystem 運用 trap
はじめに
本、記事はClaude Codeと対話しながらHermes Agentを触ってみた内容であり、以下の文章自体もClaude Codeにまとめさせたものです。
Claude Codeとの対話は、進め方をリードさせながら、実際のコマンドは私が実行して、できるだけ手触り感を把握しながら、ツッコミをClaude Codeに入れ軌道修正しつつHands-Onを進めました。そのときの気づきをギュッとQiita用にまとめさせました。ハルシネーションがあればご指摘くださいませ。
TL;DR
NousResearch の Hermes Agent には operational trap があります: agent が skill_manage(action="edit") で skill を一度自己改善すると、その skill は二度と公式 (= bundled) 側の update を受けられなくなります。
これは bug ではなく設計で、.bundled_manifest というファイルが git の merge-base と同じ役割を担っているため。本記事では:
- fswatch で atomic write を可視化して仕組みを覗く
-
.bundled_manifest= git merge-base アナロジーを解説 - trap の実害と mitigation 運用を提案
production 採用検討者は必読。
1. 前提: Hermes Agent の skill ファイルシステム
Hermes Agent には 3 つの skill 関連 storage があります:
| 部位 | パス | 役割 |
|---|---|---|
| Bundled |
/opt/hermes/skills/ (= container 内) |
Hermes 同梱の公式 skill 群、read-only |
| User dir |
~/.hermes/skills/ (= host から bind mount) |
実際に runtime が読む場所、agent も人も書き込み可 |
| Manifest | ~/.hermes/skills/.bundled_manifest |
各 skill の同期状態を <name>:<md5> 形式で記録 |
Hermes 起動時、内部の skills_sync.py が bundled と user dir を比較しつつ manifest を update します。これが最重要。
待って、と言いたい — .bundled_manifest の役割が意外なほど深いのです。
2. fswatch で atomic write を可視化する
実際に Hermes が起動するとき、何が起きているのか? fswatch で覗くと一発で分かります:
fswatch -r ~/.hermes/skills/ | grep -v sessions
別ターミナルで docker exec hermes hermes chat -Q -q "Hi" を一発打つと、こう出ます:
~/.hermes/skills/.bundled_manifest_nzjqm5fd.tmp ← 一時ファイル出現
~/.hermes/skills/.bundled_manifest ← rename で確定
~/.hermes/skills/.bundled_manifest ← metadata 更新
これは典型的な atomic write パターン:
要点:
-
.tmpファイルに新内容を全部書く → reader は影響を受けない (= まだ本物は古いまま) -
rename(2)で本物に置き換え → POSIX で atomic (= 同一 FS 内、bash のmv相当) - 中間状態を読む reader が原理的に存在しない
bash で同じことをやるなら:
echo "新内容" > /path/to/.bundled_manifest.tmp
mv /path/to/.bundled_manifest.tmp /path/to/.bundled_manifest # = rename(2) 呼ぶ
rename というコマンド (= Perl 系 / util-linux 系) もありますが、あれは batch リネーム用。atomic 置き換えしたいなら mv が正解。
3. atomic write の意味 (= read/write 競合回避)
なぜこんな手間をかけるのか? それは naive 実装だと:
Writer: open(本物, "w") ← 本物が空 (truncate) になる瞬間
Reader: open(本物, "r") ← ここで読むと「壊れた半分書きファイル」が返る 💥
Writer: 1 行目 write...
Writer: 2 行目 write...
Writer: close()
これだとレース中の reader が部分書きファイルを読む。
vs atomic write:
Writer: write to .tmp, close
Writer: rename(.tmp, 本物) ← 不可分操作
Reader: open(本物) ← 古い完全版 or 新しい完全版、中間なし
「壊れた状態を読む reader」が原理的に存在しない。これが atomic write の正体。git、vim の swap file、ほぼ全ての atomic な状態管理が同じパターンを使います。
4. ⭐ .bundled_manifest = git merge-base アナロジー
ここからが本題。.bundled_manifest の中身を覗くと、こうなっています:
log-triage:a3f2c1d8e5b9...
ml-project-bootstrap:7e4d8a2f9b1c...
file-organizer:c5e1b8d3f7a2...
各 skill の <name>:<md5> を記録。一見ただの index ですが、役割は git の merge-base です。
per-skill 同期 3 ルール
skills_sync.py は CLI 起動ごとに、各 skill について以下の判定をします:
3 ルール:
- (a) 新規 bundled (= manifest に無い): user dir に copy + manifest 追記。素直。
- (b) bundled で user dir hash == manifest hash (= 「user は触ってない」と判定): bundle が更新されていれば user dir も上書き、manifest に新 hash を記録。素直に追従。
- (c) bundled で user dir hash ≠ manifest hash (= 「user 改変済」と判定): 両方 SKIP、manifest は改変前の origin hash のまま。これが key。
git merge-base としての解釈
ルール (c) で「manifest は改変前の origin hash のまま」が肝です。これは git の merge-base と同じ意味:
改変前の bundle hash ← .bundled_manifest 記録
│
┌──────────┴──────────┐
↓ ↓
bundle が新版に進化 user が edit して進化
(= upstream branch) (= local branch)
両 branch から 「最後に同期した点」 が manifest 値として保持される。これが git の merge-base そのもの。3-way merge の base として機能する設計です。
つまり .bundled_manifest は:
- 「不変な snapshot」ではない
- 「user 未改変なら追従し続ける」
- 「user 改変したら改変前の同期点で固定」
という選択的な追従機構なのです。
5. ⚠️ 運用 trap: L2 自己改善で何が起きるか
ここが本記事の警告ポイント。
Hermes Agent の L2 (= skill 自己改善) 機能は、agent が skill_manage(action="edit") を呼んで SKILL.md を上書きする仕組みです。でも skill_manage(edit) は manifest を触りません (= source code 確認済、通常のファイル書き込みのみ)。
つまり何が起きるか:
[Before] skill v1.0 (bundled が出荷した版)
user dir hash = manifest hash (= 一致、未改変)
[Agent が L2 で skill_manage(edit) 実行]
user dir hash 変わる
manifest hash 変わらない
[After] user dir hash ≠ manifest hash
→ 次回 skills_sync は ルール(c) を発動
→ 「user が改変した」と判定
[upstream Hermes が bundle v1.x → v1.y に update]
→ ルール (c) で SKIP、user dir + manifest 両方据え置き
→ user dir には agent 改善版が残るが、bundle 更新は反映されない
→ agent が改善した瞬間から、その skill は二度と bundle 更新を受けない。
副作用 1: agent edit と人編集が完全に区別不能
skills_sync.py から見ると:
skill log-triage の状況:
user dir hash ≠ manifest hash
しかこれだけ。これが:
- agent が L2 で改善した結果なのか
- 人が手で SKILL.md を編集した結果なのか
区別する手段がない。両方とも同じ「user-modified」分岐に入ります。
副作用 2: bundle のセキュリティ修正が素通り
仮に bundle 側で log-triage skill にセキュリティ問題 (= 危険なコマンド実行を許してた) があり、upstream が修正版を ship したとします。user 側で過去に L2 改善されている場合、その修正は永遠に来ない。
これは production 運用上のリアルなリスクです。
副作用 3: 「育てたほうが取り残される」逆説
直感的には 「使い込んだ skill ほど価値が高い」 と感じますが、Hermes の skill 機構では 「使い込んだ skill ほど upstream から取り残される」。これは autonomy を信じれば信じるほど、bundle と乖離していく構造的特性です。
6. Mitigation 運用 + まとめ
推奨 mitigation
-
~/.hermes/を git 化するcd ~/.hermes git init cat > .gitignore <<EOF auth.json *.bak state.db state.db-shm state.db-wal .env sessions/ home/ EOF git add . && git commit -m "initial"→ 改変履歴を別レイヤで track。manifest が判定不能でも、
git logで「いつ・誰が (agent or 人)」が分かります。 -
Skill review gate を運用に組み込む (= Human-in-the-Loop)
- L2 後 /
skill_manage(create)後はgit statusで確認 -
git diffで内容 review - 必要に応じて手動修正 →
git commit(= 承認) orgit checkout(= 拒否)
- L2 後 /
-
skill_manage(edit)後の同期戦略を明示- 改善内容が production-grade なら manifest 手動 update (= 新 hash 書き込みで bundle 進化を受け続ける)
- 改善内容が experimental なら manifest 据え置き (= bundle と切り離す)
- これはチーム / プロジェクトごとに 明示的なポリシー化が必要
-
upstream への提案 (= 個人的に PR 投げる予定):
- SKILL.md frontmatter に
last_edit_actor: agent|userfield 追加 -
skill_manage(edit)時に manifest も新 hash で update する option を新設 - もしくは
skills_sync.pyに「agent edit は merge 候補として扱う」3-way merge 機能
- SKILL.md frontmatter に
まとめ
.bundled_manifest = "Hermes 同梱 skill の最後の同期点 (git merge-base 相当)"
skill_manage(edit) を呼ぶと:
→ user dir hash と manifest hash が divergent
→ skills_sync が「user 改変済」と判定
→ bundle 更新を二度と反映しない
Mitigation:
→ ~/.hermes/ を git 化
→ skill review gate を運用
→ 戦略を明示する
これは bug ではなく設計ですが、operational trap として認識してから採用判断する必要があります。Hermes Agent の autonomy を盲信せず、外部から HITL (Human-in-the-Loop) を強制する運用設計が現実解です。
検出スクリプト (= bonus)
L2 改善された skill を一覧する one-liner:
docker exec hermes /opt/hermes/.venv/bin/python -c "
import hashlib, pathlib
manifest = pathlib.Path('/opt/data/skills/.bundled_manifest').read_text()
expected = dict(line.split(':', 1) for line in manifest.splitlines() if ':' in line)
for skill_md in pathlib.Path('/opt/data/skills').rglob('SKILL.md'):
name = skill_md.parent.name
actual = hashlib.md5(skill_md.read_bytes()).hexdigest()
if name in expected and actual != expected[name].strip():
print(f'{name}: divergent (= L2 改善 or 人編集が入った)')
"
これで「bundle 同期から外れた skill 一覧」が即出ます。production 運用なら定期実行を推奨。
関連
- 自著: 「AI Agent の skill ecosystem は『初期値問題』だった ── ODE 視点で読み解く Hermes Agent」 = 同じハンズオン由来、IVP framing が中心
- NousResearch/hermes-agent
- POSIX rename(2) — atomic write の根幹
📝 Tags: Hermes, agent, LLM, skill, git, merge-base, atomic-write, fswatch, production, mental-model