Sprint 002 전체 구현 — F1 (자매 skill bypass) 해결:
Enforcement core:
- src/enforcement/skill-context.ts — 스킬 컨텍스트 생성/읽기/삭제/만료 체크
- src/enforcement/skill-trace.ts — 도구 사용 추적 (JSONL append)
- src/enforcement/guard.ts — pre-tool 가드 (context 유무 + 만료 + escape hatch)
Hooks (실제 로직):
- hooks/pre-tool.sh — Write/Edit/Bash 게이트 (context 없으면 exit 2)
- hooks/post-tool.sh — 도구 사용 trace 자동 기록
CLI:
- rails skill-context {create|show|clear}
- rails skill-trace {show|blocked}
Tests (13 신규, 22 total pass):
- skill-context: CRUD + 만료 감지
- skill-trace: append + read + blocked count
- guard: no-context 차단, valid 허용, expired 차단, RAILS_ENFORCE=off escape hatch
검증: tsc --noEmit ✓ | vitest 22/22 ✓ | build ✓ | rails --help ✓
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
51 lines
1.4 KiB
Bash
Executable File
51 lines
1.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# hanarang-rails pre-tool hook
|
|
# Blocks Write/Edit/Bash if no valid skill context exists.
|
|
# Input: stdin JSON event from Claude Code
|
|
# Exit: 0 = allow, 2 = block
|
|
set -euo pipefail
|
|
|
|
# Escape hatch
|
|
if [[ "${RAILS_ENFORCE:-on}" == "off" ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Read tool event from stdin
|
|
EVENT=$(cat)
|
|
TOOL=$(echo "$EVENT" | jq -r '.tool_name // empty' 2>/dev/null || true)
|
|
|
|
# Only gate Write, Edit, Bash
|
|
case "$TOOL" in
|
|
Write|Edit|Bash) ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
|
|
# Find project root
|
|
CWD="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
CONTEXT_FILE="$CWD/.rails/skill-context.json"
|
|
|
|
# Check context exists
|
|
if [[ ! -f "$CONTEXT_FILE" ]]; then
|
|
echo "[rails-enforce] No skill context. Enter the pipeline via /rails first." >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Check context not expired (TTL check)
|
|
if command -v jq >/dev/null 2>&1; then
|
|
CREATED=$(jq -r '.createdAt // empty' "$CONTEXT_FILE" 2>/dev/null || true)
|
|
TTL=$(jq -r '.ttlSeconds // 300' "$CONTEXT_FILE" 2>/dev/null || echo 300)
|
|
|
|
if [[ -n "$CREATED" ]]; then
|
|
CREATED_EPOCH=$(date -d "$CREATED" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${CREATED%%.*}" +%s 2>/dev/null || echo 0)
|
|
NOW_EPOCH=$(date +%s)
|
|
AGE=$(( NOW_EPOCH - CREATED_EPOCH ))
|
|
|
|
if [[ "$AGE" -gt "$TTL" ]]; then
|
|
echo "[rails-enforce] Skill context expired (age: ${AGE}s > ttl: ${TTL}s). Re-enter the skill." >&2
|
|
exit 2
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
exit 0
|