- CLAUDE.md 를 .claude/rules/ 4파일로 분할 (project/stack/principles/workflow) - Plans.md 루트 인덱스 (스프린트 목차 + 참조만) - hooks/ pre/post-tool.sh 스켈레톤 (no-op, Sprint 002에서 구현) - .plans/OVERVIEW.md — 목표/범위/성공기준 8개 - .plans/failure-audit.md — F1~F6 실패 감사 (증거 기반) - .plans/design/ 6개 문서: * state-machine.md (XState v5 FSM 설계) * sprint-contract.md (Zod schema + validator) * skill-enforcement.md (4계층 방어) * handoff.md (상태 전이 기반 자매 통신) * retry-policy.md (backoff + escalation) * qa-template.md (체크리스트 runtime) - .plans/sprints/ 8개 스프린트 명세 (SPRINT-000~007) - .plans/migration/from-hanarang-harness.md (자산 매트릭스 + 단계별 가이드) 총 17개 문서, 약 2146 lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.0 KiB
5.0 KiB
Design — Skill Enforcement
처방 대상: F1 (하네스 skill bypass)
문제
기존 hanarang-harness 에서 나랑이가 "worker 스폰해서 바로 시작한다" 라며 skill 진입을 건너뛰었다. skill 이 권고 수준이라 강제력이 없었다. 결과적으로 파이프라인 일관성이 무너졌다.
해결 전략 — 다층 방어
-
Layer 1 — 시스템 프롬프트 강제문 (약한 방어)
- 자매 시스템 프롬프트에 "반드시
/rails스킬을 거쳐야 한다" 명시 - LLM 지시 준수 기대치에 의존 — 약함
- 자매 시스템 프롬프트에 "반드시
-
Layer 2 — Pre-Tool Hook (중간 방어)
- 자매가 Write / Edit / Bash 를 호출하기 직전
- 현재 세션이
rails-skill-context를 세팅했는지 확인 - 세팅 안 됐으면 exit 2 로 차단하고 "먼저 /rails 를 호출하세요" 메시지
-
Layer 3 — Post-Tool Hook (강한 방어)
- 자매가 작업 결과를 커밋한 후
.rails/skill-trace.jsonl을 확인해 파이프라인이 skill 경로를 탔는지 감사- 우회 감지 시 작업을 자동 revert + escalation
-
Layer 4 — Contract Validator (최종 방어)
- Sprint Contract 의 DoD 에
skill-path-taken체크 포함 - contract validator 가
.rails/skill-trace.jsonl을 읽어 검증 - 우회한 경우
ABORT_PRECHECK반환
- Sprint Contract 의 DoD 에
구현 — Layer 2/3 상세
Pre-Tool Hook 로직
#!/usr/bin/env bash
# hooks/pre-tool.sh
set -euo pipefail
# stdin 에서 event JSON 읽기
EVENT=$(cat)
# 이 이벤트가 Write / Edit / Bash 인지 확인
TOOL=$(echo "$EVENT" | jq -r '.tool_name // empty')
case "$TOOL" in
Write|Edit|Bash) ;;
*) exit 0 ;;
esac
# 작업 디렉토리 추출
CWD="${CLAUDE_PROJECT_DIR:-$(pwd)}"
# skill context 파일 확인
SKILL_CTX="$CWD/.rails/skill-context.json"
if [ ! -f "$SKILL_CTX" ]; then
echo "[rails-enforce] Skill context missing. /rails 스킬을 먼저 호출하세요." >&2
exit 2 # 차단
fi
# skill context 가 유효한지 (최근 N초 이내) 확인
AGE=$(jq -r '.ageSeconds // 999' "$SKILL_CTX")
if [ "$AGE" -gt 300 ]; then
echo "[rails-enforce] Skill context stale (>${AGE}s). 세션을 재시작하세요." >&2
exit 2
fi
exit 0
Post-Tool Hook 로직
#!/usr/bin/env bash
# hooks/post-tool.sh
set -euo pipefail
EVENT=$(cat)
CWD="${CLAUDE_PROJECT_DIR:-$(pwd)}"
TRACE="$CWD/.rails/skill-trace.jsonl"
# 이 도구 사용을 trace 에 append
mkdir -p "$(dirname "$TRACE")"
echo "$EVENT" | jq -c \
'{ ts: now, tool: .tool_name, cwd: env.PWD, session_id: env.CLAUDE_SESSION_ID }' \
>> "$TRACE"
exit 0
Skill Context 구조
/rails <subcommand> 슬래시 커맨드 진입 시 skill 이 생성:
{
"skillName": "rails",
"subcommand": "plan",
"sprintId": "SPRINT-001",
"contractId": "01HW0XYZ...",
"sessionId": "<session-id>",
"createdAt": "2026-04-10T12:00:00Z",
"ageSeconds": 5,
"allowedTools": ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
}
skill 의 entry 스크립트가 이 파일을 기록한다. 이 파일이 있으면 "skill 진입 완료" 증거.
Skill Trace 파일
.rails/skill-trace.jsonl — 자매가 파이프라인 진행 중 호출한 도구 로그:
{"ts":1765789200,"tool":"Read","cwd":"/home/narang/projects/arang","session_id":"ab12"}
{"ts":1765789201,"tool":"Bash","cwd":"/home/narang/projects/arang","session_id":"ab12"}
{"ts":1765789202,"tool":"Write","cwd":"/home/narang/projects/arang","session_id":"ab12"}
Contract validator 는 이 파일에서 다음을 확인:
- trace 가 존재하는가?
- 첫 이벤트 이전에
skill-context.json이 세팅됐는가? - 세션 ID 가 일관된가? (중간에 다른 세션이 개입 안 했는가)
우회 사례 & 대응
| 우회 시도 | 감지 | 대응 |
|---|---|---|
| 자매가 skill 무시하고 직접 Write | Pre-hook (skill-context 없음) | exit 2 로 차단 + 에러 메시지 |
| 자매가 skill 호출 후 딴 데서 작업 | Post-hook (cwd mismatch) | trace 에 기록, validator 에서 FAIL |
| 자매가 skill 호출 후 stale 세션 재사용 | Pre-hook (ageSeconds > 300) | exit 2 + 재시작 안내 |
| 자매가 trace 파일 삭제 | Pre-hook (trace 파일 없음) | exit 2 |
| 자매가 trace 파일 변조 | Post-hook signature 불일치 | (향후) hash chain 으로 감지 |
예외 — Escape Hatch
긴급 상황에서 enforcement 를 일시 해제할 필요가 있을 수 있다:
- 환경 변수
RAILS_ENFORCE=off세팅 시 hook 이 warn 만 남기고 통과 - 단, 반드시 로그에 기록되고 다음 스프린트 시작 시 경고 표시
- production 에서는 기본 on
실패 시 복구
skill 우회가 감지된 경우:
- 자매의 최근 commit 을
git revert로 되돌림 (hanarang-rails orchestrator 권한) - 파이프라인 FSM 을 이전 상태로 rollback
- 디스코드에 에스컬레이션 메시지 전송
- 로그에 구조화 기록
참고
principles.md원칙 4failure-audit.mdF1- Claude Code hooks:
.claude/settings.json→PreToolUse/PostToolUsematcher