本指南適用於能管理 Linux 帳號與私人 GitHub 儲存庫的自架 Hermes Agent 操作人員。目標明確:建立完整 Hermes 使用者資料封存檔,在離開主機前加密,確認已抵達指定私人遠端,再在另一台拋棄式測試機器取得並匯入。這套文件程序對應 Hermes CLI 的指定提交 86fa9e6,檢查日期為 2026 年 10 月 19 日。BIG CHANGE 使用不進行真實操作的本機測試資料,檢查範例 shell 語法與控制流程;未執行真正的 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 參考、常見問題及cron 手冊。確認閘道所屬的作業系統使用者及 HERMES_HOME。全部主機設定與首次手動備份,都以該使用者執行。範例假設使用專用且乾淨的 Git 複本與 main 分支。
Hermes 完整備份包含資料根目錄下的設定、憑證、記憶、技能、工作階段及設定檔。不包含 Hermes 程式碼、相依項目與執行環境下載、既有備份、檢查點、瀏覽器憑證設定檔、SQLite 附屬檔案及部分本機檔案,也不會收集主機其他位置的無關檔案。不要使用 --quick 來達成本指南的完整資料備份。檢查範圍之外的重要項目並另行保護。即使目的地儲存庫為私人,ZIP 仍含有秘密。
在受保護的位置產生 age 身分,也就是解密封存檔所需的私密金鑰;位置必須在 HERMES_HOME、暫存目錄與 Git 複本之外。獨立儲存該身分的復原副本。輸出的接收者 age1… 是公開資訊,可放入設定;私密身分不可。遵循 age 使用參考。
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 的同一作業系統使用者設定非互動式 gh 讀取及 Git SSH 寫入。權杖與 SSH 私密金鑰必須放在 Hermes 資料、暫存區及 Git 之外。另一個帳號的互動式登入不能證明排程存取可用。執行範例前請替換大寫預留位置。如果建立儲存庫時已產生 README 或 main 分支,應檢查並略過衝突的初始化指令。
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 maingh repo create --clone 可能依本機偏好選擇 HTTPS;上面明確指定的 SSH 遠端才是此流程的目的地。僅當 isPrivate 傳回 true、協作者符合預期、有效擷取與推送 URL 各自等於唯一已審查的 SSH URL,且排程使用者能成功遠端存取時,才能繼續。Git 遠端規則允許 URL 改寫、額外目的地及獨立的 pushurl;下方指令碼每次均檢查原始值與有效值。
儲存接下來兩個程式碼區塊前,先離開複本目錄。在 Hermes 資料根目錄、Git 複本、暫存目錄及金鑰儲存區之外,建立私人設定目錄。將 hermes-github-backup.sh 和 hermes-github-backup.conf 儲存於此;安裝指令會將它們複製到所屬設定檔。專用 Git 複本必須保持乾淨。
install -d -m 700 /ABSOLUTE/PRIVATE-SETUP
cd /ABSOLUTE/PRIVATE-SETUP2. 安裝已審查的備份指令碼
將下方完整 Bash 區塊儲存為 hermes-github-backup.sh,放在該私人設定目錄。將第二個區塊儲存為旁邊的 hermes-github-backup.conf,並將所有預留位置換成絕對路徑或公開接收者。這是需要本機審查的 shell 設定,載入前應檢查,權限僅限擁有者。不得包含密碼、權杖或私密 age 身分。指令碼是完整的建議範例;語法與不進行真實操作的控制流程已檢查,但仍須在自己的安裝中調整與測試。它產生的 latest.zip.age 是加密封存檔,也就是密文。
#!/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"# 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 對齊。指令碼鎖定整個流程,直到推送與讀回確認完成;絕不搶占已持有或陳舊的鎖。僅允許將 latest.zip.age 和 latest.sha256 作為新的備份路徑加入 Git 暫存。當密文超過 40,000,000 位元組,或本機 .git 加預留空間接近 500,000,000 位元組 時停止。這是範例的保守限制,不是 GitHub 儲存保證。
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;封存檔不完整時傳回 1,即使 ZIP 已存在;其他備份正在執行時傳回 2。指令碼結束時刪除暫存 ZIP 與記錄;一般刪除不是安全抹除。檔案被略過時,使用獨立、受控的手動 hermes backup -o /PRIVATE-DIAGNOSTIC/hermes.zip 調查,檢查結束狀態與報告,再決定如何保護遺漏資料。
3. 設定排程並檢查首次到期執行
僅在手動結果通過後,由同一設定檔使用 Hermes 文件中的 --no-agent --script 模式。它執行檔案,不會呼叫 LLM。明確選擇頻率及不含秘密的本機狀態輸出。檢查閘道作業系統身分、設定檔及下次到期時間。Hermes 的指令碼逾時值依序來自 HERMES_CRON_SCRIPT_TIMEOUT、cron.script_timeout_seconds,然後採用預設 3,600 秒。依實際觀察到的執行設定並核對合適數值;逾時即失敗。精簡後的指令碼環境不會自動接收 Hermes 管理的供應商憑證,因此依賴排程前,應以實際排程使用者驗證無人值守 gh 與 SSH 存取。
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. 在全新機器驗證取得與匯入
使用另一台拋棄式 Linux 測試機器,不得帶有正式環境的 Hermes 設定檔或閘道。先安裝相容的 Hermes 版本:封存檔是使用者資料,不是應用程式或執行環境映像。給測試機器獨立授權的指定私人儲存庫讀取權,以及獨立保存的 age 身分。確認複本遠端與所選提交。不要在來源主機或現有設定檔執行 hermes import。所有復原的排程與外部操作都應停用,直到有人審查目的地並決定是否啟用。
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 身分遺失,必須有有效的獨立副本才能解密。明文秘密進入 Git 時,先輪替洩漏的憑證;之後刪除檔案不會清除較早歷史或其他複本。
GitHub 大檔案指南在 50 MiB 時警告,並阻止超過 100 MiB 的一般 Git 檔案;同時指出 Git 並非設計為備份工具。範例刻意在這些限制以下停止。每個變動的密文都留在 Git 歷史,即使 latest.zip.age 已被替換。不要默默提高 40 MB/500 MB 停止限制、改寫歷史,或將私人設定當成加密。大小或復原檢查失敗時,重新評估備份專用儲存或另行審查的 LFS 方案。帳號方案、儲存、網路及主機費用仍適用;無代理程式指令碼本身不需要 LLM 呼叫。未測量本程序的時長、成功率或節省費用。
另一個範圍較小的 Hermes 排程工作可參閱如何設定以技能為基礎的週期性 Hermes 工作並驗證首次執行。該工作的報告檢查不能取代加密封存檔及復原演練。
來源與延伸閱讀
- Hermes Agent CLI 指令,固定提交
86fa9e6— 完整備份指令、範圍、排除項與結束狀態。2026 年 10 月 19 日對照固定來源檢查;不證明範例在真實設定檔執行過。 - Hermes Agent 常見問題,同一固定提交 — 完整備份傳輸、
hermes import和hermes setup;封存檔是資料,不是執行環境映像。 - Hermes 排程工作手冊,同一固定提交 — 無代理程式指令碼模式、交付、精簡環境、閘道與逾時。設定排程前檢查已安裝版本的說明。
- age README — 接收者加密與身分解密。持續變動的
main頁面於 2026 年 10 月 19 日檢查;本機惰性測試未測試真正加密。 - GitHub 儲存庫隱私、建立儲存庫及GitHub CLI repo view — 私人存取與可見性讀回。私人存取仍依賴已審查協作者及憑證。
- Git remote 和 Git push — 有效目的地與推送參照;指令碼額外檢查本次執行的精確遠端頂端提交。
- GitHub 大檔案指南及敏感資料移除 — Git 大小與歷史限制,以及憑證洩漏後為何應輪替,而不是只刪檔案。這些持續變動的手冊於 2026 年 10 月 19 日檢查。



