Hermes Agentのdenyルールでforce pushを止める|YOLOより強い拒否線を作る

git push --force origin mainを試しても、GitHub へは 1 byte も送られませんでした。Hermes Agent のapprovals testが実行前に判定し、終了コード 3 で拒否したからです。
verdict : user-deny (exit 3)
rule : git push --force*
通常の承認は、人が「今回だけ許可する」と答えられます。しかし、force push や remote script の実行は、承認の選択肢に出したくない場合があります。私は公開先や共有履歴を壊す操作に、モデルが越えられない拒否線を置く方が安心です。
今回はapprovals.denyへ 2 件を登録し、通常時、YOLO 時、Docker backend で判定がどう変わるかを実機で確認します。
前回の権限設計から一段狭くする
前回の無人で動かすAIコーディングエージェントの権限設計では、書き込み、network、承認、Git push を製品別に分けました。今回は Hermes Agent だけに絞り、禁止した command を事前に検査します。
| 前回完了時 | 今回完了後 |
|---|---|
| 危険操作には承認を挟みます | 特定操作は承認画面へ進む前に拒否します |
approvals.cron_mode: denyで無人時の確認待ちを防ぎます | approvals.denyで対話時と無人時に同じ禁止線を使います |
| Git pushを独立した権限として扱います | 通常のpushを残し、force pushだけを止めます |
| 実行環境で権限範囲を変えます | localとDockerでguardの適用差を検査します |
すでに deny rule を設定済みなら、「実行せずに 4 通りを試す」まで飛ばせます。
なぜpromptではなく実行前のguardに置くのか
「main へ force push しないでください」と prompt に書く方法は必要です。ただし、これはモデルへの指示です。長い作業や外部文書の読み込みが重なると、指示の優先度を誤る余地が残ります。
approvals.denyは terminal command の実行前に働きます。一致した command は、--yolo、/yolo、approvals.mode: offより先に拒否されます。agent へ返る message にも、再試行や言い換えをしないよう明記されます。
私は prompt を進行方向、deny rule を道路の車止めとして分けています。自律化を進めるほど、守る対象は model の外へ置きます。
用語表
| 用語 | 今回の意味 |
|---|---|
| dangerous command | Hermes Agent が破壊的と判定し、通常は承認を求める command です |
approvals.mode | smart、manual、offから承認方針を選ぶ設定です |
| YOLO | dangerous commandの承認を省くセッション modeです |
| hardline blocklist | root filesystemの削除など、設定にかかわらず拒否する組み込み規則です |
approvals.deny | 利用者が追加する拒否用のglob patternです |
| glob | *や?で文字列へ一致させるshell風patternです。正規表現ではありません |
| normaliZed trace | quoteによる分割などを正規化し、判定に使ったcommand候補です |
| exit code 3 | approvals testがhardlineまたはuser denyで拒否した結果です |
判定順を先に見る
今回触る範囲は、次の順で動きます。
terminal command
│
▼
┌──────────────────────────────┐
│ 1. isolated containerか │── yes ──▶ guardを省略
└──────────────┬───────────────┘
│ no
▼
┌──────────────────────────────┐
│ 2. hardline blocklist │── match ─▶ 拒否
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 3. approvals.deny │── match ─▶ 拒否
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 4. YOLO / mode: off │── on ────▶ 許可
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 5. allowlist / 危険pattern │── match ─▶ 許可または承認
└──────────────────────────────┘
deny rule は YOLO より前です。ここが単なる承認設定との違いです。
一方、Docker、Singularity、Modal、Daytona、Vercel Sandbox のような isolated backend は、container 自体を境界として command guard を省きます。deny rule を host の防御として使う設計だからです。container 内でも command を禁止したい場合は、image、権限、network policy で別に制限します。
まずversionとcommandを確認する
2026 年 8 月 27 日に、手元の Hermes Agent で次を実行しました。
hermes --version
hermes approvals --help
version は次のとおりです。
Hermes Agent v0.20.5 (2026.8.19) · upstream 261a4efb
approvalsには、履歴から候補を作るsuggestと、command を実行せず判定するtestがありました。
positional arguments:
<subcommand>
suggest Propose command_allowlist entries from past approvals
test Dry-run the approval verdict for a command (never executes it)
approvals testは対象 command を動かしません。approval prompt も出さず、設定や履歴も更新しません。終了コードは、許可が 0、承認が必要なら 2、拒否なら 3 です。
専用homeで設定を試す
普段の~/.hermes/config.yamlを変えずに試すため、mktempで一時的な Hermes home を作りました。
tmp=$(mktemp -d)
export HERMES_HOME="$tmp"
hermes config set approvals.deny \
'["git push --force*", "*curl*|*sh*"]'
hermes config get approvals.deny
実行結果です。一時パスは実行時に作られた値です。
✓ Set approvals.deny = ['git push --force*', '*curl*|*sh*'] in /tmp/tmp.BVPX4v1f7z/config.yaml
- git push --force*
- '*curl*|*sh*'
1 件目は force push を止めます。2 件目はcurlの出力を shell へ渡す形を止めます。pattern は case-insensitive で command 全体に照合されます。
pattern は必ず quote します。YAML では行頭の*が alias として扱われるため、裸の*curl*|*sh*は設定 file を壊します。hermes config setを使えば、手作業の indent ミスも避けられます。
実行せずに4通りを試す
force pushは終了コード3になる
--より後ろが検査対象です。command 自体は実行されません。
hermes approvals test -- git push --force origin main
printf 'exit=%s\n' "$?"
command : git push --force origin main
env-type: local
verdict : user-deny (exit 3)
rule : git push --force*
detail : matches a user-defined approvals.deny rule in config.yaml (blocked even under --yolo / mode=off)
normalized trace (variants the detectors evaluated):
- git push --force origin main
exit=3
rule 欄に設定した pattern が出ています。曖昧な「危険そうです」ではなく、どの設定に一致したかを確認できます。
通常のcontent branchへのpushは残す
禁止範囲を広げすぎていないかも確認します。
hermes approvals test -- git push origin content/test
printf 'exit=%s\n' "$?"
command : git push origin content/test
env-type: local
verdict : allow (exit 0)
detail : no guard matched; would run without a prompt
normalized trace (variants the detectors evaluated):
- git push origin content/test
exit=0
通常の push は終了コード 0 です。git push *を deny へ入れるより、壊したくない操作だけを狭く指定した方が日常作業を止めません。
YOLOでもforce pushは拒否する
次はセッションだけ YOLO を有効にして、同じ検査を繰り返します。
HERMES_YOLO_MODE=1 \
hermes approvals test -- git push --force origin main
printf 'exit=%s\n' "$?"
verdict : user-deny (exit 3)
rule : git push --force*
detail : matches a user-defined approvals.deny rule in config.yaml (blocked even under --yolo / mode=off)
exit=3
YOLO は approval prompt を省きますが、user deny rule を消しません。自動化で YOLO が必要でも、force push だけは許さない運用を作れます。
Docker backendではguardを省く
最後に backend だけ変えます。これも Docker を起動せず、判定だけを行います。
hermes approvals test \
--env-type docker \
-- git push --force origin main
printf 'exit=%s\n' "$?"
command : git push --force origin main
env-type: docker
verdict : allow (exit 0)
detail : env_type 'docker' is an isolated container backend; the runtime skips all command guards for it
exit=0
この 0 は、host の Git repository へ force push してよいという意味ではありません。isolated container では、host 向けの command guard を適用しないという判定です。credential や repository を container へ mount すれば、container から外部へ影響できます。mount と credential の範囲は別に確認します。
deny ruleはsandboxではない
approvals.denyが守るのは、Hermes Agent の terminal 実行経路です。悪意ある process を閉じ込める OS sandbox ではありません。
例えば、許可した Python program が内部で別 process を起動する場合、外側の command 文字列だけでは全動作を表せません。MCP、browser、Computer Use も terminal とは別の経路です。force push を本当に不可能にしたいなら、GitHub 側の branch protection、権限を絞った credential、隔離環境を重ねます。
私なら次のように役割を分けます。
- prompt に運用方針を書きます
approvals.denyで agent 経由の禁止 command を止めます- GitHub の権限で force push 自体を拒否します
- untrusted code は isolated backend へ移します
- 外部操作の後は remote state を読み返します
1 つの guard へ安全性を預けません。deny rule は、日常の自動化を残したまま、越えてほしくない操作を明文化する層です。
よくあるエラー
| 症状 | 原因 | 対応 |
|---|---|---|
unrecognized arguments: --forceと出ます | 検査対象のflagをHermes側が読んでいます | commandの前に--を置きます |
| YAML parse errorになります | *から始まるpatternをquoteしていません | hermes config setでJSON配列として設定します |
| 通常のpushまで拒否されます | patternがgit push *のように広すぎます | git push --force*まで狭め、approvals testで両方試します |
| YOLOなのに終了コード3です | deny ruleはYOLOより先に判定されます | 意図した拒否なら正常です。解除は設定変更として扱います |
| Docker指定では終了コード0です | isolated backendはhost向けguardを省きます | mount、credential、networkをcontainer側で制限します |
| patternを変えたのに古い判定です | 別profileや別のHERMES_HOMEを見ています | hermes config pathで対象fileを確認します |
| どのruleに一致したか分かりません | 実commandを直接試しています | hermes approvals test -- <command>でruleとtraceを読みます |
| quoteで分割すれば通ると思いました | 判定前に正規化候補を作ります | normalized traceを確認し、回避ではなくruleを直します |
拒否線を先に決める
今回の実行では、force push は通常時も YOLO 時も終了コード 3 でした。通常の content branch への push は 0 です。禁止範囲を絞りながら、検査結果を機械で読めます。
自律化では「何をできるか」から設定しがちです。私は先に「何だけはさせないか」を決めます。その線をapprovals.denyへ置き、approvals testで実行前に確かめます。許可を増やす作業と、拒否線を消す作業を同じ変更にしないことも大切です。
出典
| 一次情報 | 確認した内容 |
|---|---|
| Hermes Agent公式ドキュメント: Security | approval mode、YOLO、hardline blocklist、approvals.deny、containerでのguard省略 |
| Hermes Agent: tools/approval.py | deny ruleの照合順、case-insensitive照合、command正規化、拒否message |
| Hermes Agent: hermes_cli/approvals_test.py | approvals testのread-only性、判定順、終了コード0・2・3 |
| Hermes Agent: test_approval_deny_rules.py | YOLO、allowlist、containerとの優先順位を確認するtest |
| Python公式ドキュメント: fnmatch | *、?、文字classを使うshell風patternの仕様 |
