この手順は Linux アカウントと非公開 GitHub リポジトリを管理できる、自前運用の Hermes Agent 担当者向けです。目標は明確です。Hermes の全ユーザーデータをアーカイブし、ホストから出す前に暗号化し、意図した非公開リモートへの到着を確認して、別の使い捨て検証マシンで取得・インポートします。根拠は Hermes CLI の固定コミット 86fa9e6で、2026年10月19日に確認しました。BIG CHANGE は無害なローカル検証データでシェル構文と制御の流れを確認しましたが、実際の Hermes バックアップ、age 暗号化、GitHub 送信、定時起動、復元は行っていません。以下をすべて自分で確認するまで、自分の環境で実証されたバックアップとは扱わないでください。

大きな変化

  • 変わったこと: Hermes の全体バックアップコマンドは作業データを ZIP にまとめ、エージェントなしの cron モードは確認済みスクリプトを実行できます。組み合わせると、AI モデルにバックアップをさせず、暗号化コピーを定期作成できます。
  • 重要な理由: 担当者は、Hermes がアーカイブを完成したか、暗号文が意図した非公開リポジトリに届いたか、新規インストールで取り込めるかを別々に確認できます。各確認は異なる障害を捉えます。ただしアプリ本体と一部のマシン固有データは含まれません。
  • 注意点: アーカイブと増え続ける Git 履歴が、この限定的な方法に収まるほど小さいかが実務上の判断点です。復旧演習の失敗やいずれかの容量停止条件に当たれば、定期実行に頼る前に別の保存計画が必要です。

1. ホスト、鍵、リポジトリを準備する

Linux、Bash、GNU stat -c と du -sb に加え、インストール済み Hermes CLI、Git、GitHub CLI(gh)、age、unzip、sha256sum、SSH、cron 用に稼働する Hermes ゲートウェイを用意します。インストールされた hermes backup、hermes import、hermes cron create --help が、固定コミットの CLI リファレンス、FAQ、cron マニュアルと一致するか確認します。ゲートウェイを所有する OS ユーザーと HERMES_HOME を特定します。ホスト設定と初回手動バックアップはすべてそのユーザーで行います。例は専用のクリーンな Git クローンと main ブランチを前提とします。

Hermes の全体バックアップにはデータルート配下の設定、認証情報、記憶、スキル、セッション、プロファイルが含まれます。Hermes 本体コード、依存関係や実行環境のダウンロード、以前のバックアップ、チェックポイント、ブラウザー認証プロファイル、SQLite の付随ファイル、一部のローカルファイルは除外されます。ホストの他の場所にある無関係なファイルも集めません。この全データ保存の目的では --quick を使わないでください。範囲外の重要なデータは調べて別に保護します。保存先が非公開でも ZIP には秘密情報が入っています。

アーカイブの復号に必要な秘密鍵である age の identity を、保護した場所に生成します。場所は HERMES_HOME、一時作業ディレクトリ、Git クローンの外にします。identity の復旧用コピーを独立した保管先に置きます。表示された受信者 age1… は公開情報なので設定に入れますが、秘密の identity は入れません。age の使用リファレンスに従ってください。

Terminal
umask 077
install -d -m 700 /PRIVATE-KEY-STORE
age-keygen -o /PRIVATE-KEY-STORE/hermes-backup-identity.txt
age-keygen -y /PRIVATE-KEY-STORE/hermes-backup-identity.txt

このバックアップ専用の非公開リポジトリを作り、利用者を確認し、Hermes cron と同じ OS ユーザーで非対話の gh 読み取りと Git SSH 書き込みを設定します。トークンと SSH 秘密鍵は Hermes データ、一時作業場所、Git の外に置きます。別アカウントでの対話ログインは定期実行のアクセス証明になりません。例を実行する前に大文字の仮置き値を置き換えます。リポジトリ作成時に README または main ブランチが既に作られた場合は確認し、衝突する初期化コマンドを省略します。

Terminal
gh repo create ACCOUNT/PRIVATE-REPOSITORY --private --clone
gh repo view ACCOUNT/PRIVATE-REPOSITORY --json isPrivate --jq .isPrivate
cd PRIVATE-REPOSITORY
git remote set-url origin git@github.com:ACCOUNT/PRIVATE-REPOSITORY.git
git remote get-url --all origin
git remote get-url --push --all origin
git branch -M main
printf '%s\n' 'Encrypted Hermes backup storage; ciphertext and checksum only.' > README.md
git add README.md
git commit -m 'Initialize private backup repository'
git push -u origin HEAD:main
git ls-remote --heads origin main

