偽のHERMES_HOMEで試したのに、hermes backupは本物のホームを1.1GBのZIPにした

偽のホームには、バックアップの対象になるファイルを8個置きました。hermes backup が書き出したZIPは、4487ファイルで展開前1.1GBです。
最初は数字の読み違いを疑いました。違いました。中身は、私が毎日使っている本物の ~/.hermes です。
鍵が入るZIPは、作る前に中身を知っておきたい
hermes backup --help には、次の説明があります。
Create a zip archive of your entire Hermes configuration, skills, sessions,
and data (excludes the hermes-agent codebase). Use --quick for a fast snapshot
of just critical state files.
--quick の側は「config、state.db、.env、auth、cron」だけを取る、とも書いてあります。
案件のメモ、定期ジョブ、各サービスの鍵をエージェントに持たせていると、更新の前にこのZIPを取っておきたくなります。ただ、取った瞬間に鍵入りのファイルが1つ増えるのも事実です。何が入るのかは、先に知っておきたいところでした。ならば鍵のない偽のホームで試そう、と考えました。ここまでは自然な発想だったと思っています。
1回目: 偽のホームを指定したのに、本物が走った
偽のホームは fakehome という名前で、~/.hermes の下に作りました。置いたのは config.yaml、ダミーの .env と auth.json、メモ、スキル、cron/jobs.json、ログ、SQLiteの state.db の8個です。ダミーの鍵文字列を auth.json、ログ、state.db のメッセージ表にも1つずつ入れました。このほか、バックアップから外れるはずのコード置き場として hermes-agent/run.py を1つ置いています。
HERMES_HOME=$PWD/fakehome hermes backup -o out/demo.zip
出力は次のとおりです。パスの途中は省略し、進捗行も落としています。
Scanning ~/.hermes/…/fakehome ...
Backing up 4487 files ...
Backup complete: …/out/demo.zip
Files: 4487
Original: 1.1 GB
Compressed: 510.5 MB
Time: 60.3s
画面の Scanning は偽ホームの名前を出しています。表示だけでは間違いに気づけません。引っかかったのは件数です。8個のはずが4487個。しかも60秒かかっています。
ZIPの中身は、名前とサイズだけを確認しました。実行したスクリプトから該当部分を抜きます。公開用に、ファイル名を並べていたパターンは一般化し、出力も件数だけにしました。
import zipfile, re
z = zipfile.ZipFile('out/demo.zip')
infos = z.infolist()
names = [i.filename for i in infos]
print('entries', len(names))
for n in ['logs/agent.log','memories/MEMORY.md','skills/demo/SKILL.md','cron/jobs.json']:
print(n, n in names, [i.file_size for i in infos if i.filename == n])
fake_env = [i for i in infos if i.filename == '.env']
print('.env size in zip', fake_env[0].file_size, '(fake .env was 105 bytes)')
print('state.db size', [i.file_size for i in infos if i.filename == 'state.db'], '(fake was 8192)')
pat = re.compile(r'(^|/)(\.env(\.bak.*)?|auth\.json|[a-z_]+_(token|secret|auth)\.json)$')
print('secret-named files:', len([n for n in names if pat.search(n)]))
entries 4487
logs/agent.log True [3946236]
memories/MEMORY.md True [2983]
skills/demo/SKILL.md False []
cron/jobs.json True [212457]
.env size in zip 24839 (fake .env was 105 bytes)
state.db size [872005632] (fake was 8192)
secret-named files: 9
偽ホームに置いた skills/demo/SKILL.md はありません。.env は105バイトのはずが24839バイト。state.db は8192バイトのはずが872MBです。別プロファイルの .env まで入っていました。
ZIPの権限も見ました。664は、同じマシンの別ユーザーも読める値です。
stat -c '%a %s %n' out/demo.zip
664 535346435 out/demo.zip
この時点で、鍵ファイル9個を含む510MBのZIPが手元に1つ。鍵らしい文字列は、DBと画像を除く60MB以下の4296ファイルを、OpenRouter、GitHub、Slack、Google形式のパターンで数えました。値は表示していません。ヒットしたのは bin/ 下のセキュリティ検査ツールのバイナリだけで、検出用の見本文字列と見ています。確認はしていません。数え終えたら、ZIPをすぐ削除しました。
原因は、偽ホームを置いた場所でした
v2026.9.24 の hermes_constants.py(get_default_hermes_root の行227〜232)を読みました。
result = native_home
if env_path is not None:
try:
env_path.resolve().relative_to(native_home.resolve()) # under ~/.hermes (normal or profile mode)
except ValueError: # Docker/custom root: <root>/profiles/<name> -> <root>, else HERMES_HOME itself
result = env_path.parent.parent if env_path.parent.name == "profiles" else env_path
HERMES_HOME が ~/.hermes の下にあると、ルートは本物の ~/.hermes に戻されます。~/.hermes の下は通常のホームかプロファイルとみなす作りです。私の偽ホームは、まさにその下でした。外に置けば、HERMES_HOME 自身がルートになります。
2回目: ~/.hermes の外で、やり直した
偽ホームを ~/.cache/backup-demo/home に作り直しました。中身は1回目と同じ構成です。
B=$HOME/.cache/backup-demo
cd $B
HERMES_HOME=$B/home hermes backup -o $B/out/demo.zip
hermes: preparing dependencies for this data root (the checkout's updates belong to ~/.hermes)...
Preparing the isolated Hermes runtime…
...
Scanning ~/.cache/backup-demo/home ...
Backing up 14 files ...
Backup complete: ~/.cache/backup-demo/out/demo.zip
Files: 14
Original: 11.2 KB
Compressed: 3.8 KB
Time: 0.0s
今度は11.2KB。それでも14ファイルあります。対象として置いたのは8個なので、残る6個は、Hermesが偽ホームの中に自分で作ったものでした。SOUL.md、logs/errors.log、更新中を示すロックファイル、それに logs/update_receipts/ の下の更新記録2つと、その書き込み用のロックです。logs/agent.log も、私が書いた48バイトではなく764バイトになっていました。
初回だけは依存関係の準備が走りました。後で測ると、backup-demo 全体が1.6GBに膨らんでいました。試験用だからと軽く考えると、容量を食います。
ダミーの鍵は、鍵ファイル以外にも残った
ダミー文字列 DUMMY-ACCESS-TOKEN-0001 を、ZIPの中で数えました。
auth.json 1
state.db 1
logs/agent.log 1
auth.json にあるのは当然です。想定外だったのは state.db とログでした。偽の会話として「この会話に〜を貼ってしまった」という1行を入れてあります。鍵ファイルを除いても、会話の記録とログには鍵が残るわけです。.env に入れた偽のOpenRouter形式の鍵も1件ありました。
ZIPから .env と auth.json を抜く手もあります。それでも state.db は残ります。この種のZIPは、丸ごと鍵入りとして扱うほうが安全だと考えを改めました。
権限は、umaskで600にできた
2回目のZIPも664でした。umask を絞って作り直すと、権限が変わります。
(umask 077; HERMES_HOME=$B/home hermes backup -o $B/out/demo-umask077.zip)
stat -c '%a %n' out/demo-umask077.zip
600 out/demo-umask077.zip
公開リポジトリには、ZIPを所有者だけが読める600で作るコミット(f19edb53)もあります。ただ、10月22日時点の main にも、最新リリースの v0.22.4(10月22日公開)にも入っていません。手元の版も664でした。更新したあとも、stat で確かめてから使うのが確実です。
復元すると、鍵ファイルは600に締まる
最後に、別の空のディレクトリへ戻しました。
mkdir -p restore
HERMES_HOME=$B/restore hermes import out/demo.zip --force
Backup contains 14 files
Target: ~/.cache/backup-demo/restore
Checking archive integrity ...
Importing 14 files ...
Import complete: 14 files restored in 0.1s
Target: ~/.cache/backup-demo/restore
Note: The hermes-agent codebase was not included in the backup.
If this is a fresh install, run: hermes update
Restored into a non-default home; leaving the gateway service alone to avoid clashing with the install at ~/.hermes.
To start a gateway for this home, run: hermes gateway run
Done. Your Hermes configuration has been restored.
config.yaml、.env、auth.json、メモ、スキル、cron/jobs.json の6ファイルは cmp で元と一致しました。復元後の .env、auth.json、state.db は600です。ZIPは664なのに、戻す側は締めてくれます。弱いのは、ZIPの置き場です。
非デフォルトのホームへ戻すときは、ゲートウェイを触らずに止まる親切な動きでした。本物の ~/.hermes に戻す試験は、まだしていません。
次に取るバックアップのために決めたこと
- 試験用の偽ホームは
~/.hermesの外に置く。lsで置いた場所を確かめる - 本物のホームを指すZIPは
umask 077で作り、同期フォルダや販売用のリポジトリには置かない --quickの中身はstate.dbを含むので、鍵入りの扱いは変えない
迷っていることもあります。510MBのZIPには、872MBの state.db が入っていました。本当に戻したいのは、設定と定期ジョブと記憶だけではないのか。ただ --quick も state.db を含むので、これで足りるか、どれだけ小さくなるかは、本物ではまだ試していません。次は同じ手順で測ります。
踏んだところ
| 症状 | 原因 | 対応 |
|---|---|---|
| 8個のはずが4487ファイルのZIPになった | 偽ホームを ~/.hermes の下に置いた | ~/.hermes の外に置く |
| 偽ホームに置いていないファイルが増えた | Hermes自身が SOUL.md やログを作る | 作られる前提で数える |
| 偽ホームが1.6GBになった | 初回に依存関係の準備が走った | 試験後に削除する |
| ZIPが664になった | 手元の版は umask 任せ(600で作るコミットは、main・v0.22.4・v2026.9.24 のいずれにも未収録) | umask 077 で作り、更新後も stat で確かめる |
