# Design — Skill Enforcement > **처방 대상**: F1 (하네스 skill bypass) ## 문제 기존 hanarang-harness 에서 나랑이가 "worker 스폰해서 바로 시작한다" 라며 skill 진입을 건너뛰었다. skill 이 권고 수준이라 강제력이 없었다. 결과적으로 파이프라인 일관성이 무너졌다. ## 해결 전략 — 다층 방어 1. **Layer 1 — 시스템 프롬프트 강제문** (약한 방어) - 자매 시스템 프롬프트에 "반드시 `/rails` 스킬을 거쳐야 한다" 명시 - LLM 지시 준수 기대치에 의존 — 약함 2. **Layer 2 — Pre-Tool Hook (중간 방어)** - 자매가 Write / Edit / Bash 를 호출하기 직전 - 현재 세션이 `rails-skill-context` 를 세팅했는지 확인 - 세팅 안 됐으면 exit 2 로 차단하고 "먼저 /rails 를 호출하세요" 메시지 3. **Layer 3 — Post-Tool Hook (강한 방어)** - 자매가 작업 결과를 커밋한 후 - `.rails/skill-trace.jsonl` 을 확인해 파이프라인이 skill 경로를 탔는지 감사 - 우회 감지 시 작업을 자동 revert + escalation 4. **Layer 4 — Contract Validator (최종 방어)** - Sprint Contract 의 DoD 에 `skill-path-taken` 체크 포함 - contract validator 가 `.rails/skill-trace.jsonl` 을 읽어 검증 - 우회한 경우 `ABORT_PRECHECK` 반환 ## 구현 — Layer 2/3 상세 ### Pre-Tool Hook 로직 ```bash #!/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 로직 ```bash #!/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 ` 슬래시 커맨드 진입 시 skill 이 생성: ```json { "skillName": "rails", "subcommand": "plan", "sprintId": "SPRINT-001", "contractId": "01HW0XYZ...", "sessionId": "", "createdAt": "2026-04-10T12:00:00Z", "ageSeconds": 5, "allowedTools": ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] } ``` skill 의 entry 스크립트가 이 파일을 기록한다. 이 파일이 있으면 "skill 진입 완료" 증거. ### Skill Trace 파일 `.rails/skill-trace.jsonl` — 자매가 파이프라인 진행 중 호출한 도구 로그: ```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 우회가 감지된 경우: 1. 자매의 최근 commit 을 `git revert` 로 되돌림 (hanarang-rails orchestrator 권한) 2. 파이프라인 FSM 을 이전 상태로 rollback 3. 디스코드에 에스컬레이션 메시지 전송 4. 로그에 구조화 기록 ## 참고 - `principles.md` 원칙 4 - `failure-audit.md` F1 - Claude Code hooks: `.claude/settings.json` → `PreToolUse` / `PostToolUse` matcher