ruleguard — 설계·구현계획 (확정 26.9.10)
출처 = [코디] Fable 5 계획 초안 + [GPT] 코덱스 의견 + [페페] 그릴링 결정 11개(
handoff.md). 이 문서가 구현의 단일 출처다. 결정 자체는handoff.md「확정 결정」.
0. 조사로 확정된 사실
- 훅은 Git Bash 로 돈다. 명령 패턴은 ctxmeter 와 같게 절대경로 Python312 를 쓴다:
'C:/Users/delix/AppData/Local/Programs/Python/Python312/python.exe' C:/dev/tools/ruleguard/ruleguard.py --hook. python3= 이 PC에선 스토어 리다이렉터 → hookify 플러그인은 무음 실패. 누적 카운트·장부·승격·승인 해제도 없다 → 안 쓴다(계속 꺼 둔다).- Claude Code 2.1.260 훅 스키마: PreToolUse
hookSpecificOutput.permissionDecision(deny > defer > ask > allow, 복수 훅이면 deny 우선) · Stop 입력stop_hook_active·last_assistant_message·prompt_id, 차단 = 최상위{"decision":"block","reason":…}(하네스가 연속 8회면 강제 종료) · UserPromptSubmit =hookSpecificOutput.additionalContext. 타임아웃·크래시 = 결정 없음(fail-open). - transcript =
~/.claude/projects/c--dev/<session>.jsonl, 최대 116MB → 매 훅 전체 읽기 금지, 오프셋 증분 읽기. - broker
/ask(POST) →GET /ask/<id>. 텔레그램 답장 첫 단어가승인/ok/yes/거부/no…면 deploy·risk 파서가 삼킨다 → 해제 답장어 =해제/유지. - 훅은 네트워크 0·서브프로세스 0·목표 PreToolUse <200ms / Stop <1s(선례: 느린 PostToolUse 플러그인이 세션 시작 타임아웃을 냈다).
1. 파일
| 경로 | 역할 |
|---|---|
C:\dev\tools\ruleguard\ruleguard.py |
엔진 단일 파일. --hook(hook_event_name 분기) · --selftest · --replay <case> · report [--rules] [--health] [--notify] · add-rule · request-off · off --ask · on · unlock --ask · confirm --ask. 장부 I/O·락·세션 상태·transcript 증분 읽기·출력 봉투·지적/자기신고 정규식·태그 파서 |
C:\dev\tools\ruleguard\detectors\<rule-id>.py |
탐지기 1개 = 훅 1개. 엔진이 파일명으로 자동 발견 |
C:\dev\tools\ruleguard\ledger.json |
등록부·시드·승인 기록·enforce_from. 엔진/CLI만 쓴다 |
(자동) events.jsonl · state\<session_id>.json · logs\hook.log |
위반 이벤트(append-only) · 세션 캐시(오프셋·차단 이력) · 예외 로그 |
편집 C:\Users\delix\.claude\settings.json |
PreToolUse·UserPromptSubmit·Stop 에 1항목씩 병합(기존 broker·orca·ctxmeter 유지, 백업 먼저) |
편집 rules\tooling.md 🆕 · C:\dev\CLAUDE.md §4 |
태그 문법·명령·해제 절차 1절 + 포인터 1줄 |
새 의존성 0(stdlib: json·re·os·msvcrt·urllib).
2. 장부 스키마
{ "enforce_from": "<설치+24h, KST ISO>", // 이 시각 전 = 전 규칙 기록만(롤아웃)
"guard_from": "<같은 값>", // 이 시각부터 장부·엔진·settings 편집 차단
"rules": { "<rule-id>": { "kind":"auto|judge", "doc":"<파일>#<절제목>",
"detector":"<rule-id>|null", "dedupe":"prompt_id|session_id|tool_use_id",
"seed":{"count":N,"dates":[...],"source":"..."},
"detector_ok":"<selftest 통과 시각|mtime>", "confirmed":null } },
"approvals": [ {"action":"off|unlock|confirm","target":..,"ask_id":..,"answer":..,"at":..} ],
"pending": [ {"id":"P-0001","session_id":..,"prompt_id":..,"excerpt":..,"at":..} ] }
events.jsonl한 줄 ={ts, rule, key, session_id, prompt_id, source:auto|pepe|self, excerpt, mode:record|block, dismissed}.- count = seed.count + 그 규칙 events 의 서로 다른 key 수(dismissed 제외) — 같은 턴 재시도는 1건(코덱스 권고). 저장 안 하고 매번 계산.
- mode = off(유효 승인) → 아니면
now>=enforce_from && count>=2 && detector_ok면 block → 탐지기 없이 count>=2 면 nag → 그 외 record. - 규칙 id = kebab-case 슬러그.
doc포인터와 1:1.add-rule은 절제목이 실제 파일에 있는지 검사하고, 같은 doc 이 다른 id 에 이미 있으면 거부(id 바꿔 카운트 초기화하는 우회 차단). - 쓰기 = temp +
os.replace원자 교체 +msvcrt.locking락파일(동시 세션 5~6개).
3. 초기 장부 (페페 확정 26.9.10)
| id | kind | 탐지기 | seed |
|---|---|---|---|
| notify-missing | auto | Stop: 작업 턴인데 notify\.py\s+(done|step|ask) 또는 7777/notify- 호출 0 |
2 (8/2·8/6) |
| heredoc-file-write | auto | PreToolUse Bash: << + 실파일 리다이렉트(>·>>·tee, /dev/null 제외) |
2 (7/14~9/9) |
| pfs-git-add-all | auto | PreToolUse Bash: pfs 리포에서 git add -A|--all|.|<디렉터리>/ |
2 (7/13·9/3) |
| broker-error-retry | auto | PreToolUse Bash: 이번 턴 직전 broker(7777) 호출이 400/500 이었는데 같은 엔드포인트 재호출 | 2 (9/5·9/8) |
| gui-session0 | judge | PreToolUse Bash: GUI 실행 명령(Start-Process·start ""·chrome/msedge/notepad/explorer 등)인데 schtasks … /IT 경로 아님 → 체크리스트 주입(allow+additionalContext, 세션당 1회) |
2 (8/31·9/2) — 페페 확인(confirm) 전까지 미종결 |
| rules-preread-missing | auto | PreToolUse Write/Edit/MultiEdit/NotebookEdit: 코드·설정 확장자인데 세션 transcript 에 rules[\\/]tooling\.md Read 없음 |
1 (9/10) |
| report-tail-missing | auto | Stop: 작업 턴인데 last_assistant_message 에 시드: 줄 또는 컨텍스트 % 줄 없음 |
0 |
| session-relay-missed · relative-path-in-report · broker-temp-deny · phone-realdevice-skip | judge | 없음 | 각 1 |
- 「작업 턴」 = Edit/Write/MultiEdit/NotebookEdit ≥1 또는 Bash 에
git commit|git push|python .*\.py— 질의·그릴링 턴은 안 걸리게.
4. 탐지기 인터페이스 (= 「훅 생성」의 정의)
RULE = {"id":..., "events":["PreToolUse"|"Stop"], "tools":[...], "kind":"auto|judge",
"mode":"deny|checklist", "dedupe":..., "doc":...}
def check(ctx) -> None | {"key":..., "msg":"무엇을 어겼나", "fix":"지금 할 일 한 줄"}
# ctx.event, ctx.payload, ctx.turn_tool_uses() (tool_use+결과), ctx.session_has(pattern),
# ctx.last_text (Stop만), ctx.state
def selftest() -> [(이름, bool), ...] # 양성·음성 최소 2쌍
- 「규칙 X 2회, 훅부터」 = ①
detectors\X.py를 Write 도구로 신규 생성(가드 예외) ②ruleguard.py --selftest통과 → 엔진이detector_ok기록 → nag 종료. 코디가 장부를 직접 만지는 단계가 없다. - judge 대체 훅은
confirmed가 비면 UserPromptSubmit 이 세션당 1회 「페페 확인 ask 보내라」 → 페페확인→confirm X --ask ASK-….
5. 페페 지적·자기신고
- UserPromptSubmit: 지적 정규식(
알림.*안 (왔|온)|왜 (안|또)|또 (빠|누락|어겼)|빼먹|몇 번|규칙.*(무시|어겼|안 지)등) →pendingP-id + 주입 「답변 끝에[규칙위반] <rule-id>또는[규칙위반] none: 사유한 줄 필수」. - Stop:
last_assistant_message의[규칙위반] …파싱 → events(source=pepe, key=prompt_id) → pending 제거. 태그 없음·미등록 id → 1회 차단(안내).none= dismissed 로 남겨 감사. - 자기신고: 태그 없이
빠뜨렸|빠트렸|누락했|안 읽었|잊었|어겼|놓쳤|빼먹이 답변에 있으면 1회 차단 「규칙 id 태그 달아라」. 같은 prompt_id 에 auto 이벤트가 있으면 병합(이중 카운트 방지).
6. 출력
| 상황 | 출력(exit 0) |
|---|---|
| PreToolUse 위반·block | permissionDecision:"deny" + 사유(규칙 id·횟수·고칠 방법·해제는 request-off) |
| PreToolUse 위반·record | 없음(기록만) |
| 체크리스트(judge) | permissionDecision:"allow" + additionalContext |
Stop 위반·block(stop_hook_active false, 이 prompt_id 첫 차단) |
{"decision":"block","reason":…} |
| Stop nag(탐지기 없는 규칙 count>=2) | 턴당 1회 block, 「detectors\X.py 생성 → --selftest」 |
| UserPromptSubmit | 위반·pending 있을 때만 [규칙장부] … additionalContext(없으면 무출력) |
| 내부 예외 | 무출력 exit 0 + logs\hook.log |
7. 리스크 대책
- fail-open +
--selftest통과 전엔 settings 에 안 건다 +report --health(이벤트별 마지막 실행 시각)로 훅 사망 감지. - 무한루프:
stop_hook_active면 절대 차단 안 함 · prompt_id 당 최대 1회 · 하네스 8회 백스톱. - 성능: UserPromptSubmit 이 파일 크기를
turn_start_offset에 저장 → Stop 은 그 뒤만 읽음 · preread 는scan_offset증분 + 찾으면 캐시. - 헤드리스
claude -p: transcript 의 entrypoint(sdk 계열) 판별로 notify·tail·preread·지적 계층을 건너뛴다(가능한지 실측). 보조로RULEGUARD=off면 즉시 exit 0. 다른 도구 코드는 안 고친다. - 서브에이전트:
agent_id가 있으면 notify·tail·preread 는 건너뜀(부모 몫). heredoc·가드는 적용. - broker: 훅은
/permission을 절대 안 부른다.request-off는/ask만. - 인코딩·경로:
sys.stdout.reconfigure(utf-8),os.path.normcase(abspath)비교.
8. 페페 승인 해제와 우회 차단
ruleguard.py request-off <rule> "사유"→/ask발송(본문에 「답장:해제또는유지」) → ask_id 출력.- 페페 텔레그램
해제. ruleguard.py off <rule> --ask ASK-…→GET /ask/<id>서버측 검증(answered · 질문에 rule id · 답이해제로 시작) → approvals 기록 → 그 규칙만 off.on은 승인 불필요. - 가드(guard_from이후): tools\ruleguard 하위 기존 파일 Edit/Write → deny(신규 detectors 만 허용) · Bash 에ruleguard|ledger.json|events.jsonl+ 쓰기 동사 → deny(ruleguard.py <명령>은 통과) · settings.json 에서 ruleguard 항목 제거 시도 → deny. 정당한 수정은unlock <경로> --ask(30분 창).
9. 롤아웃 (페페 결정 = 하루 기록 모드 후 차단)
- 설치 즉시 = 기록 모드(
enforce_from= 설치 +24h). 오탐 튜닝 자유. - 전환 2시간 전 = 예약작업 1회(숨김 vbs/wscript) →
ruleguard.py report --notify로 24h 기록을 텔레그램 보고. 페페가 막으려면request-off흐름. enforce_from도달 = 시각 기반 자동 차단(예약작업이 죽어도 전환은 된다).
10. 검증
--selftest: 락·원자쓰기 / count=seed+distinct key / mode 전이 / 태그 파서 / 지적·자기신고 정규식 양성 6·음성 6(「누락 검사 완료」 같은 정상 문장 제외) / 출력 JSON / 깨진 stdin·없는 transcript → 무출력 exit 0 / 각 탐지기 selftest / 가드 경로 판정 /RULEGUARD=off.--replay <case>: 스크립트가 내부에서 합성 입력을 만들어 자기--hook경로로 돌린다(heredoc·파이프 안 씀). 기대 JSON 정확 비교.- 성능: 실제 대형 transcript 로 PreToolUse <200ms · Stop <1s 실측.
- 실세션(기록 모드): settings 병합 후 실제 트리거 → events 1줄 + transcript
hook_success확인. - 해제 왕복·차단 실측은
enforce_from이후.