codex-security-diff-scan — 변경분 스캔
codex-security 플러그인의 security-diff-scan 스킬을 Claude가 직접 수행한다. Phase 1의
전체 스캔 스킬(codex-security-scan/SKILL.md)의 공통 규칙(bootstrap, R6 금지 필드, 정산,
리페어 루프)과 Phase 2의 워크벤치 수명주기를 그대로 참조하며, 차이점(대상 해석·diff 인벤토리·
범위 규칙)만 아래에 정의한다.
대상 해석 (R8)
| 인자 | target.kind | base / head |
|---|---|---|
--diff BASE [--head HEAD] |
refs |
base=BASE, head=HEAD(기본 현재 HEAD) |
--working-tree [--base REF] |
working_tree |
base=REF(기본 HEAD), head=워킹트리 |
| (인자 없음) | working_tree |
base=HEAD |
0단계 — 부트스트랩·등록·contract (Phase 1·2 준수)
bootstrap.py --target-repo <저장소 루트>로 pluginRoot·python·scanDir을 얻는다(Phase 1 0단계).- 워크벤치 등록(Phase 2):
workbench_glue.py register의 recipe에mode는 diff에 맞게,target.kind를refs/working_tree로, base/head를 문자열로 채운다(R10). 이어서contract로 좌표(targetId/revision/coverage.mode 기대값)를 확정하고feedback를 주입한다. 순서·finalize-first·complete 실패 3선택지 분기는 Phase 2 SKILL.md와 동일하다. - 시작 고지 강화(R11): working-tree 스캔은 등록 시점의 워킹트리 다이제스트가 기준이므로 스캔 중 파일 저장 한 번으로 complete-scan이 실패한다. "스캔이 끝날 때까지 저장소를 건드리지 마세요(짧은 diff일수록 빨리 끝납니다)"를 refs 스캔보다 강하게 안내한다.
5단계 선형 워크플로 (R9 — 각 단계 완료 전 다음 플러그인 스킬 읽기 금지)
<plugin_dir>/skills/security-diff-scan/SKILL.md를 읽고 순서를 따른다.
위협 모델 — 저장소 전체 범위:
skills/threat-model/SKILL.md절차. diff 범위가 아니라 저장소 수준 위협 모델을 만든다(무관한 diff에도 유효하도록).Discovery — diff 범위: 인벤토리를 diff-rank-input으로 생성한다.
# refs (커밋/브랜치) <python> <plugin_dir>/scripts/generate_rank_input.py make-diff-rank-input --repo <repo_root> --base <base> --mode revisions --head <head> --out <scan_dir>/artifacts/02_discovery/rank_input.jsonl # working-tree (로컬 패치) <python> <plugin_dir>/scripts/generate_rank_input.py make-diff-rank-input --repo <repo_root> --base <base> --mode local-patch --out <scan_dir>/artifacts/02_discovery/rank_input.jsonl이어서 리뷰 입력을 파생한다(필수 호출):
<python> <plugin_dir>/scripts/generate_rank_input.py copy-deep-review-input --rank-input <scan_dir>/artifacts/02_discovery/rank_input.jsonl --out <scan_dir>/artifacts/02_discovery/deep_review_input.jsonldiff 범위 인벤토리(
in_scope_files.txt)도 플러그인 스크립트로 만든다. Phase 1의rg --files전체 목록을 쓰면 안 된다(범위가 저장소 전체가 된다):# refs env -i <trusted_child_env> <python> <plugin_dir>/scripts/generate_in_scope_files.py --repo <repo_root> --scope . --diff-base <base> --diff-head <head> --diff-mode revisions --out <scan_dir>/artifacts/02_discovery/in_scope_files.txt # working-tree env -i <trusted_child_env> <python> <plugin_dir>/scripts/generate_in_scope_files.py --repo <repo_root> --scope . --diff-base <base> --diff-mode local-patch --out <scan_dir>/artifacts/02_discovery/in_scope_files.txt셸에 보간하는 모든 값은 인용본을 쓴다 —
<trusted_child_env>=trustedChildEnvShell, 나머지 경로는shellQuoted.*(repoRoot·scanDir·pluginRoot·pythonPath·trustedPathEnv). 이중인용은$()를 막지 못하고 이 값들의 출처는 미신뢰 환경이다(실측으로mkdir -p "<scan_dir>/…"와PATH="<pathEnv>"둘 다 명령 치환이 실행됐다). 그리고 인용본을 다시"…"로 감싸지 말 것 — 그러면 단일인용이 리터럴이 되고$()는 그대로 확장돼 인용이 무효가 된다(실측). 위 명령형처럼 자리표시자는 맨몸으로 쓴다. 정본 근거는 Phase 1 SKILL.md 2단계.RIPGREP_CONFIG_PATH=는 diff 경로에서는 실효가 없다 — diff 모드는git만 쓰고rg를 타지 않는다(실측: 오염된 설정에서도, PATH에rg가 아예 없어도 결과가 같다). 전체 스캔 인벤토리 호출과 형태를 맞춰 두는 방어 조치이며, 근거는 Phase 1 SKILL.md 2단계가 정본이다.<trusted_path_env>= bootstrap이 반환한trustedPathEnv(대상 저장소 내부 항목을 제거한 PATH). PATH를 이 값으로 교체한다 —$PATH를 이어 붙이지 않는다. diff 모드는 ripgrep을 쓰지 않으므로--require-search는 붙이지 않지만,committed_changed_paths가git을 PATH에서 해석해cwd=대상 저장소로 실행하므로 정화된 PATH가 필요하다. 원시$PATH를 물려주면 대상 저장소가 커밋한./node_modules/.bin/git이 실행된다(실측). 전체 스캔과 같은 이유이며 정본 서술은 Phase 1 SKILL.md 2단계에 있다(P5-KTD2). 변경된 source-like 파일만 리뷰한다. 후보는normalize_candidates.py로 단일 원장에 병합하되 (Phase 1과 동일), diff 스캔은--allow-missing-in-scope를 반드시 붙인다:<python> <plugin_dir>/scripts/normalize_candidates.py --input <raw1.jsonl> [...] --out <scan_dir>/artifacts/02_discovery/candidate_ledger.jsonl --repo-root <repo_root> --in-scope-files <scan_dir>/artifacts/02_discovery/in_scope_files.txt --allow-missing-in-scopediff 인벤토리에는 삭제된 경로가 포함되며(예:
git rm된 파일), 이 플래그가 없으면in-scope file row N: No such file or directory로 정규화가 실패한다(실측). 플래그를 붙여도 후보 location 파일의 실존 검사는 그대로 수행된다.삭제된 파일에 대한 finding 처리: head 시점에 존재하지 않는 경로만
locations에 갖는 finding은coverage_reconcile.py가 거부한다("소스 루트 하위에 존재하지 않습니다" — 플러그인 finalizer 자체는 통과시키지만 우리 정산 게이트가 더 엄격하다, 실측). 그런 후보는 finding 대신coverage.deferred(또는not_applicable서피스)로 기록하고 삭제 사실을 사유에 적는다. review_log에는 base 시점에서 읽었음을 남긴다.Validation (compact) — Phase 1과 동일.
Attack Path (compact) — Phase 1과 동일.
Canonical JSON — Phase 1의 순서 있는 결과 매핑 적용.
coverage.mode는 kind별 기대값 (refs →branch_diff, working_tree →working_tree)을 따르되, 실제 기대값은 Phase 2 contract 조회 결과가 권위다. finalizer가 덮어쓰는 R6 금지 필드는 작성하지 않는다.
완료 (Phase 1·2 준수)
bind-repo-scopes(필요 시) → coverage_reconcile.py(정산 R9 + 경로 검사 R10) → finalize_scan_contract.py
(리페어 루프 R12) → workbench_glue.py complete(게이트 실패 3선택지 분기). Phase 1·2 SKILL.md의
해당 절차를 그대로 따른다.
- 정산은
--json과 함께 호출한다 — 최종 보고가coverage.completenessAfter·deferredCount·partialWithoutDeferred키를 인용한다.--json이면 JSON은 stdout, 사람용 요약은 stderr로 나간다. - 빈 인벤토리: diff 범위에 리뷰 대상 파일이 없는 것(문서만 바뀐 커밋 등)은 diff 스캔에서
정직한 상태다. 그 경우에만
--allow-empty-inventory를 붙인다 — 통과시켜도 경고는 남는다. 인벤토리 생성 자체가 실패해서 비었을 가능성이 있으면 붙이지 말고 상류 스크립트의 종료 상태를 먼저 확인한다. - 중단된 스캔의 복구: 계약 오류로 상류가 스캔을 자동
failed종결시킨 경우workbench_glue.py recover --scan-id <id>를 시도할 수 있다. 판정 규칙은 Phase 1 SKILL.md 6단계가 정본이다 — exit 0 +ok:false면 이 명령의 대상이 아니고,failureKind가infra면 전제 문제가 아니라 실행 환경 문제다.
범위 이탈 가시화
정산 단계에서 diff에 없는 파일의 finding이 나오면 경고한다(리뷰는 diff 범위인데 finding이 범위를 벗어나면 조사 대상). coverage_reconcile.py의 경로 검사가 존재/이탈을 잡고, diff 범위 이탈은 요약에 남긴다.
실패 안내
- shallow clone에서 base 해석 실패:
git fetch --unshallow또는 필요한 base를 fetch하라는 한국어 안내로 종료한다. - detached HEAD: working-tree 스캔은 정상 등록된다(HEAD를 base로 사용).
하드 규칙
Phase 1 SKILL.md의 하드 규칙(R6 금지 필드, R7 저장소 불변, R11 미신뢰 데이터, 단일 원장, 파괴적 명령 금지)을 그대로 적용한다.