fix(plugin): migrate stale global codingbuddy-mode-detect.py on install#1389
Conversation
Cleans up the legacy mandatory-MCP hook left behind by plugin versions prior to the self-contained refactor so upgraded installs no longer keep the old script alive via global settings.json. - Add scripts/migrate-legacy-hooks.js: deletes ~/.claude/hooks/codingbuddy-mode-detect.py, removes the hooks/lib symlink when it is a symlink (never a real directory), filters only UserPromptSubmit entries whose command references the legacy script (preserving unrelated entries), drops the UserPromptSubmit key when it becomes empty, and writes a timestamped settings.json.bak-codingbuddy-migration-<ts> backup before rewriting. - Idempotent, graceful on malformed/missing files, supports CODINGBUDDY_POSTINSTALL_DRY_RUN=1 and opts.dryRun. - Wire the migration into scripts/postinstall.js so every install runs it (including CI); failures never block the install. - 21 new vitest cases in scripts/migrate-legacy-hooks.spec.ts covering stale script removal, symlink vs real-dir safety, nested UserPromptSubmit filtering, backup behavior, dry-run (both opts and env var), idempotency, malformed JSON, missing settings.json, and log visibility. - Add a Backend diagnostic marker to hooks/user-prompt-submit.py so standalone-vs-mcp fallback is obvious to users (#1384 visibility), plus two pytest regression tests. Closes #1381 Closes #1384
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
JeremyDev87
left a comment
There was a problem hiding this comment.
Review: APPROVE
CI Status: PASS
29/29 check 전체 통과 — build, lint, typecheck, test, security, circular, e2e (hooks 3.11/3.12, docker), vercel preview, plugin-validate-commands, rules-validation 포함.
Local Verification: PASS
yarn workspace codingbuddy-claude-plugin lint ✅
yarn workspace codingbuddy-claude-plugin typecheck ✅
yarn workspace codingbuddy-claude-plugin test ✅ 144/144 (21 new migrate cases)
python3 -m pytest hooks/test_user_prompt_submit.py ✅ 39/39 (2 new backend marker cases)
src/utils.ts coverage 100% stmt/line
Real-environment smoke test (bonus)
리뷰어 로컬 머신에는 실제로 #1381이 타겟팅하는 stale 환경이 존재:
~/.claude/hooks/codingbuddy-mode-detect.py(2026-03-30)~/.claude/hooks/lib(monorepo로의 symlink)~/.claude/settings.json에 legacy UserPromptSubmit 엔트리 1개
migrateLegacyHooks({ dryRun: true }) 실행 결과:
{
"removedScript": true,
"removedSymlink": true,
"strippedSettingsEntries": 1,
"removedUserPromptSubmitKey": true,
"backupPath": null,
"dryRun": true,
"warnings": []
}세 아티팩트 모두 정확히 탐지, dry-run 플래그가 원본을 전혀 건드리지 않음. 파일/심링크/설정 모두 사전 상태 그대로 유지 확인.
Severity Findings
- Critical (blocking): 없음
- High (must fix): 없음
- Medium (should fix): 없음
- Low (nice to have): 없음
Spec Compliance
#1381 (Stale hook migration) — 제안된 3단계 + 4가지 safety consideration 모두 구현:
| 요구사항 | 구현 위치 |
|---|---|
| Stale 글로벌 스크립트 삭제 | migrateStaleScript() |
lib/ 삭제 심링크일 때만 |
migrateStaleSymlink() — isSymbolicLink() 가드 + real-dir 보호 테스트 |
settings.json의 legacy UserPromptSubmit 필터 |
stripLegacyFromSettings() + filterGroup() |
| 빈 UserPromptSubmit 키 제거 | removedUserPromptSubmitKey 로직 |
| 타임스탬프 백업 | settings.json.bak-codingbuddy-migration-<ts> |
| legacy 파일명만 매칭 | isLegacyCommandString('codingbuddy-mode-detect.py') |
| 멱등성 | "running twice" 테스트로 검증 |
| Dry-run (env + opt) | 두 경로 모두 테스트 |
| 명확한 로그 라인 | [CodingBuddy Plugin] prefix + 액션별 메시지 |
#1384 (Standalone diagnosability + mandatory-MCP 제거) — 3가지 acceptance criteria 모두 충족:
| 요구사항 | 구현 |
|---|---|
| 업그레이드 설치에서 stale mandatory-MCP 제거 | postinstall.js에서 migrateLegacyHooks 자동 호출 |
| Standalone fallback 진단 용이성 | # Backend: mcp-enhanced / # Backend: standalone (self-contained, no MCP required) / # Backend: standalone-minimal (import failure) 세 개의 구별 마커 |
| 마이그레이션 + 가시성 테스트 | 21 vitest + 2 pytest |
리뷰어 노트 (정보 공유 — 블로커 아님)
안전성 설계 하이라이트:
-
Backup-first 순서:
migrateSettings()는 백업을 먼저 쓰고 성공을 확인한 뒤에만 원본을 덮어씀. 백업 write가 실패하면 rewrite 자체를 abort (return; // do not rewrite settings.json if we could not back it up). 이슈 스펙을 넘어서는 안전장치로, 디스크가 꽉 차거나 권한 이슈가 있을 때settings.json이 손상되는 최악의 시나리오를 방어함. -
Symlink API 정확성:
lstatSync().isSymbolicLink()사용 (statSync가 아닌).stat은 심링크를 따라가므로 심링크 구별에 쓰면 안 되는데, 여기서는lstat로 심링크 자체를 검사. 실수하기 쉬운 포인트를 정확히 잡음. -
Graceful degradation 3중 방어:
~/.claude없음 → early return, 에러 없음settings.json없음 → ENOENT 무시, null 반환- malformed JSON → 원본 유지, warning 추가, migration skip
세 가지 모두 테스트로 커버됨.
-
Nested filtering:
filterGroup이 그룹 전체가 아니라 그룹 내부의 개별 hook entry까지 필터링. 한 UserPromptSubmit 그룹에 legacy와 사용자 hook이 섞여 있어도 사용자 것만 살림. 테스트filters nested hooks arrays where only some sub-entries are legacy로 검증. -
Import-failure 브랜치에도
standalone-minimal마커:user-prompt-submit.py의 Exception fallback 경로에도# Backend: standalone-minimal (import failure)출력.lib/심링크 깨짐 같은 실제 breakage 시나리오를 사후 진단 가능하게 함 —#1384의 가시성 목표를 스펙 이상으로 만족. -
Dry-run semantics: 플래그를 result에 먼저 기록한 뒤 write를 gate. dry-run으로도 "무엇이 바뀔지"를 결과 객체로 확인 가능 (
removedScript: true, backupPath: null). 테스트does not write any files when dryRun is true+ "Result reports what would change even in dry-run"로 명시적 검증. -
postinstall 통합: migration 실패는
npm install을 블록하지 않고 warning만 출력 — deployment-friendly. CI 환경(CI=true)에서도 migration은 실행하되 banner만 스킵. 멱등성 덕분에 CI에서 매번 실행해도 안전.
좋은 점 — 테스트 품질:
- Mocking 없음. 실제 tmp 디렉토리, 실제 심링크, 실제 JSON 파일로 픽스처 구성. 이 덕분에 리뷰어 로컬 머신의 실제 stale 환경에서 smoke test가 가능했음.
- afterEach 기반 리소스 정리: 누락된 cleanup으로 인한 테스트 간 간섭 없음.
- 환경변수 테스트 위생:
CODINGBUDDY_POSTINSTALL_DRY_RUN테스트가 prev 값 보존/복원을finally에서 수행. - 양성/음성 양방향: MCP 브랜치 테스트가 "mcp-enhanced 포함" 뿐 아니라 "standalone wording이 leak되지 않음"까지 assert — 회귀 방지 방향까지 커버.
사소한 관찰 (수정 불필요):
- 백업 파일명이
Date.now()ms 정밀도 — 이론적으로 동시 실행 시 충돌 가능하지만, postinstall 단일 진입점이라 실질적으로 불가능. - 마이그레이션 스크립트 JSDoc에 반환 타입이 명시되지 않음 — IDE hint 개선 여지 있으나 테스트 타입 정의에서
MigrateResult로 이미 잡혀 있어 실무 지장 없음.
어느 것도 블로커/리그레션이 아님.
Recommendation: APPROVE
스펙 초과 달성 (#1381 안전장치 + #1384 가시성 3단계 마커), CI 29/29 전체 통과, 로컬 lint/typecheck/test/pytest 전체 통과, 실환경 dry-run smoke test 통과, 코드 품질 이슈 없음, 안전성 설계 모범적. 머지 가능.
Summary
Bundle fix for #1381 (primary) and #1384 (related hardening). Upgraded installs from plugin versions prior to the self-contained hook refactor kept a stale global
~/.claude/hooks/codingbuddy-mode-detect.pyalive via~/.claude/settings.json, causing everyPLAN:/ACT:prompt to emit the old mandatory-MCP template even though the newer self-contained hook was shipped. This PR migrates those installs and makes the standalone fallback visibly diagnosable.packages/claude-code-plugin/scripts/migrate-legacy-hooks.js(CommonJS, no deps):~/.claude/hooks/codingbuddy-mode-detect.pywhen present.~/.claude/hooks/libonly when it is a symlink (real directories are never touched).UserPromptSubmitentries whose command references the legacy script file name, preserving every unrelated entry, and drops theUserPromptSubmitkey entirely when it becomes empty.settings.json.bak-codingbuddy-migration-<ts>backup before rewritingsettings.json.settings.json, graceful on missing~/.claude.opts.dryRunandCODINGBUDDY_POSTINSTALL_DRY_RUN=1.[CodingBuddy Plugin] Migrated ...log lines so users can see exactly what changed.scripts/postinstall.jsso every install runs it (including CI, since it is a no-op on clean systems). Migration failures never blocknpm install— they are surfaced as warnings.# Backend: ...diagnostic marker tohooks/user-prompt-submit.py(hardening(plugin): eliminate stale mandatory-MCP hook behavior and make standalone fallback more visible #1384) so users can tell at a glance whether mode handling is backed by the MCP server or the self-contained engine. Covers the exception fallback branch too.Test plan
scripts/migrate-legacy-hooks.spec.ts, covering:libremoval vs. real-directory safety (target dir never deleted).UserPromptSubmitarrays.UserPromptSubmitkey dropped when empty; sibling keys preserved.opts.dryRunandCODINGBUDDY_POSTINSTALL_DRY_RUN=1env var.~/.claude, missingsettings.json, malformed JSON all degrade gracefully.#1384): stale-script and settings-update log lines; quiet on clean systems.LEGACY_SCRIPT_NAMEconstant exposed for callers.hooks/test_user_prompt_submit.py::TestBackendDiagnosticMarkerasserting the new# Backend: standalone/# Backend: mcp-enhancedmarkers.yarn workspace codingbuddy-claude-plugin lintyarn workspace codingbuddy-claude-plugin format:checkyarn workspace codingbuddy-claude-plugin typecheckyarn workspace codingbuddy-claude-plugin test:coverage→ 144/144 passedyarn workspace codingbuddy-claude-plugin circular→ no circular depsyarn workspace codingbuddy-claude-plugin buildpython3 -m pytest packages/claude-code-plugin/hooks/test_user_prompt_submit.py→ 39/39 passednpm audit --severity highforcodingbuddy,codingbuddy-claude-plugin,landing-page— all clean.Closes #1381
Closes #1384