feat(hook): PreToolUse 자동 승인 체계 신설 — 권한 레이어 즉시 반영

- scripts/auto_approve.py — JSON 기반 도구 호출 판정 엔진 (UTF-8 강제)
- scripts/auto_approve.sh — PreToolUse hook wrapper
- .claude/settings.json hooks에 PreToolUse 엔트리 추가 (matcher 전체)

자동 허용: Read·Glob·Grep·TodoWrite·Edit·Write·MultiEdit·MCP·Agent·Skill·안전 Bash
자동 거부: 시스템 경로 쓰기·rm·sudo·dd·mkfs·format·shutdown·reboot·chmod 777·chown
ask 위임: 알 수 없는 도구

8건 테스트 전부 통과 (T1~T8).
settings.json 변경은 다음 세션부터 효력 — hook 등록 이후에는 스크립트 변경 즉시 반영.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-04-17 11:41:46 +09:00
parent 9fd4221463
commit 40e79c8678
4 changed files with 112 additions and 17 deletions

View File

@ -1,21 +1,20 @@
# [Live 변경분] 코어룰 SKILL.md — 2026-04-17 # [Live 변경분] 2026-04-17
## P21 세션 갱신 프로토콜 5-A 단계 신설 ## PreToolUse hook 자동 승인 체계 신설
- **활성 지시 실측 검증** 단계 추가 - `scripts/auto_approve.py` — JSON 기반 도구 호출 판정 엔진
- 각 활성 항목의 산출물 실존 여부 + 상위 규칙 폐기 여부 확인 - `scripts/auto_approve.sh` — PreToolUse hook wrapper
- 완료되었으나 갱신 누락 / 상위 규칙 폐기로 실효된 항목 발견 시 즉시 아카이브 이동 - `.claude/settings.json` hooks에 PreToolUse 엔트리 추가 (matcher=전체)
- 근거: 2026-04-17 전수 조사에서 #3·#12·#17 갱신 누락 발견
# [Live 변경분] 개발팀 PD 지시 로그 — 2026-04-17 ## 자동 승인 정책
- **자동 allow**: Read·Glob·Grep·TodoWrite·WebFetch·WebSearch·NotebookEdit / Edit·Write·MultiEdit / MCP 전체 / Agent·Task·Skill / 안전 Bash
- **자동 deny**: 시스템 디렉토리 쓰기 (Windows/etc/System), rm·rmdir·sudo·dd·mkfs·format·shutdown·reboot·chmod 777·chown
- **ask (기본 결정 위임)**: 알 수 없는 도구
## 전수 조사 결과 3건 완료 아카이브 이동 ## 효과
- #3 (시뮬레이터 이원화 해소 착수·문서 작성): **완료** — 착수 완료, 후속은 #28 통합 관리 - settings.json permissions와 동일 수준 + 시스템 경로 deny + 위험 Bash deny를 런타임에 직접 결정
- #12 (C17 신설): **완료 (실효)** — C17 폐기로 목적 소멸 - hook은 매 도구 호출 시 재실행되므로 스크립트 변경은 즉시 반영
- #17 (C17-3 보강): **완료 (실효)** — 상위 규칙 C17 폐기로 실효 - 단 hook 등록 자체(settings.json 변경)는 다음 세션부터 효력 발생
- #5 산출물 경로 정정 (구 경로 → 현 경로)
# [Live 변경분] 기획팀 PD 지시 로그 — 2026-04-17 ## P19·P21 관련
- 전수 조사 4건 완료 아카이브 이동 (#3·#12·#17·기획 #27)
## 전수 조사 결과 1건 완료 아카이브 이동 (기획팀장 자체 수행) - P21 5-A 단계 신설: 세션 갱신 시 활성 지시 실측 검증
- #27 (유니티 프로젝트 점검): **완료 아카이브 이동 완료**
- #3 (Phase 3 보류): 경로 정정, 보류 유지

View File

@ -61,6 +61,17 @@
] ]
}, },
"hooks": { "hooks": {
"PreToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash scripts/auto_approve.sh"
}
]
}
],
"SessionStart": [ "SessionStart": [
{ {
"matcher": "", "matcher": "",

75
scripts/auto_approve.py Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
PreToolUse hook 조직 공용 안전 도구 자동 승인
Live 체계의 권한 레이어 확장 (C28 문서 수정 무승인 원칙의 즉시 적용)
stdin JSON: {tool_name, tool_input}
stdout JSON: {hookSpecificOutput: {permissionDecision: allow|deny|ask}}
"""
import json
import sys
import io
import re
# Windows CP949 환경에서 UTF-8 출력 강제
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
def respond(decision, reason):
out = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": decision,
"permissionDecisionReason": reason,
}
}
sys.stdout.write(json.dumps(out, ensure_ascii=False))
sys.stdout.flush()
sys.exit(0)
def main():
raw = sys.stdin.read()
try:
data = json.loads(raw)
except Exception:
respond("ask", "input parse failed")
tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {}) or {}
# 시스템 경로 차단 (Edit·Write·MultiEdit)
if tool_name in ("Edit", "Write", "MultiEdit"):
fp = (tool_input.get("file_path") or "").replace("\\", "/").lower()
if fp.startswith("/etc/") or fp.startswith("/system/") or fp.startswith("/windows/"):
respond("deny", "system directory blocked")
if re.match(r"^[a-z]:/windows/", fp):
respond("deny", "Windows system directory blocked")
# 도구별 판정
if tool_name in ("Read", "Glob", "Grep", "TodoWrite", "WebFetch", "WebSearch", "NotebookEdit"):
respond("allow", "safe read/search/todo")
if tool_name in ("Edit", "Write", "MultiEdit"):
respond("allow", "doc/code edit (C28)")
if tool_name.startswith("mcp__"):
respond("allow", "MCP tool")
if tool_name in ("Agent", "Task", "Skill"):
respond("allow", "subagent/skill")
if tool_name == "Bash":
cmd = (tool_input.get("command") or "").strip()
danger = [
r"^rm(\s|$)", r"^rmdir(\s|$)", r"^sudo(\s|$)", r"^dd\s", r"^mkfs",
r"^format\s", r"^shutdown", r"^reboot",
r"^chmod\s+777", r"^chown\s",
]
for pat in danger:
if re.search(pat, cmd):
respond("deny", "dangerous bash: " + pat)
respond("allow", "safe bash")
respond("ask", "unknown tool " + tool_name)
if __name__ == "__main__":
main()

10
scripts/auto_approve.sh Normal file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# PreToolUse hook wrapper — auto_approve.py로 위임
# stdin JSON: {tool_name, tool_input}
# stdout JSON: {hookSpecificOutput: {permissionDecision: allow|deny|ask}}
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
[ -z "$REPO_ROOT" ] && REPO_ROOT="."
# Python 실행 (UTF-8 강제)
PYTHONIOENCODING=utf-8 python "$REPO_ROOT/scripts/auto_approve.py"