Code review: Claude Opus 5.5 (read-only)¶
(Verbatim final report, 2026-09-29, against commit 66f3245. Findings reproduced in a scratchpad export of HEAD.)
Verdict: fix first¶
I reviewed HEAD 66f3245. While I was reviewing, someone was editing the working tree: uncommitted changes appeared in changeset.py, cli.py, retain_patch.py, copilot.py, fates.py, plan.py, aws.py, model.py, terraform.py, plus a new tf_oob.py. Several of those edits look aimed at findings 2, 3, 5, 10, 11 and 13. I did not review the uncommitted edits. Everything below is against HEAD.
The retain patcher itself (priority 1) looks sound. verify_patch compares the whole document after removing only the permitted fields, so every adversarial input I tried was either patched correctly or refused (comments, CRLF, anchors/aliases, merge keys, block scalars containing #, Fn::If policies with blank lines, flow values spanning lines, duplicate keys, multi-document files, tabs, BOM, quoted keys, ? explicit keys and multi-line plain values). There is one type-equality hole, finding 10. The gate and orchestration layers around the patcher are where the real problems are.
Findings¶
- [P0] The runbook's re-inventory commands drop
--keep-on-copilot, so after step 2 the kept workloads, their env and the app are all handed off —emit/runbook.py:147and:266. Nothing carries the keep-set over;--patch-bucketand--i-understand-teardown-is-unverifiedare dropped the same way. Reproducer:app(worker_migrates=False)→ regenerating from a fresh inventory gives a full hand-off while the operator keeps runningcopilot svc deployfor the worker. Fix: persist the keep-set and env scope and re-emit them exactly. - [P0] Environments outside the inventory are invisible, so the app stack and StackSet instance get handed off, imported and torn down while other envs still use them —
sources/copilot.py:78-89,mappers/fates.py:280,emit/runbook.py:350-355.meta["envs"]is collected but never used; StackSet instances elsewhere go intoinv.unavailable, which nothing reads; step 5 runsdelete-stack-instancesacross every account and region. Fix: treat every env in SSM that isn't inventoried, and every StackSet instance outside this account/region, as a kept consumer of the app layer; scopedelete-stack-instancesto this account and region. - [P1]
check --changesetpasses with only the root change set, and never checks the newTemplateURL—check/changeset.py:92-120,cli.py:151-160. Reproduced: a root change set with anAddonsStackModify carryingChangeSetIdand AfterValuehttps://evil/x, no nested change set → pass. Also{"Status":"CREATE_IN_PROGRESS","Changes":[]}→ pass,{"Status":"CREATE_PENDING"}→ pass. Fix: require every referencedChangeSetId,CREATE_COMPLETE+ExecutionStatus: AVAILABLE, AfterValue equal to the manifest URL, and treat 0 accepted as not a pass. - [P1] Runbook bash blocks don't stop on failure —
emit/runbook.py(all blocks;|| trueat :230). A failed gate doesn't stopexecute-change-setorterraform apply; worst case, step 5.4 deletes the StackSet instance (ECR, KMS, artifact bucket) right after a failedverify-retain. Fix:set -euo pipefailper block and explicit patch commands in 5.4. - [P1]
check --phase steadyignores the expected imports, so it passes vacuously —check/plan.py. A state document, a-targetplan and{"errored": true}all pass. Fix: requireresource_changes, rejecterrored, require every expected address as a no-op. - [P1] Every
AWS::Lambda::FunctionandAWS::Lambda::Permissionis classed manual-cleanup, and step 6 tells the operator to delete them —mappers/tf_compute.py:99-106,emit/runbook.py:382-384. User Lambdas in addons would be deleted. Fix: manual-cleanup only for theServiceTokenof aCustom::*in the same stack; otherwise blocked. - [P1] Templates with a
Transformaren't handled — inventory, patching andverify-retainall useTemplateStage="Original";Fn::ForEachand merge keys raise an uncaughtKeyError;Fn::Transform: AWS::Includeat theResourceslevel getsDeletionPolicyinjected. Fix: block any stack with aTransformin v0.1, or verify against theProcessedstage. - [P1] StackSet teardown order and waits contradict §2.5 and §13.1 — the app stack is deleted before
delete-stack-instances; no wait on the asynchronous operation; step 3a's wait is a single query and per-instanceSUCCEEDEDis never checked. - [P2]
verify-retaincan pass vacuously and skips the child SHA check —cli.py:201-239. - [P2]
verify_patchcompares with Python==, sotrue,1and1.0count as equal —emit/retain_patch.py:208. Fix: canonical JSON with types kept distinct. - [P2] The read-only guard has bypasses — paginator path skips the decryption guard;
ssm.get_parameter_history(WithDecryption=True)unguarded;s3.get_objectandlambda.get_functionallowed. - [P2] Secret-bearing files are exposed —
retain-patches/*.ymlwritten 0644 and not gitignored;.gitignoremisses*.plan,cs-*.json,stackset-current.yml,regen/; PLAN §2.2 vs.tfas deliverable needs a decision. - [P2] An unknown
--keep-on-copilotname is silently ignored —sources/copilot.py:133-136. - [P2] Stacks in some statuses are invisible instead of kept; nested stacks two levels deep can be processed before their parent.
- [P2] Step 2 (Protect) gaps — standalone
DBInstancegets no snapshot; no snapshot wait; Aurora members get instance-level deletion protection (UNSURE). - [P3] Patcher refusals and crashes (all fail-closed) — NEL/LS/PS characters, lone-CR endings, JSON re-serialisation (
1e400,1.10),build_patchespatching kept stacks. - [P3] Change-set file handling —
NextTokennot rejected; thecs-{stack}*.jsonglob matches prefix-sharing stacks; omitting--stackmerges nested IDs. - [P3] Smaller gate and emitter issues — steady rejects
forget(theremovedhand-off); HCL map keys not escaped for${; step 4b has no ecsodus gate.
Terraform mappers (priority 6): an argument that forces replacement shows up as a replace, which the import gate rejects (or prevent_destroy errors the plan). The only ways a mismatch is hidden are IMPORT_UNREAD ignore_changes and the service's task_definition/desired_count. Mappers (~3,000 lines) were skimmed, not fully audited.