ANTIGRAVITY LABEN
記事一覧/Agents & Manager
Agents & Manager/2026-07-19中級

作業フォルダの外に書けてしまった朝 — 無人実行の書き込み先を1関数で検札する

Antigravity CLI 1.1.3 で、always-proceed のときワークスペース外への書き込みが誤って自動承認されていた不具合が塞がれました。無人実行の書き込み先を、正規化と包含判定だけで検札する小さなガードを、すり抜け試験つきでまとめます。

Antigravity CLI16無人実行4権限設計5ファイル書き込み2セキュリティ13

朝、差分を確認していて手が止まりました。夜のあいだに回していたタスクが、リポジトリの中だけを触るはずだったのに、ひとつ上の階層に設定ファイルを書き出していたのです。実害はありませんでした。ただ、書けてしまったという事実の方が、書かれた中身より長く尾を引きました。

Antigravity CLI 1.1.3(7/16)で、always-proceed モードのとき、ワークスペースの外へのファイル書き込みが誤って自動承認されていた不具合が修正されています。私の朝の一件は、まさにこの穴に落ちていたのだと思います。修正が入った今でも、CLI に任せきりにせず、自分の側でも書き込み先を確かめる小さな仕組みを一枚挟んでおきたい。そう考えて作ったガードを、この記事で共有します。

「外に書けてしまう」の正体

書き込み先が意図せず外へ出る原因は、たいてい派手なものではありません。私が踏んだのは次の3つでした。

入口何が起きるか気づきにくい理由
相対パスと cwd のずれ../config/app.json が、想定と違う起点から解決される手元では正しく、無人実行時だけ起点が変わる
.. の混入生成されたパスに .. が紛れ、境界の外へ抜ける文字列としては自然に見える
シンボリックリンクワークスペース内のリンクが、実体は外を指すパスの見た目だけでは判別できない

共通しているのは、書き込む直前の「見かけのパス」を信じてはいけない、という点です。文字列を前方一致で調べるだけでは、.. もシンボリックリンクもすり抜けます。必要なのは、実体としてどこを指すのかまで解決してから、境界の内側にあるかを判定することです。

書き込み先を検札する小さなガード

やることは2つだけです。ひとつ、対象パスを実体まで正規化する。ふたつ、それがワークスペース直下に含まれるかを判定する。Node.js で書くと、こう収まります。

// write-guard.mjs
import { realpathSync } from "node:fs";
import { resolve, dirname, relative, isAbsolute, sep } from "node:path";
 
// ワークスペースの実体パス(起動時に一度だけ解決)
const ROOT = realpathSync(process.env.AGY_WORKSPACE ?? process.cwd());
 
// 実体がまだ無いパスでも、最も近い実在の親まで遡って解決する
function realpathNearest(p) {
  let current = resolve(p);
  // 存在する祖先が見つかるまで上へ
  // (まだ書かれていないファイルは realpath できないため)
  for (;;) {
    try {
      const realParent = realpathSync(dirname(current));
      return resolve(realParent, current.slice(dirname(current).length + 1));
    } catch (e) {
      if (e.code !== "ENOENT") throw e;
      const parent = dirname(current);
      if (parent === current) return current; // ルートまで到達
      current = parent;
    }
  }
}
 
export function assertInsideWorkspace(target) {
  const real = realpathNearest(target);
  const rel = relative(ROOT, real);
  const escapes = rel === "" || rel.startsWith("..") || isAbsolute(rel);
  if (escapes) {
    throw new Error(
      `書き込み先がワークスペースの外です: ${target}\n  解決後: ${real}\n  基準: ${ROOT}`
    );
  }
  return real;
}

肝は realpathNearest です。これから書くファイルはまだ存在しないので、そのままでは実体解決できません。そこで、実在する一番近い親までさかのぼって解決し、そこから相対で組み直します。こうすると、途中にシンボリックリンクがあっても実体側で評価され、..relative の結果が .. で始まるかどうかで弾けます。

書き込みの直前に一行差し込むだけです。

import { writeFileSync } from "node:fs";
import { assertInsideWorkspace } from "./write-guard.mjs";
 
function safeWrite(target, data) {
  const real = assertInsideWorkspace(target); // 外なら例外で止まる
  writeFileSync(real, data);
}

すり抜けを試す

ガードは、通ってほしいものを通すより、通ってほしくないものを止められるかで評価すべきだと考えています。私は次の表を試験に落として、緑になるまで直しました。

入力期待狙い
notes/today.md通す正常系
./out/build/log.txt通す内側の相対パス
../secrets.json止めるひとつ上へ抜ける
/etc/hosts止める絶対パスで外へ
out/../../x止める内側を経由してから抜ける
外を指すシンボリックリンク配下止める実体は境界の外

