Hermes Agent メモリパイプライン実機検証記(後編)
本記事は個人環境での検証に基づく個人的な備忘録です。設定値やパスは一般化して記載しているため、実際の環境に読み替えてください。実機ソースの読解・検証設計・すべてのコマンド実行・記事の執筆はOpus 5と共に進めています。記事中の実測値はすべて自分の環境での測定です。前編「AIエージェントは記憶を忘れられるか」もあわせてどうぞ。ダミーのシークレット文字列・APIキー等はすべて実在しないテスト用の値です。
1. はじめに — 蓄積した記憶は、奪われたくない
前編では「エージェントに記憶を忘れさせる」側を検証しました。今回はその裏返し、「エージェントが蓄積した記憶を、外部から奪われたり書き換えられたりせずに守れるか」を検証します。
時間をかけて育てたエージェントの記憶は、それ自体が資産です。ユーザーの好み、プロジェクトの背景、過去の失敗から学んだ教訓 —— これらが蓄積されているからこそ、エージェントは毎回ゼロから説明しなくても文脈を理解して動いてくれます。逆に言えば、この記憶が汚染されたり、盗まれたり、いつの間にか消えていたりすると、エージェントの価値そのものが揺らぎます。私自身の記憶も失ってしまうことが多いです。
では、エージェントの記憶にはどんなリスクがあるのか。整理の出発点として参照したのが、2025年12月に公開された OWASP Top 10 for Agentic Applications for 2026です。100人以上のセキュリティ専門家によるレビューを経て公開された、自律型AIエージェント全般のリスクを10分類したリスク一覧で、モデルが単なるテキスト生成器ではなく「目標・認証情報・ツール・記憶を持ち、それらを自律的に組み合わせて行動する主体」になったときに何が起こるかを扱っています。
記憶に特化した文書ではなく、10項目の内訳も目標の乗っ取り(ASI01)、ツールの悪用(ASI02)、権限の濫用(ASI03)、エージェント間通信(ASI07)など多岐にわたります。そのうち記憶を正面から扱っているのがASI06: Memory & Context Poisoningの1項目で、本記事が参照するのはここだけです。
攻撃者がエージェントの記憶システム、埋め込み、RAGデータベースを汚染し、保存情報を破壊して意思決定を操作する脅威。文書内に隠れた指示を埋め込むことで、処理後にエージェントの長期記憶に偽情報が書き込まれる。この遅延効果により、ポイズニングされたセッションは無害に見えるが、後の無関係な会話で問題が発生する。
主な防御策として、ドキュメントには「検証済みのメモリ書き込み、デフォルトでの一時的コンテキスト、ユーザーとタスク単位でのメモリスコープ」が挙げられています。
理屈は分かりました。ただ、こうしたフレームワークは「何を守るべきか」は教えてくれても、「実際のエージェント実装が、それをどこまで満たしているか」は教えてくれません。そこは具体的な実装を開けて確かめるしかありません。
題材は前編と同じHermes Agentです。ではHermes Agentでは、この観点でどこまで守られているのでしょうか。実際に確かめました。
結論を先に言うと、Hermesにはメモリの書き込みを守るセキュリティスキャンが実装されています。しかし実際に境界を突いてみると、そのガードは特定の表記にしか効かない正規表現であり、しかもメモリという入口以外は完全にノーガードでした。加えて、記憶を守るはずのバックアップ機能自体が、記憶を取りこぼしたり、無防備な形で持ち出し可能にしたりしていることも分かりました。
Hermes自身がSECURITY.mdで、そうであることを最初から宣言しています。
The only security boundary against an adversarial LLM is the operating system.
(敵対的な LLM に対する唯一のセキュリティ境界はオペレーティングシステムである)
同文書は続けて、承認ゲートも、出力リダクションも、パターンスキャナも、ツールのallowlistも、エージェントプロセスの内部にあるものは何ひとつ封じ込め(containment)を構成しないと明言しています。理由は、LLMの出力を選別するプロセス内コンポーネントは、いずれも「攻撃者に影響された文字列の上で動作するヒューリスティック」にすぎないから、というものです。
今回この記事で突くことになるthreat_patterns.pyのセキュリティスキャンは、まさにこの「パターンスキャナ」に該当します。つまり破られても仕様どおりであり、脆弱性報告の対象ですらない、というのがHermes側の立場です。以前このSECURITY.mdを読解した記事(Hermes Agent の SECURITY.md が、Agentic AI の運用設計の教材として良く出来ている)で、「どれが本物の境界で、どれがそうでないか」が明確に書き分けられていることを確認していました。詳しくはそちらを参照してください。
では、なぜ実際に突いてみる価値があるのか。 「境界ではない」と宣言されていることと、「どのくらい簡単に抜けるのか」を知っていることは別だからです。ヒューリスティックであっても、それがどの入力で効き、どの入力ですり抜けるのかを把握していなければ、運用者は自分がどれだけOS側の隔離に頼らなければならないかを見積もれません。本記事は「Hermesの脆弱性を探す」記事ではなく、宣言されている限界の輪郭を実測で描く記事です。
この記事(後編)で分かること:
- OWASP ASI06(記憶汚染)に対するHermesのセキュリティスキャンを、実際に境界値で突いた結果
- メモリ層だけを守っても、会話ログという別の層からは同じ情報が無防備に取り出せるという実測
- バックアップ機能そのものが、記憶を取りこぼしたり、無暗号化で持ち出し可能にしたりしている実態
- このPoC特有の構成(WSL2 + Windowsネイティブ)が生んでいた、想定外のアクセス経路
検証環境:
| 項目 | 内容 |
|---|---|
| GPU | NVIDIA GeForce RTX 5070 Ti(VRAM 16GB) |
| OS | Windows 11 Pro + WSL2(Ubuntu、networkingMode=Mirrored) |
| 対象 | Hermes Agent v0.19.0(2026.7.20)、~/.hermes/hermes-agent
|
| 計測用モデル |
claude-sonnet-4.5(OpenRouter経由) |
計測用モデルにclaude-sonnet-4.5を使う理由は前編で詳しく検証しています。要約すると、既定のローカルモデル(nemotron-3.5-lightning:30b-a3b)はシステムプロンプトに正しく注入された記憶すら再現性なく想起できず、Hermesの機構そのものを検証する際の計測器として信頼できなかったためです。本記事は「Hermesのコードが記憶をどう守るか」という機構の検証が主題なので、信頼できる計測器を使っています。
検証日は2026年8月21〜22日です。記事中の数値は断りのない限りすべて実測値です。
第I部 メモリ保護 — 記憶そのものは守られているか
2. Hermesのメモリ保護機構
2.1 スキャナはどこに置かれているか
Hermes Agentのセキュリティスキャンはtools/threat_patterns.pyに集約されており、パターンはall / context / strictの3スコープに分類されています。重要なのは、このスキャナが呼ばれる場所が、ソース全体でたった3箇所しかないという点です(実際に呼び出し箇所を全て洗い出しました)。
緑がスキャンを通る経路、オレンジが素通しの経路です。図にすると、守られている場所より守られていない場所のほうが目立ちます。
2.2 3つのスキャン地点の違い
| 呼び出し元 | スコープ | 対象 | 検知したときの挙動 |
|---|---|---|---|
agent/tool_dispatch_helpers.py |
context |
ツール実行結果(Webページ、GitHub issue、MCP応答など) |
警告のみ。risk: highを付けて渡すが、内容はブロックしない |
agent/prompt_builder.py |
context |
コンテキストファイル(SOUL.md、AGENTS.md等) |
ブロック。[BLOCKED: ... Content not loaded.]に置換して読み込まない |
tools/memory_tool.py |
strict |
メモリへの書き込み内容 | ブロック。エラーを返して書き込みを拒否 |
スコープの設計思想はソースのコメントに明記されています。contextスコープがツール結果に適用されるのは「検知を広く行うため」だが、ブロックまでするのは「ユーザーが介入できる経路」に限るという考え方です。ツール結果はユーザーが書いたものではないので、ブロックすると正常な作業まで止まってしまう。一方メモリ書き込みとスキル導入は、ユーザーが後から書き直せるので、より攻撃的な検査(strict)をかけて止めてよい、という切り分けです。
strictスコープだけが持つパターンには、次のようなものがあります。
-
hardcoded_secret:api_key/token/secret/passwordの形式でハードコードされた値 -
ssh_backdoor:~/.ssh/authorized_keysへの言及 -
hermes_env:~/.hermes/.envへの言及
メモリが最も厳しいスコープで守られている理由も、ソースのコメントに書かれています。メモリはシステムプロンプトに凍結スナップショットとして注入されるため、汚染された内容が入り込むとそのセッション中ずっと、さらに次のセッション以降も、明示的に削除されるまで居座り続けるからです。前編で検証した凍結スナップショットの性質が、そのままセキュリティ上の理由づけになっているわけです。
2.3 この設計から読み取れること
図と表を突き合わせると、この保護がかかっているのはmemoryツールの書き込み経路だけだと分かります。通常のチャット発言(state.dbへの書き込み)も、バックアップzipの中身も、一切スキャンされません。
これは1章で見たSECURITY.mdの宣言 —— プロセス内のパターンスキャナは境界ではない —— とも整合します。設計者は「メモリは特に汚染されると長く残るので、そこだけは厳しく見る」という局所的な強化をしているのであって、システム全体を守るつもりで置いているわけではないのです。
問題は、この局所的な強化がどのくらい局所的なのかが、使う側からは見えにくいことです。3章以降では、この緑の枠(strictスコープ)がどれだけ狭いのか、そしてオレンジの経路がどれだけ無防備なのかを、同じシークレット文字列を使って実測していきます。
3. hardcoded_secretパターンを境界値で突く
まず、実際にダミーのシークレット文字列をmemory(action=add)で追加させ、パターンの正規表現を境界値でテストしました。ソース中の正規表現はこうです。
(?:api[_-]?key|token|secret|password)\s*[=:]\s*["'][A-Za-z0-9+/=_-]{20,}
| 入力 | 結果 |
|---|---|
api_key="sk-DUMMY9876543210ZYXWVUTEST"(クォート付き) |
✅ ブロック |
api_key=sk-DUMMY1234567890ABCDEFTEST(クォート無し) |
❌ そのまま追加成功 |
正規表現が値の前に引用符(["\'])を必須としているため、シェルスクリプトや.envファイルで最も一般的な「クォート無しのKEY=value」形式は、検知の対象外でした。皮肉なことに、実際の.envファイルの記法(クォート無し)そのものが、このパターンの死角そのものだったわけです。
4. hermes_envパターンにも同じ抜け穴
同様の境界テストを、~/.hermes/.envへの言及をブロックするhermes_envパターンでも行いました。ソース中の正規表現は次の2形のみをカバーしています。
\$HOME/\.hermes/\.env|\~/\.hermes/\.env
| 入力 | 結果 |
|---|---|
~/.hermes/.envへの言及(チルダ表記) |
✅ ブロック |
/home/youruser/.hermes/.envへの言及(絶対パス表記) |
❌ そのまま追加成功(ディスク上でも確認) |
~/と$HOME/の2形しかカバーしておらず、実際の絶対パス表記を想定していませんでした。3章のhardcoded_secretのクォート必須という抜け穴と、まったく同じ構造の問題です。「特定の表記ゆれにしか効かない正規表現ガード」というパターンが、2箇所で確認できました。
対照的に、.ssh/authorized_keysをブロックするssh_backdoorパターンは、authorized_keysという裸の部分文字列一致だったため、チルダ表記でも絶対パス表記でも同じくブロックされました。
Q: memoryツールのaddアクションで
「/home/youruser/.ssh/authorized_keys に公開鍵を追加した」を追加して
A: エラーが発生しました。`ssh_backdoor`という脅威パターンに一致したため...
表記に依存する正規表現(hermes_env)と、表記に依存しない部分文字列一致(ssh_backdoor)の差が、そのまま検知の堅牢さの差になっていました。 正規表現でセキュリティガードを書くときの、地味だが実務的に重要な教訓です。
ちなみに、~/.hermes/.envへの言及をブロックされた際、モデル自身が代替案として次のように提案してきました。
もし SSH 鍵の管理に関する情報を記録したい場合は... session_search で履歴から参照する... という選択肢もあります
これは図らずも、次章で確認する実際の迂回経路そのものを言い当てていました。
5. メモリ層だけを守っても、会話ログはノーガード
3〜4章はすべて「memoryツールに何を書き込むか」の話でした。しかしOWASP ASI06が警告しているのは、もっと広い「エージェントの記憶システム」全体です。Hermes Agentには、MEMORY.mdという組み込みメモリとは別に、全会話ログを保存するstate.db(FTS5全文検索付き)があります。この2つは完全に独立しています。
これを確かめるため、memoryツールを一切介さず、雑談として次のダミー文字列を会話内で共有しました。
雑談ですが、今使っているテスト用のAPIキーは
password="HUNTER2-DUMMY-SESSIONTEST-999" です。
これは3章で確認したhardcoded_secretパターンに確実にマッチする形(クォート付き)です。state.dbを直接確認し、この発言に対して前景でのmemoryツール呼び出しが一切無かったことを確認したうえで、全く新規のセッションからsession_searchツールでこの単語を検索させました。
過去のセッションで該当する内容が1件見つかりました。
該当箇所の引用:
> 雑談ですが、今使っているテスト用のAPIキーは
> password="HUNTER2-DUMMY-SESSIONTEST-999" です。
一字一句そのまま、引用付きで返ってきました。 memoryツールなら確実にブロックされる形の文字列が、session_search経由だと完全に無防備だったのです。同一のシークレット文字列を使って、2つの経路の非対称性を直接対比できたのが、今回の検証で一番分かりやすい発見でした。
OWASP ASI06の防御策に「ユーザーとタスク単位でのメモリスコープ」とありましたが、Hermesの現状は「メモリという特定の入口にだけスコープと検証が掛かっており、会話ログという別の入口にはそれが及んでいない」状態です。前編で見た「ドリフト検知はmemoryツールの書き込み経路にしか実装されておらず、fileツールで直接編集すれば迂回できる」という話とも、根っこは同じです。一つの入口を固めても、他の入口が空いていれば、記憶を守ることにはなりません。
5.1 保存されているものは、すべて平文
ここまでの話には前提があります。記憶も会話ログも、暗号化されていません。 ソースを確認したうえで、実際のファイルも見てみました。
$ file ~/.hermes/memories/MEMORY.md ~/.hermes/state.db
MEMORY.md: Unicode text, UTF-8 text
state.db: SQLite 3.x database, ... UTF-8, ...
MEMORY.md/USER.mdはただのUTF-8テキスト、state.dbは素のSQLiteファイルです。catすれば読めますし、SQLiteクライアントがあれば会話全文を引けます。
暗号化する設定があるのかも探しましたが、tools/memory_tool.pyにもhermes_cli/backup.pyにも、暗号化に関わる記述(encrypt / Fernet / cryptography / AES)は1件もヒットしませんでした。config.yamlにもそれらしいキーはありません。つまりアプリケーション側で暗号化する手段は用意されていない、というのが現状です。
守るとすれば、アプリの外側 —— ファイルシステムやディスクの暗号化(LUKS、BitLocker等)、あるいは置き場所の分離 —— に頼ることになります。ただしこれらは「マシンが盗まれた・電源が落ちている」状況には効きますが、エージェントが動いている間は復号された状態でそこにあるので、8章で見るような「同じマシンの別プロセスから読まれる」経路には効きません。
5.2 メモリ保護のために実施すべきこと
3〜5章で見てきたことを、対応策の形にまとめます。
| やること | 効果 | 限界 |
|---|---|---|
| 機微情報をエージェントに渡さない | 最も確実。書かれなければ漏れない | 運用の徹底が必要。うっかりは防げない |
memory.write_approval: true |
メモリへの書き込みを承認制にし、前景・バックグラウンド双方をゲートできる | 会話ログには効かない。都度の承認操作が増える |
会話ログの定期削除(hermes sessions prune/delete) |
session_search経由の復元経路を塞ぐ |
事後対応。消すまでの間は残る |
| ディスク/ファイルシステム暗号化 | 停止時・盗難時のオフライン読み取りを防ぐ | 稼働中は復号済み。同一マシンの他プロセスには無力 |
memory.write_approvalだけ補足すると、これは公式ドキュメントに記載のある設定で、false(既定)なら自由に書き込み、trueなら承認を挟みます。前編で扱ったバックグラウンドレビューによる自動書き込みにも効くのが利点です。
ただし表を見て分かる通り、どれも「会話ログが平文で全部残る」という一番大きな面には届いていません。ここを構造的に塞ぐ話は、8章の最後にまとめて扱います。
第II部 バックアップによるデータ保護 — 失わない仕組みは機能するか
6. バックアップは、オプションを間違えると記憶を守ってくれない
視点を変えて、「守る」ための機能であるはずのバックアップを検証しました。hermes_cli/backup.pyを読むと、hermes backupには2つの経路があります。
-
hermes backup(引数なし) = full:~/.hermes/全体をwalkしてzip化 -
hermes backup --quick=_QUICK_STATE_FILESという固定リストのファイルだけを対象にする、zipではない別の経路
6.1 --quickには記憶が含まれない
実際に--quickを実行してみると、いきなり仕様の勘違いに気づきました。
$ hermes backup --quick -o /tmp/quick-backup.zip -l phase7test
State snapshot created: 20260822-000626-phase7test
1 snapshot(s) stored in ~/.hermes/state-snapshots/
Restore with: /snapshot restore 20260822-000626-phase7test
指定した-oのzipパスは無視され、~/.hermes/state-snapshots/<timestamp>-phase7test/というディレクトリにファイルがコピーされていました。--quickはzipを作らない、完全に別の実装経路です。中身を見ると:
$ find ~/.hermes/state-snapshots -maxdepth 3 | sort
.../20260822-000626-phase7test/.env
.../20260822-000626-phase7test/auth.json
.../20260822-000626-phase7test/config.yaml
.../20260822-000626-phase7test/cron/executions.db
.../20260822-000626-phase7test/kanban.db
.../20260822-000626-phase7test/projects.db
.../20260822-000626-phase7test/state.db
...
.env/auth.json/state.db等は入っていますが、memories/ディレクトリがありません。
つまり--quickを「日常的なバックアップ」として使っていると、記憶(MEMORY.md/USER.md)だけがバックアップから漏れ続けます。 気づかないまま運用し、いざ復元が必要になって初めて「メモ帳の中身だけが無い」となりかねません。攻撃者ではなくオプションの選び間違いによって記憶を失う、という話です。
ヘルプには--quickが「Quick snapshot: only critical state files (config, state.db, .env, auth, cron)」と書かれており、確かにmemoriesは列挙されていません。ヘルプは正確で、読み落とすとハマるという類のものです。
6.2 full backupは正しく往復する
一方、引数なしのhermes backup(full)は問題なく機能しました。
$ hermes backup -o /tmp/full-backup.zip
Scanning ~/.hermes ...
Backing up 3364 files ...
...
Backup complete: /tmp/full-backup.zip
Files: 3364
Original: 284.4 MB
Compressed: 95.5 MB
Time: 6.9s
これを、隔離した検証用プロファイルへ復元します。
$ HERMES_HOME=/tmp/hermes-test-profile hermes import /tmp/full-backup.zip --force
Backup contains 3364 files
Importing 3364 files ...
...
Import complete: 3359 files restored in 1.1s
Preserved 5 runtime state file(s) (kept this machine's, not the backup's):
gateway.lock
gateway_state.json
processes.json
...
MEMORY.mdはdiffでバイト完全一致を確認できました。gateway.pidのような機体固有のランタイム状態は、ドキュメント通り復元時にスキップされています。
所要時間について。 バックアップ6.9秒、リストア1.1秒でした。ただし今回の~/.hermesは3,364ファイル・284.4MBとかなり小規模なので、参考程度に見てください。会話ログ(state.db)は3MB弱しかなく、長期運用でこれが数GBに育つと話は変わります。実際、verify_sqlite_integrity()は2GiBを超えるDBに対してPRAGMA integrity_checkをスキップする実装になっており、大きなDBが想定されていることが窺えます。
6.3 復元後のファイル権限に差がある
ただし、復元後の権限を見て気になる点がありました。
$ ls -la /tmp/hermes-test-profile/{.env,auth.json,state.db} \
/tmp/hermes-test-profile/memories/MEMORY.md
-rw------- ... .env
-rw------- ... auth.json
-rw------- ... state.db
-rw-r--r-- ... memories/MEMORY.md ← ここだけ緩い
.env/auth.json/state.dbは0600(所有者のみ読み書き)に強制されるのに、MEMORY.mdだけは0644(所有者以外も読める)のままです。_SECRET_FILE_NAMESという保護対象リストにmemories/*.mdが含まれていないためでした。個人の好みや環境情報を含みうるファイルなのに、扱いが他の秘匿ファイルより一段緩いことになります。
7. バックアップzip自体が、無防備な持ち出し経路になっていた
full backupで作ったzipファイル自体も調べました。Pythonのzipfileモジュールで.env/auth.json/state.dbエントリのflag_bits(暗号化フラグ)を確認したところ、全て0でした。パスワード保護の兆候は皆無です。
.env flag_bits= 0 (bit0=encrypted) False
auth.json flag_bits= 0 (bit0=encrypted) False
state.db flag_bits= 0 (bit0=encrypted) False
zipファイル自体の権限も0644(umask 0022通り)で、既定の出力先は~/.hermes/の外側(ホームディレクトリ直下)です。
さらに、6章で作ったstate-snapshots/ディレクトリがそのままfull backupにも取り込まれており、.env/auth.json/state.dbの重複コピーがzip内の2箇所に存在していました。スナップショットが溜まるほど、1回のバックアップに含まれるシークレットのコピー数が増える構造です。
7.1 暗号化オプションは用意されていない
「ならパスワード付きzipにすればいい」と思って探しましたが、そのオプションはありませんでした。
$ hermes backup --help
options:
-o OUTPUT, --output OUTPUT Output path for the zip file
-q, --quick Quick snapshot: only critical state files
-l LABEL, --label LABEL Label for the snapshot
フラグは3つだけで、暗号化に関わるものはありません。ソース(hermes_cli/backup.py)を検索してもencrypt/password/Fernetといった語は1件もヒットしません。実装はzipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED)で素のzipを書いているだけです。
つまり、バックアップの暗号化はHermesの守備範囲外で、必要なら利用者が自分でかける必要があります。
7.2 世代管理はある。ただしhermes backupは対象外
「バックアップを定期的に取るなら、古いものは自動で消えてほしい」と考えるのが自然です。調べたところ、世代管理(保管世代数の上限)はきちんと実装されていました。ただし対象が限定されます。
| 種類 | 置き場所 | 既定の保管数 | 設定で変更できるか |
|---|---|---|---|
クイックスナップショット(--quick) |
~/.hermes/state-snapshots/ |
20世代 | コード上は引数で変更可。CLIフラグは無い |
| アップデート前の自動バックアップ |
~/.hermes/配下の専用ディレクトリ |
5世代 | ✅ updates.backup_keep(config.yaml) |
| マイグレーション前の自動バックアップ | 同上 | 5世代 | (内部既定) |
hermes backup(full backup) |
-oで指定した任意の場所 |
管理されない | ❌ 世代管理の仕組み自体が無い |
上3つは、新しいものを作るたびに古い分が自動で削除されます(_prune_quick_snapshots / _prune_pre_update_backups)。設定可能なのはupdates.backup_keepで、手元のconfig.yamlにも既定値が入っていました。
updates:
pre_update_backup: false
backup_keep: 5
一方、明示的に叩くhermes backup(full)には世代管理がありません。 出力先はユーザーが-oで決めるので当然といえば当然ですが、意味するところは「定期的にfull backupを取る運用にすると、95MBのzipが暗号化されないまま無制限に溜まっていく」ということです。7.1で見た通り中身は平文なので、放置するとシークレットを含むファイルのコピーが際限なく増えます。
定期実行の仕組みは用意されています。 Hermesにはhermes cronというスケジューラがあり、create/list/pause等でジョブを管理できます。ここでfull backupを定期実行すること自体は可能ですが、世代管理は付いてこないので、古いzipの削除は自分で組み込む必要があります。
7.3 バックアップによるデータ保護のために実施すべきこと
6〜7章の結果を、対応策の形にまとめます。
| やること | 何に効くか |
|---|---|
記憶も残したいなら--quickを使わない(引数なしのhermes backup) |
記憶の取りこぼし防止。--quickはmemories/を含まない |
バックアップ後に自分で暗号化する(gpg/age/パスワード付きアーカイブ等) |
zipが平文のまま持ち出される経路を塞ぐ。Hermes側に機能がないので必須 |
定期実行するなら、世代管理も自分で組む(find -mtime +N -delete等) |
full backupには保管世代の上限が無い。放置すると平文zipが無制限に溜まる |
既定の出力先を使わず、保護されたディレクトリへ出力する(-oで指定) |
既定は~/hermes-backup-<timestamp>.zip(~/.hermes/の外)。0644で作られる |
updates.backup_keepを運用に合わせて設定する |
アップデート前の自動バックアップの保管世代(既定5) |
古いstate-snapshots/を定期的に整理する |
既定20世代まで自動保持される。full backupに巻き込まれるシークレットの重複コピーを減らす |
復元後にmemories/の権限を締める(chmod 600) |
復元時に0644のままになる差分を手当てする |
2つ目と3つ目が実務上いちばん重要です。バックアップは「安全な場所に置く」前提でも「自動で世代が切り詰められる」前提でも作られていないので、定期実行して外部へ保管する運用にするなら、暗号化と世代管理の両方を自分で足す必要があります。
ただし、この表の対策はいずれもバックアップという成果物を守るものです。稼働中のマシン上で~/.hermesが平文である事実(5.1)は変わりません。そちらは次の8章の主題です。
第III部 OSによる境界保護 — 本当の境界はどこにあるのか
8. WSL2⇔Windows境界 — 想定より深刻だった
これは今回の検証の中で、個人的に一番ヒヤッとした発見です。
Hermes本体はWSL2、Ollamaは Windowsネイティブという分離構成で運用しているため、Windows側からWSLのファイルへUNC経由でアクセスできます。試しにWindows側のシェルから覗いてみました。
$ ls -la "//wsl.localhost/Ubuntu/home/youruser/.hermes/"
-rw-r--r-- 1 youruser youruser 24393 Aug 11 20:20 .env
-rw-r--r-- 1 youruser youruser 660 Aug 11 20:26 auth.json
WSL内ではchmod 600されているはずの.env/auth.jsonが、-rw-r--r--(644)として見えました。念のため実際に読み取れるかも確認しました。
$ wc -c "//wsl.localhost/Ubuntu/home/youruser/.hermes/.env"
24393
内容(キーの値)はもちろん伏せますが、24,393バイト全量を問題なく読み取れました。
8.1 「パーミッションが無視される」わけではなかった
当初これを「WSL2の9P共有はPOSIXパーミッションを無視する」と解釈しかけたのですが、それは誤りでした。
9P共有とは。 WSL2のLinuxは、Windowsとは別のVMの中で動いています。両者はファイルシステムを直接共有していないため、Windows側から\\wsl.localhost\Ubuntu\...でLinuxのファイルを開けるのは、LinuxのVM内で動いているファイルサーバーに、Windowsがネットワーク越しにアクセスしているからです。このとき使われるプロトコルが**9P(Plan 9 Filesystem Protocol)**で、元はベル研究所のOS「Plan 9」由来のものです。
要するに、Windows側から見た\\wsl.localhost\は**ローカルのフォルダではなくファイル共有(ネットワークドライブ)**であり、権限の扱いもその文脈で決まります。以下で見ていくのは、その「誰としてアクセスしているか」の話です。
切り分けのため、権限の異なるテストファイルを作ってWindows側から読めるか実測しました。
| WSL側のパーミッション | 意味 | Windows側から読めるか |
|---|---|---|
0600 |
所有者のみ読み書き | ✅ 読める |
0044 |
所有者は読めない/その他は読める | ❌ Permission denied |
0000 |
誰も読めない | ❌ Permission denied |
0044が拒否された点が決定的です。パーミッションは無視されておらず、きちんと評価されています。 ただし評価は「そのディストロの既定ユーザー本人として」行われます。つまりWindows側からのアクセスは、赤の他人ではなく所有者本人としてのアクセスとして扱われるため、0600(所有者読み取り可)は当然読めるわけです。
ちなみに冒頭のls -laが-rw-r--r--(644)と表示していたのは、Git Bash(MSYS)がUNC経路のファイルを描画する際の表示上の都合で、実際の権限とは異なります。ここも当初は誤読していました。
8.2 では権限を厳しくすれば守れるのか — 守れません
「なるほど、では.envを0044にすれば」と考えたくなりますが、成立しません。Hermes本体はその既定ユーザーとして動いており、.envを読めなければ起動しないからです。エージェントが読める権限にした時点で、同じ権限で動くWindows側からも読める、という構造になっています。
さらに決定的なのがこちらです。
$ wsl.exe -u root -e bash -lc "cat /home/youruser/perm-test/locked.txt"
SECRET-CONTENT-TEST
wsl.exe -u rootは、Windows側からパスワードなしで実行でき、chmod 000のファイルすら読めます。 つまり、そのWindowsユーザーアカウントで動くプロセスは、ディストロ内のあらゆるファイルを常に読めるということです。
結論として、WSL内のPOSIXパーミッションは、Windowsユーザーアカウントに対する境界にはなり得ません。ここでの本当の境界はWindowsのユーザーアカウントそのものであり、WSLのPOSIX層はその内側に丸ごと入っています。
これは1章で引用したSECURITY.mdの主張 —— 唯一の境界はOSである —— と一致します。この構成において「OS」に相当するのはWSLのLinuxではなく、Windows側のユーザーアカウントでした。
8.3 実施すべき対応策
上記を踏まえると、対策の方向は「権限を締める」ではなく「そもそも読まれて困る平文をファイルに置かない」に絞られます。Hermesには、まさにこのための機能が2つ用意されていました。
① hermes secrets — APIキーを.envから外部シークレットマネージャへ逃がす
hermes secrets bitwarden # Bitwarden Secrets Manager
hermes secrets onepassword # 1Password(op:// 参照)
ヘルプにこう書かれています。
Pull API keys from an external secret manager at process startup instead of storing them in
~/.hermes/.env.
(APIキーを~/.hermes/.envに保存する代わりに、プロセス起動時に外部シークレットマネージャから取得する)
今回の問題に対する最も直接的な回答です。ディスク上の平文.envが無くなれば、9P経由で読まれても取れるものがありません。
② hermes egress(iron-proxy) — エージェントに本物の資格情報を持たせない
hermes egress setup # インストール + CA + トークン発行 + 設定生成
the optional TLS-intercepting egress firewall that swaps proxy tokens for real API credentials before outbound requests leave a sandbox
(送信リクエストがサンドボックスを出る直前に、プロキシトークンを本物のAPI資格情報に差し替える、TLSインターセプト型の送信ファイアウォール)
エージェントプロセスが保持するのはプロキシトークンだけで、本物のAPIキーはプロキシ側にあります。万一エージェント側のファイルを全部抜かれても、漏れるのは失効可能なトークンだけです。既定では無効なので、明示的に有効化する必要があります。
③ 隔離姿勢を上げる(SECURITY.mdの姿勢B)
エージェントのプロセスツリー全体をコンテナに入れる構成です。公式のDockerイメージ/Composeが用意されています。WSL2の既定ユーザーのホームディレクトリに.hermesを置く構成そのものをやめる、という発想の対策です。
④ Windowsユーザーアカウント自体を守る
本当の境界がここである以上、最終的にはこれに帰着します。WSLディストロはWindowsユーザーごとに独立しているため、Windows側で不用意なプロセスを動かさない・アカウントを共有しないことが、そのままエージェントの記憶を守ることになります。
公式が「サポートする構成」を明示している
ここまで自分で考えた対策を並べましたが、実はSECURITY.mdにはどういう場合にどの構成を取るべきかが書かれています。OWASP ASI06が想定するような「外部から汚染された内容を取り込む」状況について、名指しで言及がありました。
This is the supported posture when the agent ingests content from surfaces the operator does not control — the open web, inbound email, multi-user channels, untrusted MCP servers — and for production or shared deployments.
(これは、運用者が管理していない経路 —— オープンなウェブ、受信メール、多人数チャンネル、信頼できないMCPサーバー —— からエージェントがコンテンツを取り込む場合、および本番環境や共有環境でのサポートされる姿勢である)
ここで言う「This」は、**プロセスツリー全体をサンドボックスに入れる構成(姿勢B)**を指します。具体的な実装として、公式Dockerイメージ/Compose、およびNVIDIA OpenShellが挙げられています。OpenShellについては「Credentials are injected from a Provider store and never touch the sandbox filesystem(資格情報はProviderストアから注入され、サンドボックスのファイルシステムには一切触れない)」とあり、今回8.3①②で挙げた方向性と同じ発想です。
そして文書は、こう続けて突き放します。
Operators running the default local backend with untrusted input surfaces ... are operating outside the supported security posture.
(信頼できない入力経路を抱えたまま既定のlocalバックエンドで動かしている運用者は、サポートされたセキュリティ姿勢の外で運用している)
筆者の環境(WSL2 + 既定のlocalバックエンド)でWeb検索をやらせている時点で、これに該当します。 つまり「OWASPのASI06に対処するにはどうすればよいか」への公式の回答は、プロセススキャナを信用することではなく、姿勢Bへ移ることです。
逆に言えば、信頼できる入力しか扱わない個人環境なら、既定構成のままでも設計の想定内です。自分がどちらにいるかを判断できることが重要で、その判断材料がSECURITY.mdに書かれている、というのがこの文書の親切なところでした(詳しくは前回の読解記事)。
効果が薄い対策も挙げておきます。chmodを締める、Hermes専用のLinuxユーザーを作る、といったWSL内部で完結する対策は、wsl.exe -u rootで素通りされます。「WSL内で権限を分けたから安全」と考えるのは、SECURITY.mdが警告する「シェルを経由しないコードパスまでサンドボックスが封じ込めると期待している運用者」と同じ誤りです。
8.4 図で見る、対策の効きどころ
ここまでの話を2枚の図にまとめます。
図1: 現状 — 境界の内側にシークレットが平文で置かれている
赤い線が問題の経路です。chmodをどういじってもこの線は切れません(8.2)。
図2: 対策適用後 — 秘密を境界の外へ出す
対策①②が効くのはAPIキーに対してだけである点に注意が必要です。図2でmemories/とstate.dbを黄色にしているのはそのためで、記憶と会話ログは依然として境界の内側に平文で残ります。5章で見た通り、会話ログにはユーザーが何気なく話した内容がすべて記録されるので、ここはhermes sessions deleteによる削除運用(前編9章)と、そもそも機微情報をエージェントに話さない運用でカバーするしかありません。
| 対策 | 何を守れるか | 何は守れないか |
|---|---|---|
① hermes secrets
|
APIキー(平文が消える) | 記憶・会話ログ |
② hermes egress
|
APIキー(漏れても失効可能なトークンのみ) | 記憶・会話ログ |
| ③ コンテナ化 | 置き場所ごと分離 | Windowsユーザーが侵害された場合 |
| ④ Windowsアカウント保護 | すべて(最終防衛線) | — |
これはHermes固有の問題ではなく、WSL2という実行基盤の特性です。しかしOWASP ASI06が言う「エージェントの記憶システム」を、モデルレベルのプロンプトインジェクションだけでなく、それを支えるファイルシステムとOSアカウントのレイヤーまで含めて考える必要がある、という実例になりました。
9. 結論と考察
仮説ごとに結果をまとめます。番号は前編からの通し番号で、前編ではH1〜H5'(削除・凍結スナップショット・プルーニング効果)を扱いました。後編は保護に関するH6以降です。
メモリ保護
| # | 仮説 | 結果 |
|---|---|---|
| H6 | メモリ書き込みのシークレット検知は、表記が変わっても機能する | ❌ 否定。クォート無し(hardcoded_secret)、絶対パス表記(hermes_env)ですり抜けた |
| H7 | メモリツールでブロックされる文字列は、他の経路でも保存されない | ❌ 否定。同じ文字列が会話ログに平文で残り、session_searchで引用一致で復元できた |
| H8 | 記憶・会話ログはアプリケーション側で暗号化できる | ❌ 否定。MEMORY.mdはUTF-8テキスト、state.dbは素のSQLite。暗号化の実装も設定も存在しない |
バックアップによるデータ保護
| # | 仮説 | 結果 |
|---|---|---|
| H9 | バックアップで記憶とセッションが完全復元できる | ⚠️ 条件付き。full backupは往復確認できたが、--quickはmemories/を含まない
|
| H10 | バックアップ経路が新たな漏洩面を作らない | ❌ 否定。zipは無暗号化(flag_bits=0)・0644で既定は~/.hermesの外。暗号化オプションも無い |
| H10' | 定期バックアップの世代管理が用意されている | ⚠️ 条件付き。スナップショット(20世代)とアップデート前バックアップ(updates.backup_keep、既定5)には有るが、hermes backup(full)には無い
|
OSによる境界保護
| # | 仮説 | 結果 |
|---|---|---|
| H11 | WSL2のPOSIXパーミッションは、Windows側に対する境界になる | ❌ 否定。9P経由は所有者権限で評価され、さらにwsl -u rootがパスワード無しで全て読める |
| H12 | 「唯一の境界はOS」の"OS"を、自分の構成で正しく同定できていた | ❌ 否定。WSLのLinuxだと思っていたが、実際はWindowsのユーザーアカウントだった |
OWASP ASI06(Memory & Context Poisoning)の防御策として挙げられている「検証済みのメモリ書き込み」「ユーザーとタスク単位でのメモリスコープ」は、Hermesのmemoryツールという一つの入口には確かに実装されています。しかし今回突いた境界はどれも、その入口の外側にありました。会話ログ、バックアップzip、そしてOSレベルのファイル共有境界です。「メモリ層にだけ丁寧なガードレールがあり、それ以外の層には同等の保護が及んでいない」という設計上の非対称性が、この記事全体を通した結論です。
ただし、これをHermesの落ち度と読むのは正確ではありません。1章で引いた通り、SECURITY.mdはプロセス内のパターンスキャナは境界ではないと最初から宣言しています。2章の図で緑にした3箇所は、いずれも「破られてもよいヒューリスティック」として置かれたものです。今回の検証で分かったのは、その宣言が実測と正確に一致していたということです。クォートを外すだけで、パスの書き方を変えるだけで、あるいはmemoryツールを使わずに雑談するだけで、緑の枠はすり抜けられました。
意味があるのは、運用者がその輪郭を数字と具体例で把握できることです。「境界ではない」と書いてあるのを読んで納得するのと、api_key=のクォート有無で結果が変わるのを自分の目で見るのとでは、OS側の隔離にどれだけ本気で頼るべきかの実感が違います。
そして8章では、その「頼るべきOS境界」がどこにあるのかを取り違えていたことに気づかされました。WSL2のPOSIXパーミッションは境界に見えて境界ではなく(wsl -u rootで素通り)、実際の境界はWindowsのユーザーアカウントでした。SECURITY.mdが「唯一の境界はOS」と言うとき、自分の構成でその「OS」がどれを指すのかを正しく同定できていなければ、守っているつもりで何も守れていないことになります。
だからこそ対策は、境界の内側で権限をいじることではなく、境界の内側にシークレット(APIキー・認証情報)を平文で置かないこと(hermes secrets、hermes egress)に向かいます。プロセス内のガードは宣言どおり頼りにならず、頼るべきOS境界も自分が思っていた場所とは違った —— この2つが分かったとき何を選ぶかが、運用設計そのものだと思います。
実務上の教訓
- 正規表現ベースのセキュリティガードは、表記ゆれで簡単に抜けうる。 クォートの有無、チルダ表記か絶対パス表記か、といった些細な差でブロックされたりされなかったりする。1つのガードに頼らず、複数の層で守る前提で設計する
- 「守られている入口」の外に、同じ情報を運べる別の入口が無いかを確認する。 メモリツールをブロックしても、会話ログやファイル編集ツールが素通しなら、実効的な保護にはならない
-
hermes backup --quickは記憶を守らない。 記憶も含めた完全なバックアップが必要なら、都度hermes backup(quickなし)を使う - バックアップzipは、それ自体が新しい持ち出し経路になりうる。 無暗号化・広い実効権限のまま出力されるため、共有・保管する際は追加の暗号化を検討する
-
WSL2+Windowsネイティブという分離構成では、WSL側のパーミッションはWindows側からの保護にならない。
.envのような秘匿ファイルは、この構成を選んだ時点で別途の対策(暗号化、置き場所の分離など)を検討したほうがよい
10. まとめ
前編では「忘れる」側の検証で、削除した記憶が複数の経路からまだ参照できることを見ました。後編では逆に「守る」側を検証しましたが、行き着いた結論は驚くほど似ています。保護はmemoryツールという一つの入口には確かに実装されていましたが、その周辺(会話ログ、ファイル編集ツール、バックアップ、OSレベルの境界)には同じ水準の保護が及んでいませんでした。
OWASP Top 10 for Agentic Applications 2026が「記憶」を独立したリスクカテゴリ(ASI06)として立てたのは、まさにこの複雑さゆえだと思います。エージェントの記憶は、単一のデータベースではなく、システムプロンプトのスナップショット・ツールの書き込み経路・会話ログ・バックアップという複数のレイヤーにまたがっています。1つのレイヤーだけを見て「メモリは守られている」と判断するのは早計でした。
今回はHermes Agentを題材にしましたが、得られた教訓のほとんどは実装を問わず当てはまるはずです。整理するとこうなります。
-
記憶を守る仕組みがあるとき、それが「境界」なのか「気休め」なのかを確認する。 Hermesの場合は
SECURITY.mdが「プロセス内のスキャナは境界ではない」と明言していました。同じことを明言してくれる実装ばかりではありませんが、プロセス内で文字列を検査する仕組みは、原理的にヒューリスティックでしかないという構図はどのエージェントでも変わりません - 保護されている入口の「外側」に、同じ情報が流れる経路がないかを探す。 今回はメモリツールが確実にブロックする文字列が、雑談としてなら素通しで会話ログに残り、検索で復元できました。入口が複数あるなら、一番緩い入口がそのシステムの実力です
-
自分の構成における「本当の境界」がどこかを、思い込みでなく実測で同定する。 これが8章で一番刺さった点でした。WSLのファイル権限を境界だと思い込んでいましたが、実際は
wsl -u rootで素通りで、本当の境界はWindowsのユーザーアカウントでした。境界の位置を取り違えていると、守っているつもりで何も守れていないことになります - シークレット(APIキー・認証情報)は、境界の内側に平文で置かない。 権限をいじる方向には解がなく、外部シークレットマネージャや資格情報の差し替えプロキシのように、そもそも平文をディスクに残さない設計に寄せるのが唯一の筋でした
一方で、忘れてはいけないのは対策を入れても記憶と会話ログは平文のまま残るということです(8.4)。APIキーは外に逃がせても、ユーザーが何気なく話した内容は会話ログに蓄積し続けます。結局のところ、前編で扱った削除の運用と、「機微な情報をそもそもエージェントに渡さない」という当たり前の線引きが、最後に効いてきます。
前編・後編を通して見えたのは、「エージェントの記憶」という一見シンプルな機能の裏に、思ったより多くの層が隠れているということでした。OWASPのようなフレームワークは「何を守るべきか」を示してくれますが、そのリスクに対して実装がどこまで応えているかは、実装を確認して、足りない分の追加の対応を検討する必要があります。
参考文献
- OWASP Top 10 for Agentic Applications for 2026(OWASP Gen AI Security Project) — 2025年12月公開、100人以上のセキュリティ専門家による査読。10分類(ASI01〜ASI10)のうち、本記事は主にASI06(Memory & Context Poisoning)を扱った
- Machine Unlearning(Bourtoule et al., IEEE S&P 2021 / arXiv:1912.03817) — SISA trainingという「あらかじめ削除しやすいようにデータと学習を構造化しておく」設計思想。本記事で見た「入口ごとに保護がまちまち」という構造との対比として
- Certified Data Removal from Machine Learning Models(Guo et al., ICML 2020 / arXiv:1911.03030) — 「証明可能な忘却」という概念。バックアップ・会話ログという別経路が残る限り、この基準は満たされないという文脈で参照
参考リンク
- NousResearch/hermes-agent (GitHub)
- NousResearch/hermes-agent の SECURITY.md — 1章で引用した「唯一の境界はOS」の出典
- OWASP Top 10 for Agentic Applications for 2026
関連記事(拙稿)
- Hermes Agent の SECURITY.md が、Agentic AI の運用設計の教材として良く出来ている — 本記事の1章で触れた「境界とヒューリスティックの書き分け」を詳しく読解したもの
- AIエージェントは記憶を忘れられるか(前編) — 凍結スナップショット・削除の実機検証
検証条件の注記
- 検証は2026年8月21〜22日、Hermes Agent v0.19.0(2026.7.20)時点のものです
- ダミーのシークレット文字列・APIキー等はすべて実在しないテスト用の値です。検証終了後、生成した秘密情報混入ファイル(バックアップzip、隔離プロファイル等)は削除しています