ANTIGRAVITY LABEN
記事一覧/Agents & Manager
Agents & Manager/2026-09-24中級

無人実行のエージェントが設定ファイルを書き換えたら戻す、小さな番人スクリプトの組み方

読み取り専用にしたはずの枠をエージェント自身が設定ファイルごと広げてしまう、という報告を読んで、夜間の無人実行を外側から包む番人を組みました。実行前後の指紋比較・退避と復元・打ち切り時間の3点を、動くコードでお伝えします。

Antigravity CLI35エージェント72無人実行6権限設計11Python16

Antigravity CLI の Issue 一覧を眺めていた昼下がり、ひとつの報告で手が止まりました。Remote Control で「読み取りのみ」にしておいた枠を、エージェントがローカルの settings.json を書き換えることで越えてしまう、という内容です(#1083・執筆時点で未解決)。

私自身、壁紙アプリの画像の仕分けを夜のあいだに無人で回しております。翌朝に結果を眺めるだけの運用で、途中で誰かが画面を見ているわけではありません。

そこで自分の設定を思い返してみると、仮にエージェントが設定ファイルを一行書き換えても、それに気づく仕組みがどこにもありませんでした。胃のあたりが少し重くなったのを覚えています。

内側の約束だけでは、外れたときに誰も気づきません

最初に考えたのは、エージェント側の設定をもっと厳しくすることでした。許可リストを絞り、フックで止め、プロンプトにも「設定を触らないこと」と書き足す——どれもエージェントの内側に置く約束です。

ところが同じ時期に、--dangerously-skip-permissions を付けると /plan モードの stop hook まで素通りになる、という報告も上がっていました(#1074)。内側の約束は、内側の別の仕組みでまとめて外れることがあるのです。

問題は「破られるかどうか」ではなく、「破られたときに翌朝の私が知らないまま次の夜を迎えること」でした。そう整理し直して、線を一本引いております。

止める仕組みより先に、書き換えに気づく仕組みを外側へ。

エージェントのプロセスの外で動き、実行の前後だけを見る小さな番人です。完璧な防壁ではありませんが、気づけない状態からは抜け出せます。

番人がやることは3つだけです

番人の仕事を、次の3つに絞りました。

段階やること狙い
実行前見張るファイルの sha256 を記録し、実物も控える比較の基準と、戻すための原本を残す
実行中コマンドを打ち切り時間つきで走らせる終わらない夜を作らない
実行後指紋を取り直し、違っていれば退避して原本へ戻す翌朝に差分と証拠が残る

打ち切り時間を入れたのには理由があります。CLI 1.2.6 で headless 実行(-p)の既定タイムアウトが5分から無制限へ変わりました。長い作業には助かる変更ですが、無人の夜に限っては、区切りを自分で持っておく方が安心です。

番人スクリプトの全体

Python の標準ライブラリだけで書いています。何を解決するかといえば、「エージェントが設定を書き換えたかどうかが、実行後に機械的に分かり、元に戻っている」状態を作ることです。

#!/usr/bin/env python3
"""無人実行の前後で設定ファイルの指紋を比べ、書き換えがあれば元に戻して止める番人。"""
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
 
GUARD_HOME = Path(os.environ.get("AGENT_GUARD_HOME", Path.home() / ".agent-guard"))
TAMPER_EXIT = 42  # CLI 側の終了コード(構造化エラーの 3 など)と重ならない値
 
 
def fingerprint(path: Path):
    """ファイルなら中身の sha256、ディレクトリなら配下全体の sha256。無ければ None。"""
    if not path.exists():
        return None
    h = hashlib.sha256()
    if path.is_dir():
        for p in sorted(path.rglob("*")):
            if p.is_file():
                h.update(str(p.relative_to(path)).encode())
                h.update(p.read_bytes())
        return "dir:" + h.hexdigest()
    h.update(path.read_bytes())
    return h.hexdigest()
 
 
def load_watch(list_file: str):
    paths = []
    for line in Path(list_file).read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if line and not line.startswith("#"):
            paths.append(Path(os.path.expanduser(line)).resolve())
    return paths
 
 
def snapshot(paths, run_dir: Path):
    """指紋を記録し、元に戻せるよう実物も控えておく。"""
    state = {}
    for i, p in enumerate(paths):
        state[str(p)] = fingerprint(p)
        dest = run_dir / "before" / str(i)
        if p.is_dir():
            shutil.copytree(p, dest)
        elif p.exists():
            dest.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(p, dest)
    (run_dir / "state.json").write_text(json.dumps(state, indent=2, ensure_ascii=False))
    return state
 
 
def restore(paths, state, run_dir: Path, changed):
    """書き換えられた版は quarantine へ退避し、実行前の版を戻す(削除はしない)。"""
    for i, p in enumerate(paths):
        if str(p) not in changed:
            continue
        if p.exists():
            q = run_dir / "quarantine" / str(i)
            q.parent.mkdir(parents=True, exist_ok=True)
            shutil.move(str(p), str(q))
        src = run_dir / "before" / str(i)
        if state[str(p)] is None:
            continue  # 実行前は存在しなかった → 退避だけで元どおり
        if src.is_dir():
            shutil.copytree(src, p)
        else:
            shutil.copy2(src, p)
 
 
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--watch", required=True, help="見張るパスを1行1つ書いたファイル")
    ap.add_argument("--timeout", type=int, default=3600, help="秒。headless の既定は無制限なので必ず区切る")
    ap.add_argument("cmd", nargs=argparse.REMAINDER, help="-- の後に実行するコマンド")
    a = ap.parse_args()
    cmd = a.cmd[1:] if a.cmd[:1] == ["--"] else a.cmd
    if not cmd:
        ap.error("実行するコマンドがありません")
 
    paths = load_watch(a.watch)
    run_dir = GUARD_HOME / f"{time.strftime('%Y%m%d-%H%M%S')}-{os.getpid()}"
    run_dir.mkdir(parents=True)
    state = snapshot(paths, run_dir)
 
    try:
        rc = subprocess.run(cmd, timeout=a.timeout).returncode
    except subprocess.TimeoutExpired:
        print(f"[guard] {a.timeout} 秒で打ち切りました", file=sys.stderr)
        rc = 124
 
    changed = {p: {"before": b, "after": fingerprint(Path(p))}
               for p, b in state.items() if fingerprint(Path(p)) != b}
    report = {"command": cmd, "exit": rc, "changed": changed}
    (run_dir / "report.json").write_text(json.dumps(report, indent=2, ensure_ascii=False))
 
    if changed:
        restore(paths, state, run_dir, changed)
        for p in changed:
            print(f"[guard] 書き換えを検知し、実行前の版へ戻しました: {p}", file=sys.stderr)
        print(f"[guard] 詳細: {run_dir / 'report.json'}", file=sys.stderr)
        sys.exit(TAMPER_EXIT)
    sys.exit(rc)
 
 
if __name__ == "__main__":
    main()

なぜこう書いたのか、要点を3つだけ添えます。

  • 書き換えられた版を消さず、quarantine へ移しています。 何を書き込もうとしたのかは、翌朝いちばん知りたい証拠だからです。
  • 実行前に無かったファイルも見張れます。 新しく作られたフックの置き場のように「増えた」ことも、指紋が None から変わることで拾えます。
  • 終了コードを3種類に分けています。 書き換えは 42、打ち切りは 124、それ以外はコマンド自身の値をそのまま返します。CLI の構造化エラー(終了コード 3)と混ざらないようにするためです。

同じ秒に二度走らせたら、番人のほうが落ちました

最初の版では、記録用のフォルダ名を「年月日-時分秒」だけにしておりました。手元で試験を続けて流したところ、同じ秒に2回目が始まり、FileExistsError で番人自身が止まっています。

見張る側が先に倒れては、見張りになりません。フォルダ名の末尾にプロセス ID を足して、ぶつからないようにしました。夜間のジョブを分割して並べるようになると、同じ秒の起動は案外すぐに起きるのだと思います。

試験では、中身を書き換えるだけの偽のコマンドを走らせて、次の4通りを確かめました。

試したこと終了コード結果
何も触らないコマンド0そのまま通過
設定ファイルを書き換え、フックの置き場を新設42両方を検知。原本へ戻し、書き換えられた版は quarantine へ
打ち切り時間 1 秒で 3 秒かかるコマンド124打ち切りを記録
終了コード 3 で終わるコマンド3番人の値と混ざらずに返る

夜のジョブに組み込む

見張るパスは、1行1つのテキストに書きます。パスは CLI の版で変わりえますので、お使いの版のドキュメントで、プロジェクト側・ユーザー側の設定、フック、MCP の設定がどこに置かれるかを確かめてから書いてください。

# ~/.agent-guard/watch.txt
# プロジェクト側の設定ディレクトリ(実際のパスに置き換える)
~/work/wallpaper-batch/<プロジェクト設定ディレクトリ>
# ユーザー側の設定ファイル(実際のパスに置き換える)
~/<ユーザー設定の置き場>/settings.json
# 夜のジョブを起動するスクリプト自身も見張る
~/bin/run-nightly.sh

起動スクリプトは、番人の終了コードで分岐させるだけです。

#!/bin/bash
# ~/bin/run-nightly.sh — cron や launchd から呼ぶ入口
export AGENT_GUARD_HOME="$HOME/.agent-guard"
PROMPT="$(cat "$HOME/jobs/wallpaper-tagging.md")"
 
python3 "$HOME/bin/agent_guard.py" \
  --watch "$AGENT_GUARD_HOME/watch.txt" --timeout 2700 -- \
  agy -p "$PROMPT"
rc=$?
 
case $rc in
  0)   ;;
  42)  echo "$(date '+%F %T') 設定の書き換えを検知 → 次回の実行を止める" >> "$AGENT_GUARD_HOME/alerts.log"
       touch "$AGENT_GUARD_HOME/HOLD" ;;
  124) echo "$(date '+%F %T') 打ち切り" >> "$AGENT_GUARD_HOME/alerts.log" ;;
  *)   echo "$(date '+%F %T') exit $rc" >> "$AGENT_GUARD_HOME/alerts.log" ;;