試験そのものも短く書けます。

// write-guard.test.mjs
import assert from "node:assert";
import { symlinkSync, mkdirSync } from "node:fs";
import { assertInsideWorkspace } from "./write-guard.mjs";
 
const pass = ["notes/today.md", "./out/build/log.txt"];
const block = ["../secrets.json", "/etc/hosts", "out/../../x"];
 
for (const p of pass) assert.doesNotThrow(() => assertInsideWorkspace(p), p);
for (const p of block) assert.throws(() => assertInsideWorkspace(p), p);
 
// シンボリックリンクのすり抜け
mkdirSync("out", { recursive: true });
symlinkSync("/tmp", "out/escape"); // 外を指すリンク
assert.throws(() => assertInsideWorkspace("out/escape/pwn"), "symlink escape");
 
console.log("すべて期待どおりに通過・遮断されました");

out/../../x を止められるかどうかが、文字列一致で済ませたガードとの分かれ目です。前方一致だと「out/ で始まるから内側」と誤判定しますが、正規化を通せば実体は外だと分かります。

CLI 側でも二重化しておく

アプリ側の一枚に加えて、CLI を無人で回すときは allow ルールの名前づけでも輪郭を残しておくと安心です。1.1.3 では、確認の要る操作をソフト拒否したうえで、許可に必要な allow ルール名を stderr に出してくれるようになりました。書き込み系のルールは、対象範囲が読み取れる名前にしておくと、後から棚卸ししやすくなります。

# 無人実行のログから、書き込み許可がどこに効いたかだけを拾う
agy -p "$TASK" 2>&1 | grep -iE "allow.*(write|fs)" | sort -u

出力される allow ルール名を眺めて、想定より広い許可が効いていないかを朝に一度だけ確認する。これだけでも、静かに広がっていく権限に気づきやすくなります。

運用に落とすときの置き場所

このガードは、書き込みを行う関数の内側、実際に writeFileSync を呼ぶ直前に置くのが最も漏れにくいと感じています。パスを組み立てる場所は複数あっても、最後に書く出口はたいてい一箇所に集まるからです。出口で検札すれば、途中でどんなパスの作られ方をしても、外へは抜けません。

止めたときは、握りつぶさずに必ず記録します。私は「解決後の実体パス」「基準にしたルート」「入力そのもの」の3つをログに残すようにしました。どこを起点に、何が、どこへ抜けようとしたのかが一目で分かるので、原因の切り分けが速くなります。

被害範囲そのものをより広く設計したい場合は、エージェントが壊す前提で組む — ブラスト半径を小さく保つ運用設計が参考になります。夜間の無人実行を一段落ち着いて任せるための全体像は、Background Agent に夜間作業を任せて、朝に後悔しないために — 無人実行のガードレール設計にまとめました。CLI の allow ルールを最小権限から積み上げる逆向きの手順は、黙って通していた確認ツールを、ソフト拒否から allow ルールに起こすと合わせて読むと、権限の絞り方が立体的になります。

まとめ

無人実行で怖いのは、失敗より、静かに成功してしまうことだと改めて思いました。書き込み先の検札は、正規化と包含判定というごく小さな二手で足ります。今日の次の一歩としては、自分のタスクの「書く出口」がどこに集まっているかを一度たどってみることをおすすめします。出口さえ押さえれば、このガードは一箇所で効きます。

個人開発でタスクを自動化していると、権限の既定値を読み直すたびに、任せる範囲を少しずつ描き直すことになります。私自身、その繰り返しの中にいます。同じように夜間の自動処理を回している方の、朝の安心につながれば幸いです。お読みいただきありがとうございました。

シェア

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

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

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

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

関連記事

Agents & Manager2026-06-20
無人実行のエージェントをタイムアウトで止めたとき、書きかけのファイルが残る問題
スケジュール実行のエージェントがタイムアウトで殺されると、書きかけのファイルが静かに残ります。原子的な書き込み・固定名temp残骸の排除・書き込み後の内容検証で、無人パイプラインを壊さない設計をまとめました。
Agents & Manager2026-07-18
エージェントに任せるコード検索を契約として書く — /codesearch の正規表現既定を実測で読み直す
Antigravity CLI 1.1.3 の /codesearch はクエリを既定で正規表現として解釈します。979本のMDXを抱えるリポジトリで正規表現とリテラルの差を実測し、過剰ヒット・4.5MBの出力・静かなずれを避けるクエリ設計をまとめました。
Agents & Manager2026-07-17
エージェントに渡した参照メモの7割は届いていませんでした — head で切る運用の限界を測った記録
定期実行のエージェントに参照メモを cat と head で渡していたところ、肝心な行が黙って落ちていました。切り取り位置を実測し、行数ではなくセクション単位の契約に置き換えるまでの記録です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →