diff --git a/.claude/hooks/block-secrets.mjs b/.claude/hooks/block-secrets.mjs new file mode 100755 index 0000000..ca4aa90 --- /dev/null +++ b/.claude/hooks/block-secrets.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +// PreToolUse 가드 — 비밀값 파일 접근을 하드 차단한다. +// +// 차단 대상: .project-env, .env, .env.*, ~/.env (LITELLM_KEY 등). +// - 허용: .project-env.example (참조용 예시는 OK) +// 적용 도구: Read/Edit/Write/NotebookEdit 의 경로, 그리고 Bash 명령 문자열(cat/less/grep 등 우회 포함). +// +// settings.json 의 permissions.deny 는 Read 도구만 막지만, 이 훅은 Bash 우회까지 막는다. +// 반환: permissionDecision=deny 면 도구 호출이 실행 전에 거부된다. + +import { readFileSync } from "node:fs"; + +let input = {}; +try { + input = JSON.parse(readFileSync(0, "utf8") || "{}"); +} catch { + process.exit(0); // 입력 파싱 실패 시 통과(다른 가드에 위임). +} + +const tool = input.tool_name || ""; +const ti = input.tool_input || {}; + +// .project-env(단, .example 제외) 또는 .env / .env.* / ~/.env 를 가리키는 토큰. +const SECRET_RE = + /(?:^|[\s'"=(/])\.project-env(?!\.example)|(?:^|[\s'"=(/])\.env(?:\.[\w.-]+)?(?![\w])/; + +let target = ""; +switch (tool) { + case "Read": + case "Edit": + case "Write": + target = ti.file_path || ""; + break; + case "NotebookEdit": + target = ti.notebook_path || ""; + break; + case "Bash": + target = ti.command || ""; + break; + default: + process.exit(0); +} + +if (SECRET_RE.test(target)) { + process.stdout.write( + JSON.stringify({ + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: + "비밀값 파일(.project-env / .env / ~/.env)은 읽거나 수정할 수 없습니다. " + + "DB·MinIO·LiteLLM 자격증명은 런타임에 환경변수로만 주입됩니다($DATABASE_URL 등). " + + "키 구조가 궁금하면 .project-env.example 을 보세요.", + }, + }) + ); + process.exit(0); +} + +process.exit(0); diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..824af2a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "deny": [ + "Read(.project-env)", + "Read(.env)", + "Read(.env.*)", + "Read(//home/coder/.env)", + "Read(~/.env)" + ], + "allow": [ + "Bash(npm install)", + "Bash(npm run dev:*)", + "Bash(npm run start:*)", + "Bash(npm run db:check:*)", + "Bash(npm run minio:check:*)", + "Bash(npm run files:check:*)", + "Bash(podman build:*)", + "Bash(podman compose:*)" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Read|Edit|Write|NotebookEdit|Bash", + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/block-secrets.mjs\"" + } + ] + } + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bc837f1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,59 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +> 이 프로젝트에서 Claude Code가 따라야 할 규약 + +너는 사내 개발 워크스페이스에서 동작하는 코딩 에이전트다. 이 프로젝트는 **Node(ESM) 앱**이고, +**로컬 개발/테스트는 podman**, **배포는 Coolify(Gitea push → Dockerfile 자동빌드)** 로 한다. + +## 절대 규칙 +- **비밀값을 코드/커밋에 넣지 마라.** DB·MinIO 자격증명은 항상 환경변수에서만 읽는다. + 값은 프로젝트의 `.project-env` 에 있고(프로젝트 폴더 cd 시 셸이 자동 export), 이 파일은 **절대 커밋 금지**(`.gitignore`에 있음). 참고용 예시는 `.project-env.example`. +- 컨테이너는 `docker` 명령으로 실행해도 되지만 **실제 런타임은 rootless podman**이다(`docker`는 podman 별칭). +- 운영 배포를 직접 하지 마라. 배포는 **git push → Coolify**가 담당한다. 너는 코드/Dockerfile만 책임진다. + +## 하네스 가드 (자동 적용 — `.claude/`) +- `.claude/settings.json` + `.claude/hooks/block-secrets.mjs` 가 **PreToolUse 훅**으로 비밀값 파일 접근을 + 하드 차단한다: `.project-env`, `.env`, `.env.*`, `~/.env`. Read/Edit/Write 뿐 아니라 **Bash 우회** + (`cat`/`less`/`grep` 등 명령 문자열)도 막힌다. 키 구조 참고가 필요하면 `.project-env.example` 만 보면 된다. +- 값을 직접 들여다보려 하지 말고, 항상 런타임 환경변수(`$DATABASE_URL`, `$S3_*`)와 `src/config.js` 를 통해서만 쓴다. +- 자주 쓰는 명령(`npm install`, `npm run dev|start|db:check|minio:check`, `podman build|compose`)은 미리 허용돼 있다. + +## 아키텍처 한눈에 +- 외부 연결정보는 **`src/config.js` 한 곳**에서만 env를 읽는다 → `src/db.js`(pg Pool, `pingDb`), + `src/s3.js`(S3Client, `pingS3`)가 이를 소비. `src/server.js`(Express)가 `/healthz` `/db` `/s3` 로 노출. +- `scripts/check-db.js` / `check-minio.js` 는 같은 `pingDb`/`pingS3` 를 CLI로 호출하는 얇은 래퍼다. +- DB는 직원 전용 schema가 `search_path` 에 고정돼 격리된다. S3(MinIO)는 `forcePathStyle: true` 필수. + +## 외부 연결 (프로젝트 `.project-env` 에서 env로 주입됨) +- **Postgres**: `DATABASE_URL` (직원 전용 schema로 격리, `search_path` 고정). 코드는 `src/db.js`를 통해 접근. +- **MinIO(S3)**: API endpoint `https://minio.bokdev.in` (콘솔 `minioc`가 아님), 버킷 `coolify-user-data`. + 키는 `.project-env`의 `S3_ACCESS_KEY_ID`/`S3_SECRET_ACCESS_KEY`. 코드는 `src/s3.js`를 통해 접근. +- **LiteLLM**: AI 호출은 사내 gateway(`https://litellm.bok.or.kr`) 경유. virtual key는 워크스페이스 전역인 `~/.env`(`LITELLM_KEY`)에서 관리 — `update-litellm-key` 명령으로 입력. (프로젝트 설정 아님) + +## 로컬 개발 워크플로 (podman) +```bash +# 이 폴더는 new-project 로 만들어졌고 .project-env(DB/S3)가 이미 채워져 있다. +# 폴더에 들어와 있으면 셸이 .project-env 를 자동 export 한 상태다($DATABASE_URL 등 사용 가능). +npm install +npm run dev # node --watch 로 핫리로드, http://localhost:3000 + +# 컨테이너로 동일하게 돌려보기 +podman build -t sample . +podman run --rm -p 3000:3000 --env-file .project-env sample +# 또는 +podman compose up --build +``` +연결 확인: `npm run db:check`, `npm run minio:check`, 또는 `/db` `/s3` 엔드포인트. + +## 배포 워크플로 (Coolify) +1. Gitea repo에 push (`main` 등 배포 브랜치). +2. Coolify가 이 **Dockerfile**로 자동 빌드/배포한다(로컬 podman 빌드와 동일 Dockerfile). +3. Coolify 쪽 Environment Variables 에 `DATABASE_URL`, `S3_*` 를 채운다(로컬 `.project-env`와 같은 키). +4. 포트는 컨테이너 `3000`. Coolify에서 도메인/포트 매핑. + +## 코드 규약 +- ESM(`import`/`export`), Node 22+. 외부 연결정보는 반드시 `src/config.js` 한 곳을 거친다. +- 새 외부 의존성을 추가하면 `package.json`에 반영하고 `npm install`로 lock 갱신. +- 변경 후에는 `npm run db:check` / `npm run minio:check`로 연결이 살아있는지 스스로 확인하라.