ガードが素通りする書き方

更新: 2026-09-24
AIエージェントシェルgitlaunchd検査

検査は、通ったときの見た目が「何も起きなかった」になる。だから壊れていても気づけない。エージェントの手前に置いたガードで、これが1日に3回起きた。

ここには、その3つの再現と直し方、ガードそのものを壊して確かめる手順を置く。経緯は開発日記のAIに鍵を渡さず、合図1枚でmainへにある。

1 grepへパイプして、終了コードを失う

監査コマンドの出力をgrepへ渡し、||で「指摘なし」と表示する書き方だ。監査コマンドが失敗した、という想定で再現する。

audit() { echo "audit: 内部エラーで異常終了" >&2; return 2; }

# [A] 素通りする書き方
audit 2>&1 | grep -i critical || echo "指摘なし"

# [B] pipefailを付けても同じ
( set -o pipefail; audit 2>&1 | grep -i critical || echo "指摘なし" )

# [C] 出力と終了コードを分ける
out=$(audit 2>&1); rc=$?
if [ "$rc" -ne 0 ]; then
  echo "監査の実行に失敗(終了コード $rc)。ここで止める"
elif printf '%s\n' "$out" | grep -qi critical; then
  echo "critical あり。止める"
else
  echo "指摘なし"
fi

それぞれが表示したものは次のとおり(bash 3.2.57、macOS標準)。

[A]  -> 指摘なし
[B]  -> 指摘なし
[C]  -> 監査の実行に失敗(終了コード 2)。ここで止める
素通りする書き方監査コマンド終了コード2(失敗)grep終了コード1(一致なし)|| echo「指摘なし」を出力表示: 指摘なし止まる書き方監査コマンド終了コード2(失敗)出力と終了コードを分けて受ける終了コードを先に判定する表示: 監査の実行に失敗
「||」の左が見ているのはgrepの終了コード。監査コマンドが落ちたのか、一致が無かっただけなのかを区別できない。

分かること。

  • ||が判定しているのは、grepの終了コードだ。 1は「一致なし」の意味で、監査コマンドの成否は見ていない
  • set -o pipefailでも直らない。 パイプラインの終了コードは、最後に失敗したコマンドのものになる。ここでは右端のgrepの1が残るので、「一致なし」と「監査が落ちた」を区別できない
  • 直し方は、出力と終了コードを分けて受けて、終了コードを先に見る。 検査結果を変数に取り、失敗なら止める
  • 検査は、反映より前に置く。 この日の実例では、監査より先にGatewayを再起動していた。反映してから検査すると、検査が止めても反映は済んでいる

同じ系統の書き方(|| trueで失敗を捨てる)は、全スクリプトを探すと合計3か所あった。うち1つ(sandbox explain)を「失敗したら止める」形に直した直後に、その場で実在の問題を捕まえた。複数エージェント構成ではagentIdの指定が必須で、以前の|| trueのままなら気づかずに通していた。

2 git statusは、新しいディレクトリを畳む

秘密情報らしいファイル名を検査するのに、git status --porcelainの出力を使っていた。一時リポジトリで再現する。

git init -q . && git commit --allow-empty -qm init
mkdir newdir
echo 'S=1' > .env; echo 'S=1' > newdir/.env; echo k > newdir/id_rsa; echo x > top.pem

git status --porcelain
git status --porcelain -uall

出力はこうだった。

?? .env
?? newdir/
?? top.pem

?? .env
?? newdir/.env
?? newdir/id_rsa
?? top.pem

既定では、未追跡のディレクトリがnewdir/の1行に畳まれる。旧い検査は、この出力の最後の列をgrepしていた。

git status --porcelain | awk '{print $NF}' | grep -iE '(^|/)\.env($|\.)|\.pem$|(^|/)id_(rsa|ed25519)' || true
.env
top.pem

.envとtop.pemは検出できて、newdir/.envとnewdir/id_rsaは検出できなかった。直後のgit add -Aは中身をすべてステージするので、そのままcommitまで進み得る。

git status --porcelain(旧)git diff --cached --name-only(新)?? .env?? newdir/?? top.pem.envnewdir/.envnewdir/id_rsatop.pem検査に掛かる検査に掛かる検査に掛かる検査に掛かる検査に掛かる検査に掛かる中が見えない新規ディレクトリは1行に畳まれる先にgit add -Aでステージしてから調べる
検査は、表示用の出力ではなく、commitされるファイル名の一覧そのものに掛ける。