gh repo create --clone はローカル設定から HTTPS を選ぶ場合があります。上記の明示的 SSH リモートがこの手順の送信先です。isPrivate が true を返し、共同作業者が意図した人のみで、実効の取得・送信 URL がそれぞれ確認済みの一つの SSH URL と一致し、定期実行ユーザーでリモートアクセスが成功する場合にだけ進みます。Git リモートの規則は URL 書き換え、追加送信先、独立した pushurl を許します。以下のスクリプトは毎回、元の値と実効値を調べます。

次の二つのコードを保存する前にクローンから出ます。Hermes データルート、Git クローン、一時作業場所、鍵保管先の外に非公開の設定ディレクトリを作ります。hermes-github-backup.sh と hermes-github-backup.conf をそこに保存します。インストールコマンドが所有プロファイルへコピーします。専用 Git クローンはクリーンに保ちます。

Terminal
install -d -m 700 /ABSOLUTE/PRIVATE-SETUP
cd /ABSOLUTE/PRIVATE-SETUP

2. 確認済みのバックアップスクリプトを置く

以下の Bash ブロック全体を hermes-github-backup.sh として非公開の設定ディレクトリに保存します。二つ目は hermes-github-backup.conf として隣に置き、すべての仮置き値を絶対パスまたは公開受信者に置き換えます。読み込む前にローカルで確認するシェル設定なので、所有者だけの権限にします。パスワード、トークン、秘密の age identity は含めません。これは提案する完全なサンプルです。構文と無害な制御フローは確認しましたが、自分の環境で調整・試験が必要です。生成される latest.zip.age は暗号化アーカイブ、つまり暗号文です。

Terminal
#!/usr/bin/env bash
# Documentation-based Linux sample for an operator to review and adapt.
# Place with hermes-github-backup.conf under the owning HERMES_HOME/scripts/.
# Requires GNU stat -c and du -sb. Git Bash was used only for inert fixtures.
set -Eeuo pipefail
umask 077

script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
owning_home=$(CDPATH= cd -- "$script_dir/.." && pwd -P)
if [[ -n ${HERMES_HOME:-} && $(CDPATH= cd -- "$HERMES_HOME" && pwd -P) != "$owning_home" ]]; then
  printf 'Backup stopped: cron profile and script directory differ.\n' >&2
  exit 1
fi
export HERMES_HOME="$owning_home"

config="$script_dir/hermes-github-backup.conf"
[[ -f "$config" ]] || { printf 'Backup stopped: reviewed configuration missing.\n' >&2; exit 1; }
# This file is local, owner-reviewed shell configuration. It must contain no secret.
# shellcheck source=/dev/null
source "$config"
: "${HERMES_BIN:?set an absolute Hermes executable path}"
: "${REPO_DIR:?set an absolute private clone path}"
: "${STAGING_PARENT:?set an absolute private staging path}"
: "${AGE_RECIPIENT:?set an age public recipient}"
: "${EXPECTED_REMOTE:?set the exact private GitHub SSH remote}"
: "${BRANCH:=main}"

