Claude CodeのAuto既定化に備える差分ガードをPythonで作る
「Anthropic is turning Claude Code’s auto mode on by default」という報道が出た。承認ダイアログを押す回数が減るのは助かる。ただ、変更の危なさまで小さくなるわけじゃない。 TechCrunchの報道 を読んで、まずここを分けておくべきだと感じた。
これが意味するのは、承認の有無ではなく、できあがった差分をどの条件で人に返すかを決める段階に入った、ということだ。自分は自動実行を止めるより、.github/workflows/、infra/、migration、実行権限の変更だけを機械的に弾く方が実務では回ると思っている。通常のアプリ修正まで毎回止める運用は、数日で形だけになる。
今朝、空のGitリポジトリで下のスクリプトを動かした。通常の2ファイル変更は通し、ワークフロー追加、sk- 形式のキー、実行ビット変更、変更ファイル数の超過は止まることを確認している。
git diff だけだと未追跡ファイルを見落とす
差分監査でよくある抜けが、新規作成した秘密情報ファイルだ。git diff --name-only HEAD は追跡済みファイルしか返さない。エージェントが config/local.env を新しく作った場合、そのままでは検査対象に入らない。
そこで git ls-files --others --exclude-standard を足す。追跡済みの変更と未追跡ファイルを合わせてから、パスと内容を見る。ここがこのスクリプトの肝だ。
#!/usr/bin/env python3
"""Block an agent change set that needs a human review."""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
PROTECTED = (
".github/workflows/",
"infra/",
"migrations/",
"Dockerfile",
"docker-compose",
)
SECRETS = (
("AWS access key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
("OpenAI-style key", re.compile(r"\b(?:sk|rk|pk)-[A-Za-z0-9_-]{20,}\b")),
("private key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")),
)
def git(*args: str) -> str:
return subprocess.check_output(
["git", *args], text=True, stderr=subprocess.STDOUT
).strip()
def changed_files(base: str) -> list[str]:
tracked = git("diff", "--name-only", "--diff-filter=ACMRTUXB", base).splitlines()
untracked = git("ls-files", "--others", "--exclude-standard").splitlines()
return sorted(set(filter(None, tracked + untracked)))
def scan_secrets(paths: list[str]) -> list[str]:
hits: list[str] = []
for name in paths:
path = Path(name)
if not path.is_file():
continue
raw = path.read_bytes()
if b"\0" in raw:
continue
text = raw.decode("utf-8", errors="replace")
for label, pattern in SECRETS:
if pattern.search(text):
hits.append(f"{label}: {name}")
return hits
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base", default="HEAD")
parser.add_argument("--max-files", type=int, default=12)
args = parser.parse_args()
files = changed_files(args.base)
reasons: list[str] = []
if len(files) > args.max_files:
reasons.append(f"changed files: {len(files)} > {args.max_files}")
for name in files:
if any(name == prefix or name.startswith(prefix) for prefix in PROTECTED):
reasons.append(f"protected path: {name}")
summary = git("diff", "--summary", args.base)
if "mode change" in summary:
reasons.append("executable-bit change")
reasons.extend(scan_secrets(files))
if reasons:
print("BLOCK: human review required", file=sys.stderr)
for reason in reasons:
print(f"- {reason}", file=sys.stderr)
return 2
print(f"OK: {len(files)} changed file(s) passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())
diff_guard.py としてリポジトリに置き、エージェントの作業完了後に実行する。
python3 tools/diff_guard.py --base HEAD --max-files 12
git diff --check
pytest -q
テスト用のリポジトリでは、まず2ファイルの通常変更を通した。
OK: 2 changed file(s) passed
次に未追跡のワークフローとキーらしい文字列を置くと、終了コードは 2 になった。
BLOCK: human review required
- protected path: .github/workflows/deploy.yml
- OpenAI-style key: keys.txt
実行ビットの変更と、--max-files 2 に対する3ファイル変更も同じくブロックできた。実行権限は内容のdiffだけ眺めていると見逃しやすい。git diff --summary を別に取る理由はこれだ。
この順番にすると、速さを残したまま、人が見る対象を差分に絞れる。
止める対象は少ない方が続く
保護パスはプロジェクトごとに変える。Terraformを使わないなら infra/ は消していいし、決済サービスなら payments/ を足す。反対に、src/ を丸ごと保護するのはおすすめしない。普段の機能修正まで人手レビュー待ちにすると、Autoモードを入れた意味が薄くなる。
秘密情報の正規表現も万能ではない。ここでは既知の形式だけを早く止めている。すでにgitleaksやGitHubのsecret scanningを使っているなら置き換えず、最後のローカルガードとして重ねるのがいい。誤検知で止まっても、このスクリプトは変更を消さない。理由を出して終了するだけなので、作業中の差分を落とさず確認へ戻れる。
おわりに
Autoモードで問題になるのは、エージェントが何回コマンドを実行したかではない。どの差分が本番に近い場所へ触れたかだ。保護パス、未追跡ファイル、実行ビット、秘密情報の4つを先に決めておけば、普段の修正は速く流せる。承認画面を増やす前に、差分の出口を1本作っておくと運用がかなり楽になる。