直し方は、先にステージして、commitされる一覧を調べる形にする。

git add -A
git diff --cached --name-only -z | tr '\0' '\n' | grep -iE '(^|/)\.env($|\.)|\.pem$|(^|/)id_(rsa|ed25519)'

もう1つ、同じ検査に|| trueが付いていて、gitそのものが失敗しても「該当なし」に見えた。 gitリポジトリではない場所で再現する。

set -euo pipefail
SUSPECT=$(git -C ./notrepo status --porcelain 2>/dev/null | awk '{print $NF}' | grep -iE '\.env$' || true)
[ -z "$SUSPECT" ] && echo "該当なし"
該当なし

このスクリプトでは直後のgit add -Aが落ちるので、実害にはならなかった。ただ、表示だけを見ると「検査済み」に見える。gitの出力をいったん変数に取り、|| dieで止めてから、grepにかける。

3 止め方が、動くものに届いていない

自動マージを止める環境変数(AUTO_MERGE=0)を、導入スクリプトの実行時に付けて指定していた。しかし、launchdのジョブは導入時のシェルの環境を引き継がない。plistのEnvironmentVariablesに書いた変数だけが届く。

導入スクリプトを、HOMEを一時ディレクトリ、launchctlを何もしないスタブに差し替えて実行し、生成されたplistの環境変数を読んだ。

導入コマンドplistの環境変数
旧: AUTO_MERGE=0を付けて導入PATH HOME のみ
新: AUTO_MERGE=0を付けて導入PATH HOME WS AUTO_MERGE=0
新: 指定なしAUTO_MERGE=1

旧版では、止めたつもりで、ジョブは常に既定値(マージまで進む)で動いていた。これは検査ではなく止める手段の話だが、書いたことと、動くものに届いていることは別だという点で同じ型だ。

なお、確かめたのは生成されるplistまでで、実際のlaunchdに登録して、合図を置いてPR作成で止まるところまでは実機で通していない。

ガードを壊して確かめる手順

3つに共通するのは、動いていることしか見ておらず、止めるべきものを止めることを確かめていなかった点だ。確かめるときの手順を置く。

  1. ガードごとに、通してはいけないものを実物で用意する。秘密情報の検査なら、.envと鍵のファイル
  2. それを素直な置き場所と、外れた置き場所の両方に置く。外れた置き場所は、新規ディレクトリの中、深い階層、日本語や空白を含むパス
  3. ガードの内側のコマンドを、わざと失敗させる。存在しないディレクトリ、壊れたリポジトリ。「該当なし」「指摘なし」と出たら素通り
  4. 検査のブロックは、実ファイルから切り出して実行する。書き写したものを試すと、元のコードとは別物を試すことになる
  5. 止める手段は、生成された設定を読むだけでなく、実際に止まるところまで通す

手順4の切り出しは、次のようにした。

F=scripts/host/push-from-host.sh
START=$(grep -n '^# --- 秘密情報の混入検査' $F | cut -d: -f1)
END=$(grep -n '^if \[ "\$ASSUME_YES"' $F | cut -d: -f1)
sed -n "${START},$((END-1))p" $F > block.sh

cat > harness.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
die() { echo "エラー: $*" >&2; exit 1; }
say() { printf '\n== %s\n' "$*"; }
WT="$1"
source "$(dirname "$0")/block.sh"
echo "PASS-THROUGH: 検査を通過した"
EOF

bash harness.sh ./t1    # 一時リポジトリ t1 に対して実行する

この日は、次の7ケースで確かめた。

ケース期待
新規ディレクトリ内の.env中止し、ステージを戻す
リポジトリ直下の.pem中止
深い階層のid_ed25519中止
日本語・空白を含むパスの.env中止
無害な新規ディレクトリ通過
日本語・空白を含む無害なパス通過
gitリポジトリではない場所中止(終了コード128)

すべて期待どおりだった。

コードレビューで見る所

  • || trueや|| echoの左側に、検査コマンドそのものが置かれていないか
  • 判定が「出力が空か」だけで、コマンド自体の成否を見ていないか
  • 検査より前に、反映(再起動・commit・push)が済んでいないか
  • 検査が見ているのは、実際に外へ出る一覧か。表示用の出力ではないか
  • 設定を書いただけでなく、それが動くものに届いているか。launchdのジョブは、導入時の環境を引き継がない

関連

WIKI一覧へ