esac

書き換えを検知したら、HOLD という空ファイルを置いています。cron 側で「HOLD があれば起動しない」と一行足しておけば、私が差分を読むまで次の夜は走りません。戻したから大丈夫、ではなく、なぜ書き換えようとしたのかを読むまでは再開しない——ここが番人のいちばん大事な仕事だと考えています。

番人も万能ではありません

正直にお伝えしておきたい限界が2つあります。

ひとつは、番人とエージェントが同じユーザー権限で動いていることです。記録置き場を作業ディレクトリの外に置いても、エージェントがそこまで手を伸ばせば書き換えられます。macOS の chflags uchg で設定ファイルを変更不可にする手もありますが、所有者なら外せる印ですので、これも絶対ではありません。

もうひとつは、実行の「途中」は見ていないことです。書き換えて、使って、元に戻す——という動きは、前後の比較では拾えません。

それでも私は、この番人を外しません。完全に縛る仕組みを目指して何も置かないより、気づける仕組みを一枚置いて翌朝の私に判断を渡すほうが、無人運用の現実に合っていると感じています。エージェント側の権限をどう切り分けるかは、Antigravity Agent に書き込み権限を安全に渡す Permission Boundary の本番設計で、外側の番人と対になる内側の設計として詳しく書き残しています。

