{ "permissions": { "allow": [ "Read", "Glob", "Grep", "Bash(npm *)", "Bash(npx *)", "Bash(node *)", "Bash(git *)", "Bash(ls *)", "Bash(mkdir *)", "Bash(which *)", "Bash(python *)", "Bash(pip *)", "Bash(taskkill *)", "Bash(mv *)", "Bash(rm *)", "Edit(src/**)", "Edit(tests/**)", "Edit(docs/**)", "Edit(scripts/**)", "Edit(.claude/**)", "Edit(*.md)", "Edit(*.json)", "Write(src/**)", "Write(tests/**)", "Write(docs/**)", "Write(scripts/**)", "Write(.claude/**)", "Write(*.md)", "Write(*.json)" ], "deny": [ "Bash(rm -rf /)", "Bash(git push --force*)", "Bash(npm publish*)", "Edit(.env*)", "Edit(secrets.*)" ] }, "hooks": { "UserPromptSubmit": [ { "matcher": "", "hooks": [ { "type": "command", "command": "cat <<'INJECT'\n[D3RO-VOICE 강제 규칙]\n1. 구현 전 반드시 설계서 참조: docs/design/00~03 (아키텍처, 서비스 명세, IPC 타입, DB/UI)\n2. 서비스: 싱글톤 + EventEmitter. 설계서 01의 인터페이스를 그대로 구현\n3. IPC: 설계서 02의 ipc-channels.ts 채널명/타입을 그대로 사용\n4. 에러: 설계서 02의 D3ROError + ErrorCode enum 사용\n5. DB: 설계서 03의 drizzle-orm 스키마를 그대로 사용\n6. UI: 설계서 03의 컴포넌트 Props/상태/IPC 명세를 그대로 따름\n7. 윈도우: 프리로딩 + 2-phase 리사이즈 (설계서 01 WindowManagerService)\n8. 상태머신: RecognitionState + AudioState 분리 (설계서 01 VoiceModeService)\n9. 팝업: Vanilla JS, 설계서 03의 HTML/CSS/애니메이션 스펙 준수\n10. any 타입 절대 금지, console.log 절대 금지\n11. 테마 SSOT: 색상은 d3roPalette/d3roShadow(CSS var) 사용, 렌더러 tsx/ts에 하드코딩 hex 금지 (theme.ts 제외)\n12. i18n: React 컴포넌트 UI 문자열은 t() 함수 사용, 하드코딩 한국어/영어 금지\n13. 작업 완료 즉시 memory/project_status.md 갱신 의무 — 미루지 말 것\nINJECT" } ] } ], "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "FILE=$(cat /dev/stdin | python -c \"import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))\" 2>/dev/null); if echo \"$FILE\" | grep -qE '\\.(ts|tsx)$'; then cd 'D:/workspace/D3ROVoice' && npx tsc --noEmit 2>&1 | head -20 || true; fi" } ] }, { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "FILE=$(cat /dev/stdin | python -c \"import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))\" 2>/dev/null); if echo \"$FILE\" | grep -qE '\\.(ts|tsx)$'; then ISSUES=''; ANY=$(grep -nE ': any[^a-zA-Z]||as any' \"$FILE\" 2>/dev/null | grep -v '// eslint' | grep -v 'import' | head -3); if [ -n \"$ANY\" ]; then ISSUES=\"any 타입: $ANY\"; fi; LOG=$(grep -n 'console\\.log' \"$FILE\" 2>/dev/null | head -3); if [ -n \"$LOG\" ]; then ISSUES=\"$ISSUES | console.log: $LOG\"; fi; if [ -n \"$ISSUES\" ]; then echo \"[HARNESS VIOLATION] $ISSUES — 즉시 수정 필요\"; fi; fi" } ] }, { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "FILE=$(cat /dev/stdin | python -c \"import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))\" 2>/dev/null); if echo \"$FILE\" | grep -qE 'src/renderer/.*\\.(tsx|ts)$' && ! echo \"$FILE\" | grep -q 'theme\\.ts' && ! echo \"$FILE\" | grep -q 'theme-vars'; then HC=$(grep -noE '#[0-9a-fA-F]{6}' \"$FILE\" 2>/dev/null | grep -v '//' | grep -v 'var(' | head -3); if [ -n \"$HC\" ]; then echo \"[SSOT WARN] 하드코딩 hex 색상 — d3roPalette/CSS var 사용:\"; echo \"$HC\"; fi; fi" } ] }, { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "FILE=$(cat /dev/stdin | python -c \"import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))\" 2>/dev/null); if echo \"$FILE\" | grep -qE 'src/renderer/.*\\.tsx$' && ! echo \"$FILE\" | grep -qE 'locales/|i18n|test'; then I18N=$(python -c \"import re; lines=open('$FILE','r',encoding='utf-8').readlines(); [print(f'{i+1}:{l.rstrip()}') for i,l in enumerate(lines) if re.search(r'[\\uAC00-\\uD7AF]{2,}',l) and '//' not in l and 'import' not in l]\" 2>/dev/null | head -3); if [ -n \"$I18N\" ]; then echo \"[i18n WARN] 하드코딩 한국어 문자열 — t() 함수 사용 필요:\"; echo \"$I18N\"; fi; fi" } ] } ], "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "FILE=$(cat /dev/stdin | python -c \"import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))\" 2>/dev/null); if echo \"$FILE\" | grep -qE '\\.env|\\.secret|credentials'; then echo 'BLOCKED: 시크릿 파일 수정 금지' >&2; exit 2; fi; exit 0" } ] } ] } }