fail() { printf 'Backup stopped: %s.\n' "$1" >&2; exit 1; }
for path in "$HERMES_BIN" "$REPO_DIR" "$STAGING_PARENT"; do
  [[ $path == /* ]] || fail 'a configured path is not absolute'
done
[[ -x "$HERMES_BIN" ]] || fail 'Hermes executable unavailable'
[[ $AGE_RECIPIENT == age1* ]] || fail 'age public recipient invalid'
[[ $EXPECTED_REMOTE == git@github.com:*/*.git ]] || fail 'remote is not the reviewed GitHub SSH URL'
[[ $BRANCH =~ ^[a-zA-Z0-9._/-]+$ ]] || fail 'branch name invalid'
[[ -d "$STAGING_PARENT" ]] || fail 'staging directory unavailable'
stage_parent=$(CDPATH= cd -- "$STAGING_PARENT" && pwd -P)
[[ $stage_parent != "$owning_home" && $stage_parent != "$owning_home"/* ]] || fail 'staging is inside Hermes data'

# Atomic, nonblocking whole-flow lock: keep it through push and remote readback.
# A stale directory after a crash requires operator inspection; never steal it.
lockdir="$stage_parent/.hermes-github-backup-flow.lock"
mkdir -- "$lockdir" 2>/dev/null || fail 'another backup flow holds the lock or a stale lock needs review'
work=''
cleanup() {
  if [[ -n $work ]]; then
    rm -f -- "$work/hermes.zip" "$work/latest.zip.age" "$work/latest.sha256" "$work/backup.log"
    rmdir -- "$work" 2>/dev/null || true
  fi
  rmdir -- "$lockdir" 2>/dev/null || true
}
trap cleanup EXIT

[[ -d "$REPO_DIR/.git" ]] || fail 'Git repository unavailable'
repo=$(CDPATH= cd -- "$REPO_DIR" && pwd -P)
[[ $repo != "$owning_home" && $repo != "$owning_home"/* ]] || fail 'repository is inside Hermes data'
[[ $stage_parent != "$repo" && $stage_parent != "$repo"/* ]] || fail 'staging is inside Git repository'
[[ $repo != "$stage_parent"/* ]] || fail 'Git repository is inside staging'

command -v age >/dev/null || fail 'age unavailable'
command -v gh >/dev/null || fail 'GitHub CLI unavailable for private visibility check'
command -v unzip >/dev/null || fail 'zip verifier unavailable'
command -v git >/dev/null || fail 'Git unavailable'
remote_repo=${EXPECTED_REMOTE#git@github.com:}
remote_repo=${remote_repo%.git}
visibility=$(gh repo view "$remote_repo" --json isPrivate --jq .isPrivate 2>/dev/null) || fail 'GitHub repository visibility unreadable'
[[ $visibility == true ]] || fail 'GitHub repository is not confirmed private'
raw_fetch_urls=$(git -C "$repo" config --get-all remote.origin.url) || fail 'Git fetch URL missing'
[[ $raw_fetch_urls == "$EXPECTED_REMOTE" ]] || fail 'Git has multiple or unexpected raw fetch URLs'
raw_push_urls=$(git -C "$repo" config --get-all remote.origin.pushurl || true)
[[ -z $raw_push_urls ]] || fail 'separate Git pushurl requires independent review'
effective_fetch_urls=$(git -C "$repo" remote get-url --all origin) || fail 'effective fetch URL unreadable'
effective_push_urls=$(git -C "$repo" remote get-url --push --all origin) || fail 'effective push URL unreadable'
[[ $effective_fetch_urls == "$EXPECTED_REMOTE" ]] || fail 'Git fetch URL rewrite or extra destination'
[[ $effective_push_urls == "$EXPECTED_REMOTE" ]] || fail 'Git push URL rewrite or extra destination'
[[ $(git -C "$repo" branch --show-current) == "$BRANCH" ]] || fail 'local branch changed'
[[ -z $(git -C "$repo" status --porcelain --untracked-files=all) ]] || fail 'repository has unrelated changes'

# No interactive prompt is possible in an unattended Hermes cron tick.
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND='ssh -o BatchMode=yes'
remote_head=$(git -C "$repo" ls-remote --heads origin "$BRANCH" | awk '{print $1}')
[[ -n $remote_head && $remote_head == "$(git -C "$repo" rev-parse HEAD)" ]] || fail 'remote unreadable or branch has changed'

work=$(mktemp -d "$stage_parent/hermes-backup.XXXXXXXX") || fail 'private staging creation failed'

if "$HERMES_BIN" backup -o "$work/hermes.zip" >"$work/backup.log" 2>&1; then
  :
else
  backup_rc=$?
  fail "Hermes full backup returned $backup_rc; temporary zip/log will be removed, so diagnose with a separate controlled manual run"
fi
[[ -s "$work/hermes.zip" ]] || fail 'Hermes archive absent or empty'
unzip -tq "$work/hermes.zip" >"$work/backup.log" 2>&1 || fail 'zip integrity check failed'
age -r "$AGE_RECIPIENT" -o "$work/latest.zip.age" "$work/hermes.zip" >"$work/backup.log" 2>&1 || fail 'archive encryption failed'
[[ -s "$work/latest.zip.age" ]] || fail 'encrypted archive absent or empty'

# Deliberately below GitHub's 50 MiB warning. Git history retains every version.
cipher_bytes=$(stat -c %s "$work/latest.zip.age")
(( cipher_bytes <= 40000000 )) || fail 'encrypted archive exceeds this small-repository recipe'
git_bytes=$(du -sb "$repo/.git" | awk '{print $1}')
(( git_bytes + cipher_bytes + 10000000 <= 500000000 )) || fail 'repository history approaches the review limit'
hash=$(sha256sum "$work/latest.zip.age" | awk '{print $1}')
printf '%s  latest.zip.age\n' "$hash" >"$work/latest.sha256"

cp -- "$work/latest.zip.age" "$repo/latest.zip.age"
cp -- "$work/latest.sha256" "$repo/latest.sha256"
git -C "$repo" add -- latest.zip.age latest.sha256
staged=$(git -C "$repo" diff --cached --name-only)
[[ $staged == $'latest.sha256\nlatest.zip.age' ]] || fail 'staged paths are not the two reviewed files'
git -C "$repo" diff --cached --check >/dev/null || fail 'staged content check failed'
git -C "$repo" commit -m "Encrypted Hermes backup $(date -u +%Y-%m-%dT%H:%M:%SZ)" >/dev/null || fail 'local backup commit failed'
run_head=$(git -C "$repo" rev-parse HEAD)
(( $(du -sb "$repo/.git" | awk '{print $1}') <= 500000000 )) || fail 'Git history exceeds review limit before push'
[[ $(git -C "$repo" rev-parse HEAD) == "$run_head" ]] || fail 'local HEAD changed before push'
git -C "$repo" push origin "$run_head:refs/heads/$BRANCH" >/dev/null || fail 'remote push failed; inspect exact remote before retrying'
remote_head=$(git -C "$repo" ls-remote --heads origin "$BRANCH" | awk '{print $1}')
[[ $remote_head == "$run_head" ]] || fail 'this run commit not confirmed at remote'
printf 'Encrypted backup verified at private remote commit %s.\n' "$run_head"
Terminal
# No passwords, tokens, age identities or private keys belong here.
HERMES_BIN=/absolute/path/to/hermes
REPO_DIR=/absolute/path/to/dedicated-private-github-clone
STAGING_PARENT=/absolute/path/to/private-staging-outside-hermes-and-git
AGE_RECIPIENT=age1replace_with_operator_public_recipient
EXPECTED_REMOTE=git@github.com:ACCOUNT/PRIVATE-REPOSITORY.git
BRANCH=main

一時作業場所は Hermes データルートと Git クローンの外に作ります。確認済みファイルを所有プロファイルの scripts/ ディレクトリに配置します。毎回、クローンをクリーンにし、リモート main と揃えます。スクリプトは送信と読み戻しまで全工程をロックし、保持中や古いロックを横取りしません。Git に新規追加するバックアップのパスは latest.zip.age と latest.sha256 だけです。暗号文が 40,000,000 バイト を超えるか、ローカル .git と余裕分が 500,000,000 バイト に近づくと停止します。これはサンプルの保守的な制限であり、GitHub の保存保証ではありません。

Terminal
export HERMES_HOME=/absolute/path/to/owning-hermes-data-root
install -d -m 700 /ABSOLUTE/PRIVATE-STAGING
install -d -m 700 "$HERMES_HOME/scripts"
install -m 700 hermes-github-backup.sh "$HERMES_HOME/scripts/hermes-github-backup.sh"
install -m 600 hermes-github-backup.conf "$HERMES_HOME/scripts/hermes-github-backup.conf"
"$HERMES_HOME/scripts/hermes-github-backup.sh"

手動実行は終了コードゼロで終わり、リモートを再読してから、その実行で作ったコミットを表示するはずです。gh repo view が引き続き非公開を示すこと、クローンの追跡対象パス、意図したリモートの正確なコミットを確認します。ローカルのアーカイブやコミットだけでは外部保存になりません。止まった実行を成功として記録しないでください。Hermes の全体バックアップは、選択した全ファイルを追加した場合にのみ 0、ZIP があっても不完全なら 1、別のバックアップが実行中なら 2 を返します。スクリプトは終了時に一時 ZIP とログを削除しますが、通常の削除は安全な消去ではありません。ファイルが飛ばされた場合は独立した管理下の手動 hermes backup -o /PRIVATE-DIAGNOSTIC/hermes.zip で調べ、終了状態とレポートを確認し、省略された内容の保護を判断します。

3. 定期実行を設定して初回を確認する

手動の結果を得てから、同じプロファイルで文書化された Hermes の --no-agent --script モードを使います。LLM を呼ばずにファイルを実行します。頻度を意図的に決め、秘密情報を含まない状態をローカルへ出します。ゲートウェイの OS ユーザー、プロファイル、次回時刻を確認します。Hermes のスクリプト制限時間は HERMES_CRON_SCRIPT_TIMEOUT、次に cron.script_timeout_seconds、最後に既定の3,600秒の順で決まります。観察した実行に合う値を設定・確認します。時間切れは失敗です。制限されたスクリプト環境には Hermes 管理のプロバイダー認証情報が自動で渡らないため、依存する前に実際の定期実行ユーザーで無人の gh と SSH アクセスを確認します。

Terminal
hermes cron create '0 3 * * *' --no-agent --script hermes-github-backup.sh --deliver local --name 'Encrypted Hermes backup'
hermes cron list
hermes cron status
hermes cron doctor

最初の予定された実行時刻後、正確なジョブ ID と実行記録、ローカル出力(既定プロファイルでは ~/.hermes/cron/output/)、障害記録、意図したリモートの先頭コミットを調べます。ゲートウェイが稼働を続け、その起動で作られたリモートコミットを実行が報告したか確認します。手動実行は cron が動いた証明になりません。出力がなければ同じジョブとゲートウェイを調べ、二つ目の予定を作らないでください。状態メッセージに ZIP 内容、秘密、認証情報を含めません。

4. 新しいマシンで取得とインポートを実証する

使用するのは、本番 Hermes プロファイルやゲートウェイのない、別の使い捨て Linux マシンです。まず互換性のある Hermes を入れます。アーカイブはユーザーデータで、アプリや実行環境のイメージではありません。テストマシンに、意図した非公開リポジトリと別保管の age identity への独立した許可済み読み取り権限を与えます。クローンのリモートと選んだコミットを確認します。ソースホストや既存プロファイルでは hermes import を実行しません。復元されたすべての予定と外部操作は、人が送信先を確認して有効化を決めるまで無効のままにします。

Terminal
umask 077
unset HERMES_HOME
git clone git@github.com:ACCOUNT/PRIVATE-REPOSITORY.git /PRIVATE-RECOVERY/hermes-backup-clone
cd /PRIVATE-RECOVERY/hermes-backup-clone
sha256sum -c latest.sha256
age --decrypt -i /PRIVATE-KEY-STORE/hermes-backup-identity.txt -o /PRIVATE-RECOVERY/hermes.zip latest.zip.age
unzip -tq /PRIVATE-RECOVERY/hermes.zip
hermes import /PRIVATE-RECOVERY/hermes.zip
hermes setup
hermes cron list

インポートされたプロファイル、スキル、記憶、セッション、cron 定義を意図した保護対象と照合します。hermes setup はプロバイダー設定を調べますが、調整が必要な認証情報やマシン固有パスも確認します。実際の結果を記録します。チェックサム一致と復号できる ZIP だけでは使える復元の証明になりません。演習後は自組織の取扱方針に従って平文の検証 ZIP を削除します。通常削除に安全な消去の保証はありません。

確認で止まった場合

Hermes バックアップ不完全、ZIP・暗号化エラー、予期しない実効 Git URL、非公開でないか読めないリポジトリ、工程ロック保持、容量停止、送信失敗、リモートコミット不一致、インポート失敗のいずれも、その実行を未確認にします。原因を解決し、再試行前に現状を調べます。送信結果が不明なら正確なリモートブランチとコミットを先に確認します。クラッシュ後にロックが残れば、生きたプロセスとローカル・リモート状態を調べ、使用中ロックを絶対に削除しません。秘密の age identity を失うと復号には独立した有効コピーが必要です。平文秘密が Git に入ったら公開された認証情報を先に更新します。後のファイル削除では以前の履歴や他のクローンは消えません。

GitHub の大容量ファイルガイドは 50 MiB で警告し、100 MiB を超える通常の Git ファイルを禁止し、Git はバックアップ用途に設計されていないと述べています。サンプルは意図的にその手前で止まります。暗号文を変えるたびに Git 履歴へ残り、latest.zip.age を置き換えても消えません。40 MB/500 MB の停止値を黙って上げたり、履歴を書き換えたり、非公開設定を暗号化だと思い込んだりしないでください。容量や復旧確認に失敗したらバックアップ専用保存先か別途審査した LFS 計画を再検討します。アカウント、保存、通信、ホスト費用はかかり得ますが、エージェントなしのスクリプト自体は LLM 呼び出し不要です。所要時間、成功率、節約効果は測っていません。

別の狭い Hermes 定期実行タスクは、スキルを使う周期的 Hermes ジョブを設定し初回を確認する方法を参照してください。そのレポート確認は暗号化アーカイブと復旧演習の代わりではありません。

出典と参考資料