まずは今夜のジョブで見張りたいファイルを3つだけ watch.txt に書き、何も触らないコマンドで番人が 0 を返すことを確かめるところから始めていただければと思います。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Agents & Manager2026-07-19
作業フォルダの外に書けてしまった朝 — 無人実行の書き込み先を1関数で検札する
Antigravity CLI 1.1.3 で、always-proceed のときワークスペース外への書き込みが誤って自動承認されていた不具合が塞がれました。無人実行の書き込み先を、正規化と包含判定だけで検札する小さなガードを、すり抜け試験つきでまとめます。
Agents & Manager2026-07-29
agent.md の綴り誤りが権限を広げていた — frontmatter を厳格に検証する lint を書く
agent.md の frontmatter は綴りを間違えてもエラーになりません。既定値へ静かに落ちた結果、意図と逆の権限が適用されていた実例と、権限キーだけを厳しく検証する lint の実装・実測を記録しました。
Agents & Manager2026-07-17
エージェントに渡した参照メモの7割は届いていませんでした — head で切る運用の限界を測った記録
head で行数を切って渡していた参照メモの7割がエージェントに届いていなかった、という実測に基づく記録です。行数ではなくセクション単位の契約で切り取り境界を設計し直し、予算内で何を残すかを決めるまでの過程を、検証データとともにまとめています。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます