배경
cmd >> "$FILE" 2>/dev/null은 보기와 다르게 동작합니다. 셸은 리다이렉션을 좌→우로 적용하므로, 목적지를 열지 못하면 그 에러가 stderr가 막히기 전에 실제 stderr로 나갑니다. 명령의 종료 상태도 실패가 됩니다.
이 결함은 이번 라운드에서 네 번 독립적으로 발견됐고 모두 수정됐습니다:
| PR |
대상 |
| #1054 |
start.sh _cmdline_is_project_bot |
| #1055 |
hook-common.sh log() |
| #1056 |
log() 복사본 6개 |
| #1058 |
다중행 audit() 2개 + distill.sh 2줄 + lifecycle-common.sh 2곳 + backups-rotate |
hook-common.test.sh의 감사가 log()/audit() 본문(단일행·다중행)에서의 재발을 막습니다.
이 이슈의 범위 — 남은 43곳
감사를 log/audit으로 의도적으로 좁게 잡았기 때문에, 같은 순서를 가진 나머지 사이트는 그대로 남아 있습니다. 다만 성격이 다릅니다.
핵심 차이: 이들은 전부 가드가 있습니다.
cmd > "$tmp" 2>/dev/null && mv "$tmp" "$dst" # 열기 실패 시 mv 안 함 → 정확성 보존
cmd >> "$LEDGER" 2>/dev/null || true # 실패 무시가 명시적
cmd > "$f" 2>/dev/null || rm -f "$f" # 실패 시 정리
if cmd > "$tmp" 2>/dev/null; then ... # 상태를 실제로 검사
즉 기능적 결과는 옳고, 새는 것은 stderr 메시지뿐입니다. 이미 수정한 log/audit은 "조용하고 실패하지 않겠다"는 계약 자체를 깨뜨렸던 것과 대비됩니다.
파일별 분포 (main aebe40b 기준)
| 파일 |
건수 |
성격 |
hooks/skill-review/autoinstall.sh |
10 |
|| true / && / if 가드 |
hooks/load-memory.sh |
6 |
핫패스 — 검색 레인 4 + 타이밍 플러시 + scan 레인 |
hooks/skill-review.sh |
4 |
|| true |
hooks/distill/wiki-queue.sh |
3 |
&& mv |
hooks/lib/autonomy-guard.sh |
2 |
|| exit 0 / if |
scripts/ccc-skill-autosave.sh |
3 |
&& / if |
| 기타 15개 파일 |
15 |
대부분 && mv 임시파일 패턴 |
(hook-common.sh 1건은 결함을 설명하는 주석이며 코드가 아닙니다.)
착수를 보류하는 이유
- 실익이 노이즈 제거뿐입니다. 정확성 회귀는 없습니다.
- 핫패스가 섞여 있습니다.
load-memory.sh 6건은 모든 세션 시작의 크리티컬 패스이고, 그중 4건은 백그라운드 검색 레인입니다. 노이즈만을 위해 여길 편집하는 건 #1040에서 확인한 것과 같은 종류의 나쁜 거래입니다.
- 일괄 적용이 위험합니다. #1058이 실증했습니다 —
lifecycle-common.sh는 append 실패가 함수의 정당한 실패 신호라 로거와 같은 || :를 붙였다면 그 자체가 회귀였습니다. 43곳도 사이트별 판단이 필요합니다.
착수 조건
실제 노이즈가 문제로 관측될 때 착수합니다. 구체적으로:
- 훅 stderr 오염이 세션에서 보고되거나
- cron 로그에 이 계열 메시지가 쌓이는 것이 확인되거나
- 해당 파일을 다른 이유로 건드릴 때 편승
착수 시 원칙: 순서 수정은 어디서나 안전하지만, || : 추가는 계약이 명시적으로 best-effort인 곳에만. 상태를 소비하는 호출자가 있으면 절대 삼키지 말 것.
재현
grep -rnE '(^|[^0-9&2])>>?[[:space:]]+"[^"]+"[[:space:]]+2>/dev/null' \
--include='*.sh' claude/hooks scripts | grep -v '\.test\.sh:'
기록 목적의 이슈입니다 (#1041 선례와 동일한 성격). 누군가 이 패턴을 다시 발견해 전수 조사를 반복하지 않도록 분류와 판단 근거를 남깁니다.
관련: #584, #1054, #1055, #1056, #1058
배경
cmd >> "$FILE" 2>/dev/null은 보기와 다르게 동작합니다. 셸은 리다이렉션을 좌→우로 적용하므로, 목적지를 열지 못하면 그 에러가 stderr가 막히기 전에 실제 stderr로 나갑니다. 명령의 종료 상태도 실패가 됩니다.이 결함은 이번 라운드에서 네 번 독립적으로 발견됐고 모두 수정됐습니다:
start.sh_cmdline_is_project_bothook-common.shlog()log()복사본 6개audit()2개 +distill.sh2줄 +lifecycle-common.sh2곳 + backups-rotatehook-common.test.sh의 감사가log()/audit()본문(단일행·다중행)에서의 재발을 막습니다.이 이슈의 범위 — 남은 43곳
감사를
log/audit으로 의도적으로 좁게 잡았기 때문에, 같은 순서를 가진 나머지 사이트는 그대로 남아 있습니다. 다만 성격이 다릅니다.핵심 차이: 이들은 전부 가드가 있습니다.
즉 기능적 결과는 옳고, 새는 것은 stderr 메시지뿐입니다. 이미 수정한
log/audit은 "조용하고 실패하지 않겠다"는 계약 자체를 깨뜨렸던 것과 대비됩니다.파일별 분포 (main
aebe40b기준)hooks/skill-review/autoinstall.sh|| true/&&/if가드hooks/load-memory.shhooks/skill-review.sh|| truehooks/distill/wiki-queue.sh&& mvhooks/lib/autonomy-guard.sh|| exit 0/ifscripts/ccc-skill-autosave.sh&&/if&& mv임시파일 패턴(
hook-common.sh1건은 결함을 설명하는 주석이며 코드가 아닙니다.)착수를 보류하는 이유
load-memory.sh6건은 모든 세션 시작의 크리티컬 패스이고, 그중 4건은 백그라운드 검색 레인입니다. 노이즈만을 위해 여길 편집하는 건 #1040에서 확인한 것과 같은 종류의 나쁜 거래입니다.lifecycle-common.sh는 append 실패가 함수의 정당한 실패 신호라 로거와 같은|| :를 붙였다면 그 자체가 회귀였습니다. 43곳도 사이트별 판단이 필요합니다.착수 조건
실제 노이즈가 문제로 관측될 때 착수합니다. 구체적으로:
착수 시 원칙: 순서 수정은 어디서나 안전하지만,
|| :추가는 계약이 명시적으로 best-effort인 곳에만. 상태를 소비하는 호출자가 있으면 절대 삼키지 말 것.재현
기록 목적의 이슈입니다 (#1041 선례와 동일한 성격). 누군가 이 패턴을 다시 발견해 전수 조사를 반복하지 않도록 분류와 판단 근거를 남깁니다.
관련: #584, #1054, #1055, #1056, #1058