chore(harness): 비밀 파일 읽기 차단 가드 + 규약 문서화

- .claude/hooks/block-secrets.mjs: PreToolUse 훅으로 비밀값 파일 접근 하드 차단
  (Read/Edit/Write + Bash cat/less 우회까지). example 파일은 허용.
- .claude/settings.json: permissions.deny + 자주 쓰는 dev 명령 allow + 훅 연결
- CLAUDE.md: init 표준 헤더, 하네스 가드/아키텍처 섹션 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 08:01:46 +00:00
parent 21ace490e5
commit 3252dfaa66
3 changed files with 154 additions and 0 deletions

60
.claude/hooks/block-secrets.mjs Executable file
View File

@@ -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);

35
.claude/settings.json Normal file
View File

@@ -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\""
}
]
}
]
}
}

59
CLAUDE.md Normal file
View File

@@ -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`로 연결이 살아있는지 스스로 확인하라.