coredot.today
에이전트 여럿, 저장소 하나 6편 — 에이전트가 길을 잃지 않는 저장소
블로그로 돌아가기
Gitgit worktreeAI 에이전트Claude CodeCodex병렬 개발AGENTS.mdCLAUDE.mdmiseGit LFSpre-commit 훅

에이전트 여럿, 저장소 하나 6편 — 에이전트가 길을 잃지 않는 저장소

에이전트는 매번 기억 없이 새로 시작하고, Mac은 저마다 설치된 버전이 다릅니다. 규칙은 AGENTS.md와 CLAUDE.md(@AGENTS.md)에, 도구 버전과 명령은 mise.toml에, 비밀은 .env.example 견본에, 큰 파일은 LFS·S3에, 안전장치는 pre-commit 훅·CI·main 보호에 담아 저장소 자체가 규칙과 환경을 들고 다니게 만드는 법을 실제 실행 결과와 함께 정리합니다. 마지막엔 새 Mac에서 다섯 줄로 똑같은 환경을 되살립니다.

코어닷투데이2026-10-1288분

길을 잃지 않는 저장소크게 보기

들어가며: "테스트는 어떻게 돌려요?"를 세 번째 듣던 날

민지는 요즘 cafe-menu 주문 웹앱을 Claude Code와 Codex에게 번갈아 맡깁니다. 맥북에서는 Claude Code가 결제 화면을 고치고, 사무실 맥 스튜디오에서는 Codex가 메뉴 검색을 만듭니다. 2편과 3편에서 worktree로 작업 폴더를 나눴고, 4편에서는 Mac 사이를 push와 fetch로 오가는 법을 익혔습니다. 여기까지 오니 코드 충돌은 거의 사라졌습니다.

대신 다른 종류의 일이 반복됐습니다. 새 세션을 열 때마다 에이전트가 묻습니다. "테스트는 npm test인가요, npx vitest인가요?" 한 번은 Codex가 main에 바로 커밋하려 했고, 한 번은 Claude Code가 .env를 읽어서 로그에 키 일부를 찍었습니다. 가장 황당했던 건 맥북에서 통과한 테스트가 맥 스튜디오에서 깨진 날입니다. 원인은 코드가 아니라 두 Mac에 깔린 node 버전이 달랐다는 것이었습니다.

도윤의 진단은 짧았습니다. "에이전트가 멍청한 게 아니라, 저장소가 아무것도 말해 주지 않아서 그래요. 규칙은 민지 씨 머릿속에, 버전은 각 Mac에, 비밀은 슬랙 DM에 흩어져 있잖아요."

이번 편의 목표는 그 흩어진 것들을 저장소 안의 파일로 옮기는 것입니다. 시리즈의 세 번째 원칙, "개발환경 = 저장소 안의 파일(mise.toml·AGENTS.md)로 재현"을 실제로 만드는 편입니다. 다 만들고 나면 새 Mac에서 다섯 줄만 치면 맥북과 똑같은 환경이 되살아나고, 어떤 에이전트가 들어와도 같은 규칙을 읽게 됩니다.

📌
이 글의 기준 (2026년 9월) — Claude Code 설명은 공식 문서 code.claude.com/docs(memory · permissions · worktrees)를, Codex 설명은 OpenAI 공식 문서(learn.chatgpt.com의 AGENTS.md 안내)를 확인한 내용만 담았습니다. mise 2026.9.14, git 2.55, git-lfs 3.3.0의 출력은 모두 직접 실행한 결과입니다. 재현은 스크래치 폴더에서 가짜 원격(git init --bare)과 두 개의 clone으로 "맥북"과 "맥 스튜디오"를 흉내 냈고, mise는 데이터·설정·캐시 폴더를 전부 임시 폴더로 돌려 실제 컴퓨터 설정을 건드리지 않았습니다. 출력 속 로컬 경로는 ~/cafe-menu나 https://github.com/minji/cafe-menu.git로 바꿔 적었습니다(해시와 구조는 그대로).

1. 왜 저장소가 규칙과 환경을 들고 다녀야 하나

사람 개발자는 한 번 들은 규칙을 기억합니다. "우리 팀은 main에 직접 push 안 해", "테스트는 커밋 전에 꼭". 에이전트는 다릅니다. 세션마다 빈 머리로 시작합니다. Claude Code 공식 문서도 첫 문장부터 "각 세션은 새 컨텍스트 창으로 시작한다"고 적고, 세션을 넘어 지식을 전하는 수단으로 CLAUDE.md 같은 파일을 소개합니다. 에이전트가 기억하는 건 그 폴더에 있는 파일뿐입니다.

여기에 Mac이 두 대가 되면 문제가 하나 더 붙습니다. 실제로 이번 재현을 하면서 본 장면입니다. 같은 폴더에서 그냥 node -v를 치면 Homebrew로 깔린 시스템 node가, mise exec -- node -v를 치면 저장소가 정한 node가 나옵니다.

bash
node -v
mise exec -- node -v
text
v24.12.0
v24.21.0

둘 다 "node 24"지만 패치 버전이 다릅니다. 이 정도 차이로 테스트가 깨지는 일은 드물어도, 메이저 버전이 다르거나 python이 3.12와 3.13으로 갈리면 이야기가 달라집니다. 그리고 에이전트는 "이 Mac의 node가 몇이지?"를 궁금해하지 않습니다. 그냥 PATH에 먼저 걸린 것을 씁니다.

그래서 필요한 건 사람의 기억과 각 Mac의 상태에 기대던 것을, 저장소의 파일로 옮기는 일입니다. 파일로 옮기면 세 가지가 동시에 해결됩니다.

🧠
문제 — 규칙은 머릿속, 버전은 Mac마다, 비밀은 DM에
에이전트는 세션마다 추측합니다. 테스트 명령, 브랜치 규칙, node 버전, 필요한 환경변수 이름까지. 추측은 대개 맞지만, 틀리는 날은 꼭 급한 날입니다.
📁
해결 — 저장소가 들고 다니게
규칙은 AGENTS.md·CLAUDE.md, 버전과 명령은 mise.toml·mise.lock, 비밀의 "자리"는 .env.example, 큰 파일은 LFS·S3, 안전장치는 훅·CI·보호 규칙. 전부 git으로 함께 움직입니다.
🔁
결과 — 어느 Mac, 어느 에이전트든 같은 출발선
git clone 한 번이면 규칙과 환경이 같이 옵니다. 에이전트가 환경을 추측하지 않고, 사람은 같은 설명을 반복하지 않습니다.

이번 편에서 cafe-menu에 더할 파일을 한 장으로 먼저 보면 이렇습니다. 각 파일을 누가 읽는지, 무엇을 막는지가 핵심입니다.

파일누가 읽나막아 주는 사고커밋?
AGENTS.mdCodex 등 대부분의 에이전트, 사람에이전트마다 규칙·명령을 추측예
CLAUDE.mdClaude Code (@AGENTS.md로 공통 규칙 가져옴)규칙이 두 파일로 갈라져 어긋남예
mise.toml · mise.lockmise, CI, 에이전트(명령 목록)Mac마다 다른 도구 버전, "테스트 어떻게 돌려요?"예
.env.example사람, 에이전트필요한 환경변수를 몰라 값을 지어냄예
.env앱, mise—절대 아니오
.gitignore · .worktreeincludegit, Claude Code비밀 커밋, 새 worktree에 .env 없음예
.gitattributes (LFS)git-lfs큰 바이너리가 기록을 불림예
.githooks/pre-commit · ci.ymlgit, GitHub Actions비밀·큰 파일·깨진 코드가 들어감예

2. AGENTS.md — 모든 에이전트가 함께 읽는 안내문

하나의 규칙서를 함께 읽는 두 로봇크게 보기

AGENTS.md는 코딩 에이전트를 위한 README라고 보면 됩니다. README가 사람에게 "이 프로젝트는 이렇게 쓰세요"라고 알려 준다면, AGENTS.md는 에이전트에게 "빌드는 이렇게, 테스트는 이렇게, 이건 하지 마"를 알려 줍니다. 형식은 그냥 마크다운이고 필수 항목도 없습니다.

공식 사이트(agents.md)는 이 형식을 "코딩 에이전트를 안내하는 단순하고 열린 형식"이라고 소개하고, 2026년 9월 기준 6만 개가 넘는 오픈소스 프로젝트가 쓰고 있으며 현재는 리눅스 재단 산하 Agentic AI Foundation이 관리한다고 밝힙니다. 지원 도구 목록에는 OpenAI Codex, GitHub Copilot, Cursor, Google Jules, Aider, Zed, Devin 등 스물다섯 개 넘는 이름이 올라 있습니다. 사이트가 권하는 내용은 프로젝트 개요, 빌드·테스트 명령, 코드 스타일, 테스트 방법, 보안 주의사항입니다.

2-1. Codex는 AGENTS.md를 어떻게 찾나

Codex 공식 문서에 적힌 탐색 규칙은 꽤 구체적입니다. 순서대로 보면 이렇습니다.

1 · 전역
Codex 홈 폴더(기본 ~/.codex, CODEX_HOME으로 바꿀 수 있음)에서 AGENTS.override.md가 있으면 그것을, 없으면 AGENTS.md를 읽습니다. 이 단계에서는 비어 있지 않은 첫 파일 하나만 씁니다.
2 · 프로젝트
프로젝트 루트(보통 Git 루트)에서 시작해 지금 작업 폴더까지 한 단계씩 내려가며 각 폴더에서 AGENTS.override.md → AGENTS.md → 설정한 대체 파일 이름 순으로 찾습니다. 폴더마다 최대 한 파일만 씁니다.
3 · 합치기
찾은 파일을 루트에서 아래 방향으로 빈 줄을 사이에 두고 이어 붙입니다. 뒤에 붙은 것(작업 폴더에 가까운 것)이 앞의 규칙을 덮어쓰는 효과를 냅니다.
4 · 한도
합친 크기가 project_doc_max_bytes(기본 32 KiB)에 닿으면 더 붙이지 않습니다. 빈 파일은 건너뜁니다. 탐색은 실행당 한 번, TUI에서는 보통 세션을 띄울 때 한 번입니다.

~/.codex/config.toml에서 두 설정을 바꿀 수 있다고 문서가 예시를 보여 줍니다. 대체 파일 이름을 추가하거나(예: 이미 TEAM_GUIDE.md를 쓰는 팀), 한도를 늘리는 식입니다.

toml
# ~/.codex/config.toml (문서 예시)
project_doc_fallback_filenames = ["TEAM_GUIDE.md", ".agents.md"]
project_doc_max_bytes = 65536

어떤 파일이 실제로 읽혔는지 확인하려면, 문서는 Codex에게 "지금 지시사항을 요약해 달라"고 물어보는 방법을 안내합니다. 이 글에서는 에이전트 세션을 실제로 띄우지 않았으므로 그 출력은 싣지 않습니다.

🧩
모노레포라면 폴더마다 AGENTS.md — agents.md 사이트는 "편집하는 파일에 가장 가까운 AGENTS.md가 이기고, 사용자가 채팅에서 한 지시가 모든 것을 이긴다"고 설명합니다. 결제 모듈만 규칙이 까다롭다면 services/payments/AGENTS.md를 따로 두면 됩니다. Codex 문서는 같은 폴더에 AGENTS.override.md를 두어 상위 규칙을 대체하는 예도 보여 줍니다. 다만 Claude Code는 AGENTS.override.md를 읽지 않습니다(아래 3장). 두 에이전트에 똑같이 먹히는 규칙은 AGENTS.md에 두세요.

2-2. cafe-menu의 AGENTS.md

민지와 도윤이 합의한 AGENTS.md는 이렇습니다. 짧고, 확인할 수 있는 문장만 넣었습니다. "코드를 깔끔하게"보다 "커밋 전에 mise run test가 통과해야 한다"가 에이전트에게 훨씬 잘 먹힙니다.

markdown
# AGENTS.md — cafe-menu

카페 주문 웹앱. 사람과 모든 코딩 에이전트(Claude Code, Codex 등)가 함께 읽는 규칙입니다.

## 환경
- 도구 버전은 `mise.toml`이 정한다. 전역 node/python을 쓰지 말고 `mise run <task>`로 실행한다.
- 처음 한 번: `mise install` → `mise run install` → `cp .env.example .env`

## 명령
- 테스트: `mise run test` (커밋 전에 반드시 통과)
- 문법 검사: `mise run lint`
- 개발 서버: `mise run dev` (포트 5173)

## 구조
- `src/order.js` 주문 계산 로직, `test/` 단위 테스트, `menu.md` 메뉴 원본

## 규칙
- 한 작업 = 한 브랜치. `main`에 직접 커밋·push하지 않는다.
- `.env`와 비밀 값은 읽거나 커밋하지 않는다. 새 환경변수는 `.env.example`에 이름만 추가한다.
- 새 의존성을 추가하기 전에 사람에게 먼저 묻는다.
- 큰 파일(10MB 이상, 데이터셋·모델)은 커밋하지 않는다. 위치(`MENU_DATA_URL`)만 코드에 둔다.

눈여겨볼 점이 하나 있습니다. 명령을 npm test가 아니라 mise run test로 적었습니다. 이러면 에이전트가 어느 Mac에서 실행하든 mise가 정한 node 버전으로 돌아갑니다. 이 부분은 4장에서 자세히 봅니다.


3. CLAUDE.md — @AGENTS.md 한 줄로 원본을 하나로

Claude Code는 원래 CLAUDE.md라는 자기만의 지시 파일을 읽습니다. 그렇다면 AGENTS.md와 CLAUDE.md를 둘 다 써야 할까요? 공식 문서(memory 페이지)를 확인해 보면 답이 생각보다 깔끔합니다.

3-1. Claude Code는 AGENTS.md를 직접 읽나

읽습니다. 단, 조건이 있습니다. 공식 문서에 따르면 Claude Code는 v2.1.277부터 AGENTS.md를 프로젝트 지시사항으로 직접 읽을 수 있습니다. 그런데 기본 설정에서는 작업 폴더나 그 위에 CLAUDE.md가 없을 때만 AGENTS.md를 읽습니다. 문서의 표를 옮기면 이렇습니다.

저장소에 있는 것Claude Code가 읽는 것 (기본값)
AGENTS.md만 있고, CLAUDE.md·CLAUDE.local.md는 없음AGENTS.md
AGENTS.md와 CLAUDE.md(또는 CLAUDE.local.md)가 함께 있음CLAUDE.md만 (AGENTS.md는 안 읽음)
AGENTS.md를 import하는 CLAUDE.mdCLAUDE.md + import로 들어온 AGENTS.md

두 번째 줄이 함정입니다. AGENTS.md를 잘 써 놓고, Claude 전용 메모 몇 줄을 CLAUDE.md에 따로 적는 순간 Claude Code는 AGENTS.md를 보지 않게 됩니다. 문서는 또 Claude Code가 읽지 않는 파일도 분명히 적어 둡니다. AGENTS.local.md, AGENTS.override.md, .agents/ 폴더 아래의 파일은 읽지 않습니다.

기본 동작은 세션 안의 /config에서 Project instructions 값을 바꿔 조정할 수 있습니다(claude-md-or-agents-md가 기본, claude-md-and-agents-md로 둘 다 읽기 등). 다만 이 설정은 사용자 설정(~/.claude/settings.json)이나 관리형 설정에서만 먹히고 프로젝트 설정 파일에서는 무시된다고 문서에 적혀 있습니다. 즉 저장소에 커밋해서 팀 전체에 강제할 수는 없습니다. 그래서 저장소 쪽에서 확실하게 하려면 다음 방법을 씁니다.

3-2. 권장 구조: CLAUDE.md는 @AGENTS.md + Claude 전용 메모

CLAUDE.md는 @경로 문법으로 다른 파일을 가져올(import) 수 있습니다. 문서가 밝힌 규칙은 이렇습니다.

  • 상대 경로와 절대 경로 모두 됩니다. 상대 경로는 작업 폴더가 아니라 import를 적은 파일 기준으로 풀립니다.
  • 가져온 파일이 또 다른 파일을 가져올 수 있고, 최대 네 단계까지 따라갑니다.
  • 백틱(`)으로 감싼 코드 안의 @경로는 import로 보지 않습니다. 경로를 글자 그대로 쓰고 싶을 때 쓰는 요령입니다.
  • 저장소 바깥 파일(예: 홈 폴더)을 가져오면 처음 한 번 승인 창이 뜹니다. 남이 커밋한 파일이 내 컴퓨터의 파일을 몰래 끌어오지 못하게 하려는 장치입니다.

그리고 문서의 "다른 코딩 도구와 파일 하나를 공유하기" 절이 바로 우리가 원하는 구조를 권합니다. CLAUDE.md 첫 줄에 @AGENTS.md를 두고, 그 아래에 Claude 전용 지시를 덧붙이라는 것입니다. Claude는 가져온 AGENTS.md를 먼저 읽고 나머지를 읽습니다. 문서에 따르면 이 import는 설정값이 무엇이든 AGENTS.md를 두 번 읽게 만들지 않습니다.

cafe-menu의 CLAUDE.md는 이렇게 짧습니다.

markdown
@AGENTS.md

## Claude Code 전용
- push는 사람이 한다. 커밋까지만 하고 멈춘다.
- 큰 변경은 plan 모드로 계획부터 보여 준다.

이렇게 두면 역할이 분명해집니다.

AGENTS.md
공통 규칙의 유일한 원본
↓
Codex
AGENTS.md를 직접 읽음
CLAUDE.md
@AGENTS.md + Claude 전용 2줄
Cursor · Copilot 등
AGENTS.md 지원 도구
↓
Claude Code
CLAUDE.md를 읽으며 AGENTS.md도 함께
🔗
심볼릭 링크(ln -s AGENTS.md CLAUDE.md)는요? 문서도 Claude 전용 내용이 없다면 심볼릭 링크로 충분하다고 합니다. 다만 두 가지를 짚습니다. Claude의 Edit·Write 도구는 링크를 통해 쓰기를 거부하고 원본(AGENTS.md)을 고치라고 안내한다는 점, 그리고 저장소를 Windows에서 clone하는 사람이 있으면 링크가 한 줄짜리 텍스트 파일로 풀릴 수 있다는 점입니다. Claude 전용 메모를 둘 거라면 import 방식이 낫습니다.

3-3. 파일 위치와 CLAUDE.local.md

CLAUDE.md는 여러 곳에 둘 수 있고, 넓은 범위부터 차례로 읽혀 서로 덮어쓰지 않고 이어 붙습니다.

범위위치누구와 공유
조직 관리 정책macOS: /Library/Application Support/ClaudeCode/CLAUDE.md그 컴퓨터의 모든 사용자
사용자~/.claude/CLAUDE.md나 (모든 프로젝트)
프로젝트./CLAUDE.md 또는 ./.claude/CLAUDE.md팀 (git으로)
로컬./CLAUDE.local.md (.gitignore에 추가)나 (이 프로젝트만)

작업 폴더와 그 위 폴더들의 CLAUDE.md는 시작할 때 읽고, 하위 폴더의 CLAUDE.md는 Claude가 그 폴더의 파일을 읽을 때 불러옵니다. 문서는 CLAUDE.md 하나를 200줄 이내로 유지하라고 권합니다. 길어질수록 컨텍스트를 많이 먹고 지키는 정도가 떨어진다는 이유입니다. 특정 파일에만 해당하는 규칙은 .claude/rules/ 폴더에 paths 머리말을 붙여 나누면, 그 파일을 만질 때만 불러옵니다.

CLAUDE.local.md는 나만의 메모(내 샌드박스 URL, 선호하는 테스트 데이터)를 두는 곳입니다. 여기서 두 가지 주의가 있습니다.

  • CLAUDE.local.md도 "CLAUDE.md가 있다"로 친다. AGENTS.md만 믿고 CLAUDE.md를 두지 않은 저장소에서 개인 메모용 CLAUDE.local.md를 만들면, 그 순간 Claude가 AGENTS.md를 읽지 않게 됩니다(문서가 직접 경고하는 내용). 우리처럼 CLAUDE.md에 @AGENTS.md를 두면 이 문제가 없습니다.
  • worktree를 건너가지 않는다. gitignore된 CLAUDE.local.md는 만든 worktree에만 있습니다. 문서는 여러 worktree에서 같은 개인 메모를 쓰려면 홈 폴더의 파일을 @~/.claude/my-project-instructions.md처럼 import하라고 안내합니다.

제대로 읽혔는지는 세션 안에서 /memory나 /context를 열어 Memory files 목록을 보면 됩니다.

3-4. 직접 만들어 보기

아래 도구에 프로젝트 이름과 명령, 규칙을 넣으면 AGENTS.md와 CLAUDE.md 한 쌍을 만들어 줍니다. Codex 기본 한도(32 KiB)와 CLAUDE.md 권장 길이(200줄)도 함께 보여 줍니다.

🧱
AGENTS.md·CLAUDE.md는 "부탁"이지 "잠금"이 아닙니다. Claude Code 문서는 CLAUDE.md를 강제 설정이 아니라 컨텍스트로 다룬다고 분명히 말하고, Claude의 판단과 무관하게 막아야 하는 동작은 훅이나 권한 규칙으로 막으라고 합니다. "main에 push하지 마"를 적었다고 끝이 아닙니다. 7장의 안전장치까지 함께 거세요.

4. mise.toml — 도구 버전과 명령을 저장소에

같은 레시피로 같은 블록을 쌓는 두 컴퓨터크게 보기

규칙을 파일로 옮겼으니 이제 환경입니다. mise는 프로젝트마다 필요한 도구(node, python, go 등)의 버전을 설치하고, 환경변수를 설정하고, 자주 쓰는 명령(task)을 실행해 주는 도구입니다. nvm과 pyenv와 make를 한 파일로 합쳤다고 생각하면 가깝습니다. 모든 설정은 저장소 루트의 mise.toml에 들어갑니다.

에이전트 입장에서 mise.toml의 가치는 한 문장입니다. AI가 환경을 추측하지 않게 합니다. "이 프로젝트는 node 몇이지?"와 "테스트는 무슨 명령이지?"의 답이 한 파일에 적혀 있고, mise run test 한 줄이 그 답을 그대로 실행합니다.

4-1. [tools]와 [tasks] 쓰기

cafe-menu의 mise.toml입니다. 주문 화면은 node, 메뉴 통계 스크립트는 python을 씁니다.

toml
# 이 저장소에서 쓰는 도구 버전 — 어느 Mac, 어느 에이전트든 같은 버전을 쓴다
[tools]
node = "24"
python = "3.13"

# 자주 쓰는 명령에 이름을 붙인다 — "테스트는 어떻게 돌려요?"의 답이 한 곳에 있다
[tasks.install]
description = "의존성 설치 + git 훅 연결"
run = [
  "git config core.hooksPath .githooks",
  "npm install",
]

[tasks.dev]
description = "개발 서버 (http://localhost:5173)"
run = "npm run dev"

[tasks.test]
description = "단위 테스트"
run = "npm test"

[tasks.lint]
description = "문법 검사"
run = "npm run lint"

[tasks.stats]
description = "메뉴 가격 통계 (파이썬)"
run = "python scripts/menu_stats.py"

# .env가 있으면 읽어서 환경변수로 넣는다
[env]
_.file = { path = ".env", redact = true }

install 태스크에 git config core.hooksPath .githooks가 들어 있는 이유는 7장에서 설명합니다. 지금은 "설치할 때 훅도 같이 연결한다"로만 알아 두세요.

4-2. mise install — 버전 맞추기

mise install은 mise.toml의 [tools]에 적힌 것을 전부 설치합니다. "24"는 "24로 시작하는 최신 버전"이라는 뜻이라, 2026년 9월 말 기준 24.21.0이 골라졌습니다(진행 표시 줄은 생략).

bash
mise install
text
mise node@24.21.0 v24.21.0
mise node@24.21.0 11.19.0
mise ✓ node@24.21.0    6.3s  node-v24.21.0-darwin-arm64.tar.gz
mise python@3.13.15 Python 3.13.15
mise ✓ python@3.13.15  6.6s  cpython-3.13.15+20260924-aarch64-apple-darwin-install_only_stripped.tar.gz
mise ████████████████ 2/2 · installed 2 tools in 9.6s

두 런타임을 받는 데 10초가 채 안 걸렸습니다. 설치된 도구는 mise 자기 폴더에 들어가고, Homebrew로 깔린 시스템 node는 건드리지 않습니다. mise ls로 무엇이 어디서 정해졌는지 볼 수 있습니다.

bash
mise ls
text
node    24.21.0  ~/cafe-menu/mise.toml  24
python  3.13.15  ~/cafe-menu/mise.toml  3.13

4-3. mise tasks · mise run — 명령 목록이 곧 문서

mise tasks는 저장소가 제공하는 명령을 설명과 함께 보여 줍니다. 에이전트에게 "이 저장소에서 쓸 수 있는 명령은 mise tasks로 봐"라고 한 줄만 알려 주면, 에이전트가 README를 뒤지지 않아도 됩니다.

bash
mise tasks
text
dev      개발 서버 (http://localhost:5173)
install  의존성 설치 + git 훅 연결
lint     문법 검사
stats    메뉴 가격 통계 (파이썬)
test     단위 테스트

mise run test는 mise가 고른 node로 npm test를 실행합니다.

bash
mise run test
text
[test] $ npm test

> test
> node --test

✔ 아메리카노 2잔 + 라테 1잔 (0.455625ms)
✔ 없는 메뉴는 에러 (0.127125ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 61.731333

python 태스크도 같은 방식입니다. 출력 끝의 python=3.13.15는 스크립트가 직접 찍은 버전으로, 시스템 python(3.14)이 아니라 mise.toml이 정한 버전으로 돌았다는 증거입니다.

bash
mise run stats
text
[stats] $ python scripts/menu_stats.py
items=2 avg=4750 python=3.13.15
🐚
셸에서 그냥 node를 쳐도 mise 버전이 나오게 하려면 — 1장의 node -v가 시스템 버전(24.12.0)을 보여 준 건 셸에 mise를 "활성화"하지 않았기 때문입니다. mise activate --help에 따르면 ~/.zshrc에 eval "$(mise activate zsh)" 한 줄을 넣으면 폴더를 옮길 때마다 그 폴더의 mise.toml 버전이 켜집니다. 셸 설정을 건드리고 싶지 않다면 mise run이나 mise exec -- 명령만 써도 됩니다. 스크립트와 CI에는 오히려 mise exec가 권장됩니다. 에이전트에게는 AGENTS.md에 "항상 mise run으로"라고 적어 두는 쪽이 가장 확실합니다.

4-4. mise.lock — "24"가 정확히 무엇이었는지 적어 두기

node = "24"는 편하지만, 한 달 뒤 새 Mac에서 mise install을 하면 24.22.0이 깔릴 수 있습니다. 그걸 막는 게 잠금 파일(lockfile) mise.lock입니다. mise 문서에 따르면 잠금 파일은 기본으로 새로 만들어지지 않고(이미 있으면 갱신만), mise lock으로 직접 만들거나 설정에서 lockfile = true를 켜야 합니다.

bash
mise lock
text
→ Targeting 7 platform(s) for ~/cafe-menu/mise.lock: linux-arm64, linux-arm64-musl, linux-x64, linux-x64-musl, macos-arm64, macos-x64, windows-x64
→ Processing 2 tool(s): node@24.21.0, python@3.13.15
mise lock          ✓ 14 platform entries
✓ Updated 14 platform entries (0 skipped)
✓ Lockfile written to ~/cafe-menu/mise.lock

(플랫폼별 진행 줄 14개는 생략했습니다.) 만들어진 파일 앞부분을 보면, 요청한 "24"가 실제로 24.21.0이었다는 기록과 함께 플랫폼마다 다운로드 주소와 체크섬이 들어 있습니다.

toml
# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html

lockfile_version = 2

[[tools.node]]
version = "24.21.0"
backend = "core:node"
specifiers = ["24"]

[tools.node."platforms.macos-arm64"]
checksum = "sha256:bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057"
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-arm64.tar.gz"

리눅스 항목까지 들어 있으니 GitHub Actions의 우분투 러너도 같은 버전을 받습니다. mise 문서는 mise.toml과 mise.lock을 함께 커밋하라고 권합니다. 설치할 때 mise install --locked를 쓰면 잠금 파일에 해당 플랫폼 주소가 없을 때 조용히 새 버전을 고르지 않고 실패합니다. 이번 재현에서 새 폴더에 --locked로 설치했을 때 문제없이 끝났습니다. 버전을 올리고 싶을 때는 mise lock --bump(잠금 파일만 최신으로)나 mise upgrade를 씁니다.

4-5. mise trust — 남이 만든 설정 파일을 믿을 것인가

mise.toml은 명령을 실행하고 환경변수를 바꿀 수 있습니다. 그래서 mise는 처음 보는 설정 파일을 신뢰(trust)할지 확인합니다. 맥 스튜디오를 흉내 낸 새 clone에서 mise tasks를 치자 이렇게 멈췄습니다.

bash
git clone https://github.com/minji/cafe-menu.git && cd cafe-menu
mise tasks
text
mise ERROR error parsing config file: ~/cafe-menu/mise.toml
mise ERROR Config files in ~/cafe-menu/mise.toml are not trusted.
Trust them with `mise trust`. See https://mise.jdx.dev/cli/trust.html for more information.
mise ERROR Version: 2026.9.14 macos-arm64 (2026-09-25)
mise ERROR Run with --verbose or MISE_VERBOSE=1 for more information

(대화형 터미널이었다면 오류 대신 "믿겠습니까?" 확인 창이 떴을 것입니다. 재현은 비대화형이라 오류로 끝났습니다.) 내용을 확인하고 mise trust를 치면 풀립니다.

bash
mise trust
text
mise trusted ~/cafe-menu

mise trust --help를 보면 규칙이 조금 더 세밀합니다.

  • [tools]에 버전 문자열만 있고 [tasks]에 템플릿이 없는 "안전한" 설정은 신뢰가 필요 없습니다. cafe-menu가 확인을 요구한 건 [env]에서 .env를 읽기 때문입니다.
  • 일반 모드에서는 mise install, mise run, mise exec가 실행할 때 설정을 자동으로 신뢰합니다. 실제로 다른 새 clone에서 mise trust 없이 mise install부터 치자 설치가 진행됐고, 그 뒤 mise trust --show는 trusted로 바뀌어 있었습니다.
  • 신뢰는 git worktree끼리 공유됩니다. 메인 checkout을 신뢰했으면 같은 저장소의 linked worktree도 신뢰된 것으로 봅니다. 에이전트마다 worktree를 만드는 이 시리즈의 방식과 잘 맞습니다.
🔐
"자동 신뢰"는 편의이지 검사가 아닙니다. 모르는 사람의 저장소를 clone하자마자 mise install이나 mise run을 치면, 그 저장소의 mise.toml을 믿고 실행한다는 뜻입니다. 내 팀 저장소라면 괜찮지만, 처음 보는 저장소라면 먼저 cat mise.toml로 태스크와 [env]를 읽어 보세요. 에이전트에게 모르는 저장소의 설치를 맡길 때도 마찬가지입니다.

5. 비밀: .env.example은 커밋, .env는 절대

API 키 같은 비밀 값은 저장소에 넣지 않습니다. 이건 기존 시리즈 2편에서 .gitignore로 다룬 원칙 그대로입니다. 에이전트 시대에 달라진 점은 두 가지입니다. 에이전트는 git add -A를 망설임 없이 치고, 새 worktree와 새 Mac이 자주 생깁니다. 그래서 비밀 그 자체는 빼되, 비밀의 "자리"는 저장소에 남겨야 합니다.

파일내용git
.env.example필요한 변수의 이름과 비밀이 아닌 기본값커밋
.env진짜 값 (API 키, 토큰).gitignore로 제외
비밀번호 관리자 · 팀 금고진짜 값의 원본git 밖

cafe-menu의 .env.example과 .gitignore입니다.

bash
# .env.example — 이 파일을 .env로 복사한 뒤 값을 채우세요:  cp .env.example .env
PAYMENT_API_KEY=
ORDER_WEBHOOK_URL=http://localhost:5173/webhook
MENU_DATA_URL=s3://cafe-menu-data/menu/2026-10/
bash
# .gitignore
# 비밀 — 절대 커밋하지 않는다 (견본은 .env.example)
.env
.env.*
!.env.example

# 개인 메모 — 에이전트용 개인 지시문
CLAUDE.local.md

# 다시 만들 수 있는 것
node_modules/
dist/
.DS_Store

# 큰 데이터는 저장소 밖(S3·MinIO)에 둔다
data/raw/

.env.*로 .env.local, .env.production 같은 변형까지 막고, !.env.example로 견본만 예외로 풀었습니다. 규칙이 제대로 걸렸는지는 .env를 담아 보면 압니다.

bash
git add .env
text
The following paths are ignored by one of your .gitignore files:
.env
hint: Use -f if you really want to add them.
hint: Disable this message with "git config set advice.addIgnoredFile false"

그런데 힌트가 말하듯 -f를 붙이면 뚫립니다. 이 구멍은 7장의 pre-commit 훅이 막습니다.

5-1. mise가 .env를 읽게 하면 좋은 점

4장의 mise.toml 끝에 넣은 [env] _.file = ...은 .env를 읽어 환경변수로 넣으라는 뜻입니다. 이러면 mise run dev로 띄운 앱이 따로 dotenv 라이브러리 없이도 값을 받습니다. 새 clone에서 cp .env.example .env 후 확인해 보면 이렇습니다.

bash
mise env | grep -E "MENU|PAYMENT"
text
export MENU_DATA_URL='s3://cafe-menu-data/menu/2026-10/'
export PAYMENT_API_KEY=''

재현해 보니 .env가 아직 없을 때도 mise는 오류 없이 넘어갔습니다(문서에는 이 경우의 동작이 명시돼 있지 않아, 직접 확인한 결과로만 적습니다). redact = true는 mise 문서에 따르면 태스크 출력에서 값을 가려 주는 기능일 뿐이고, 파일을 암호화하거나 자식 프로세스가 값을 읽지 못하게 하지는 않습니다.

5-2. 에이전트가 .env를 읽지 않게: 규칙 + 권한

AGENTS.md에 ".env를 읽지 않는다"고 적는 것은 부탁입니다. Claude Code에서는 권한 규칙으로 한 겹 더 막을 수 있습니다. 공식 문서(permissions)는 Read(./.env)나 Read(./secrets/**) 같은 Read deny 규칙을 예로 듭니다.

json
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(!.env.example)"
    ]
  }
}

세 번째 줄이 중요합니다. Read(./.env.*)만 두면 견본인 .env.example까지 막혀서 에이전트가 필요한 변수 이름조차 못 봅니다. 문서에 따르면 !로 시작하는 패턴은 gitignore의 부정(negation)처럼 동작해, 같은 deny 목록에서 앞에 나온 규칙으로부터 해당 경로를 빼 줍니다.

문서가 밝힌 범위도 정확히 알아 두세요. 이 규칙은 Claude의 파일 도구와, Bash에서 Claude Code가 알아보는 파일 명령(cat, head, tail, sed 등)과 리다이렉션에 적용됩니다. 하지만 파일 이름을 적지 않고 읽는 명령(예: 그 폴더에서 grep -r 패턴 .)이나 스스로 파일을 여는 Python·Node 스크립트에는 적용되지 않습니다. 운영체제 수준에서 막으려면 샌드박스를 켜라고 문서는 안내합니다. 결국 가장 확실한 방어는 진짜 비밀을 개발용 .env에 되도록 두지 않는 것입니다. 로컬에는 테스트용 키만 두고, 운영 키는 배포 환경의 비밀 저장소에 두세요.

5-3. worktree마다 .env를: .worktreeinclude

새 worktree는 새 checkout이라 gitignore된 .env가 따라오지 않습니다. 3편에서 다룬 .worktreeinclude가 이 문제를 풉니다. Claude Code 공식 문서에 따르면 저장소 루트의 이 파일은 .gitignore 문법을 쓰고, 패턴에 맞으면서 gitignore된 파일만 새 worktree에 복사합니다(추적되는 파일은 중복 복사하지 않음). --worktree로 만든 worktree, 서브에이전트 worktree, 데스크톱 앱의 병렬 세션 모두에 적용됩니다.

bash
# .worktreeinclude
.env

한 줄이면 됩니다. 이 파일 자체는 커밋해서 팀과 공유하고, 복사되는 .env는 각자 Mac에만 있습니다.


6. 큰 파일: Git LFS, 그리고 S3·MinIO

주문 웹앱에 메뉴 소개 영상(8MB)을 넣자는 얘기가 나왔습니다. 그냥 커밋하면 어떻게 될까요? git은 모든 버전을 영원히 기록하니, 영상을 세 번 고치면 저장소에 24MB가 쌓이고 모든 clone이 그걸 받습니다. 에이전트마다 worktree를 만들고 Mac마다 clone하는 우리 방식에서는 더 아픕니다.

GitHub 공식 문서는 기준선을 이렇게 적습니다. 50MiB가 넘는 파일을 추가하면 git이 경고하고, 100MiB가 넘는 파일은 GitHub가 거부하며, 저장소는 1GB 이하가 이상적이고 5GB 이하를 강하게 권합니다. 100MiB 넘는 파일을 추적하려면 Git LFS를 써야 한다고 안내합니다.

6-1. Git LFS 기본

Git LFS(Large File Storage)는 큰 파일의 내용은 별도 저장소에 올리고, git 기록에는 포인터(130바이트 안팎의 작은 텍스트)만 남기는 확장입니다. 이 Mac에는 git-lfs 3.3.0이 깔려 있어 직접 해 봤습니다.

bash
git lfs version
git lfs install
git lfs track "*.mp4"
cat .gitattributes
text
git-lfs/3.3.0 (GitHub; darwin arm64; go 1.19.3)
Updated Git hooks.
Git LFS initialized.
Tracking "*.mp4"
*.mp4 filter=lfs diff=lfs merge=lfs -text

git lfs install은 Mac마다 한 번(전역 git 설정에 LFS 필터 등록), git lfs track은 저장소마다 한 번입니다. track은 .gitattributes에 규칙을 적을 뿐이니, 이 파일을 반드시 커밋해야 다른 Mac과 에이전트도 같은 규칙을 씁니다.

bash
git add .gitattributes assets/menu-video.mp4
git commit -m "메뉴 소개 영상 (LFS)"
git show HEAD:assets/menu-video.mp4
text
[main 40ab1d3] 메뉴 소개 영상 (LFS)
 2 files changed, 4 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 assets/menu-video.mp4
version https://git-lfs.github.com/spec/v1
oid sha256:2daeb1f36095b44b318410b3f4e8b5d989dcc7bb023d1426c492dab0a3053e74
size 8388608

git show로 커밋 안을 들여다보면 8MB 영상 대신 세 줄짜리 포인터가 들어 있습니다. 작업 폴더에는 진짜 영상이 있고, 다른 폴더에 clone해도 영상이 제대로 받아졌습니다(ls로 8388608바이트 확인).

🪝
직접 해 보다 만난 함정: LFS가 내 훅 폴더에 파일을 만든다 — 이 저장소는 7장에서 core.hooksPath를 .githooks로 바꿔 두었는데, 그 상태에서 git lfs install을 치자 "Updated Git hooks."와 함께 .githooks/ 안에 pre-push·post-checkout·post-commit·post-merge 네 파일이 새로 생겼습니다. LFS는 훅이 있는 곳(core.hooksPath)에 자기 훅을 씁니다. 지우지 말고 함께 커밋하면, 다른 Mac에서도 push할 때 LFS 파일이 같이 올라갑니다.
🌳
Claude Code worktree와 LFS — 공식 문서(worktrees)에 따르면 git lfs install --local로 LFS를 저장소 설정에만 넣은 경우, Claude Code가 만든 worktree에는 진짜 파일 대신 포인터 파일이 들어갑니다. 저장소 자체 설정의 필터는 누구든 써 넣을 수 있는 셸 명령이라 worktree를 만들 때 실행하지 않기 때문입니다. 해결은 그 worktree에서 git lfs pull. 처음부터 --local 없이 git lfs install(전역)로 설정하면 이 문제가 없습니다.

LFS 저장 용량과 전송량에는 호스팅 서비스마다 별도 한도와 요금이 있습니다. 이 글에서는 구체적인 숫자를 확인하지 않았으니, 쓰기 전에 쓰는 서비스의 요금 안내를 확인하세요.

6-2. 데이터셋·모델은 저장소 밖에, 위치만 코드에

LFS는 "코드와 함께 버전이 바뀌는 적당히 큰 파일"(로고 원본, 짧은 영상, 테스트용 샘플)에 어울립니다. 수 GB짜리 데이터셋이나 학습된 모델 가중치는 다릅니다. 이런 건 S3 같은 오브젝트 스토리지(자체 서버라면 MinIO 같은 S3 호환 저장소)에 두고, 저장소에는 어디에 있는지만 남기는 편이 낫습니다.

저장소 (git)
코드 · mise.toml · .env.example의 MENU_DATA_URL
→
오브젝트 스토리지
s3://cafe-menu-data/menu/2026-10/
↓
각 Mac · CI
필요할 때 받아서 gitignore된 data/raw/에

위치를 코드에 박아 두는 요령은 두 가지입니다. 첫째, 주소는 .env.example의 MENU_DATA_URL처럼 이름 붙은 설정으로 둡니다. 둘째, 날짜나 버전을 경로에 넣어(2026-10/) 덮어쓰지 않습니다. 그러면 git 커밋 하나가 "이 코드는 이 데이터 버전과 짝"이라는 기록이 됩니다. 받는 명령도 태스크로 만들어 두면 에이전트가 경로를 추측할 일이 없습니다. 아래는 형태만 보여 주는 예시입니다(이 글에서는 실제 버킷이 없어 실행하지 않았습니다).

toml
[tasks."data:pull"]
description = "메뉴 데이터 받기 (S3 → data/raw/)"
run = "aws s3 sync $MENU_DATA_URL data/raw/"

7. 안전장치 네 겹: 훅 → CI → main 보호 → push 확인

네 개의 검문소를 지나는 길크게 보기

지금까지는 "이렇게 해 주세요"를 파일로 적는 일이었습니다. 이번 장은 "이건 안 됩니다"를 구조로 막는 일입니다. 한 겹으로는 부족합니다. 각 겹은 뚫리는 방식이 다르기 때문입니다.

1 · 내 Mac
pre-commit 훅 — 커밋 직전에 .env·큰 파일을 막습니다. 가장 빠르지만, 연결하지 않은 clone에서는 돌지 않고 건너뛸 수도 있습니다.
2 · 원격
CI — PR마다 서버에서 lint·test를 돌립니다. 어느 Mac에서 무엇을 건너뛰었든 여기서는 똑같이 검사합니다.
3 · 원격
main 보호 규칙 — main 직접 push 금지, PR 필수, CI 통과 필수. 에이전트든 사람이든 예외 없이.
4 · 에이전트
Claude Code 권한 ask — push하기 전에 사람에게 묻게 합니다. 공유가 시작되는 지점을 사람이 쥡니다.

7-1. pre-commit 훅: .env와 큰 파일을 커밋 직전에

git 훅(hook)은 git이 특정 시점에 자동으로 실행하는 스크립트입니다. git 공식 문서(githooks)에 따르면 pre-commit은 git commit이 커밋을 만들기 전에 실행되고, 0이 아닌 값으로 끝나면 커밋을 중단합니다. 훅 폴더는 기본이 .git/hooks인데, core.hooksPath 설정으로 바꿀 수 있습니다. .git/ 안은 커밋되지 않으니, 훅을 팀과 공유하려면 저장소 안의 .githooks/ 같은 폴더에 두고 core.hooksPath로 연결하는 게 흔한 방법입니다.

sh
#!/bin/sh
# .githooks/pre-commit — 커밋 직전에 도는 검사. 막히면 커밋이 만들어지지 않는다.

# 1) .env 계열 파일이 스테이징되면 막는다 (.env.example은 허용)
bad=(gitdiff−−cached−−name−only−−diff−filter=ACMR∣grep−E′(∣/)e˙nv(.˙+)?(git diff --cached --name-only --diff-filter=ACMR | grep -E '(^|/)\.env(\..+)?(gitdiff−−cached−−name−only−−diff−filter=ACMR∣grep−E′(∣/)e˙nv(.˙+)?' | grep -v '\.env\.example$')
if [ -n "$bad" ]; then
  echo "pre-commit: 비밀 파일은 커밋할 수 없습니다 -> $bad" >&2
  echo "  git restore --staged $bad  로 스테이징을 취소하세요." >&2
  exit 1
fi

# 2) 5MB 넘는 파일은 막는다 (Git LFS나 S3로)
for f in $(git diff --cached --name-only --diff-filter=ACM); do
  size=(gitcat−file−s":(git cat-file -s ":(gitcat−file−s":f" 2>/dev/null || echo 0)
  if [ "$size" -gt 5242880 ]; then
    echo "pre-commit: f가5MB를넘습니다(f 가 5MB를 넘습니다 (f가5MB를넘습니다(size bytes). Git LFS나 S3를 쓰세요." >&2
    exit 1
  fi
done
exit 0

실행 권한을 주고 연결합니다.

bash
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks

이제 5장에서 남겨 둔 구멍, git add -f .env를 시험해 봅니다.

bash
git add -f .env
git commit -m "결제 키 설정"
text
pre-commit: 비밀 파일은 커밋할 수 없습니다 -> .env
  git restore --staged .env  로 스테이징을 취소하세요.

커밋은 만들어지지 않았습니다. 큰 파일도 시험해 봤습니다. LFS를 설정하기 전에 8MB 영상을 커밋하려 하자 이렇게 막혔습니다.

text
pre-commit: assets/menu-video.mp4 가 5MB를 넘습니다 (8388608 bytes). Git LFS나 S3를 쓰세요.

LFS로 추적하도록 바꾼 뒤에는 같은 훅을 통과했습니다. 훅이 보는 건 스테이징된 내용인데, LFS 파일은 스테이징 단계에서 이미 작은 포인터로 바뀌어 있기 때문입니다.

훅의 약점 두 가지를 꼭 알아 두세요.

  • clone으로 연결되지 않습니다. core.hooksPath는 각 clone의 .git/config에 들어가는 설정이라, 새 Mac에서 clone만 하면 훅이 돌지 않습니다. 그래서 4장의 mise run install 태스크에 git config core.hooksPath .githooks를 넣었습니다. "설치 = 훅 연결"로 묶어 두면 빠뜨릴 일이 줄어듭니다.
  • 건너뛸 수 있습니다. git 문서에 적힌 대로 pre-commit은 --no-verify 옵션으로 건너뛸 수 있습니다. 그래서 훅은 "실수 방지"이지 "최종 관문"이 아닙니다. 에이전트가 이 옵션을 쓰지 않도록 AGENTS.md에 적거나 권한 규칙으로 막아 둘 수는 있지만, 최종 관문은 다음 두 겹입니다.
🧰
훅 관리 도구를 써도 됩니다 — 여기서는 원리를 보이려고 셸 스크립트 하나로 만들었지만, 규모가 커지면 pre-commit 프레임워크나 lefthook 같은 훅 관리 도구를 쓰는 팀도 많습니다. 어떤 도구든 "설정 파일은 저장소에, 연결은 설치 단계에서"라는 원칙은 같습니다.

7-2. CI: 어느 Mac에서 왔든 같은 검사

CI(지속적 통합)는 PR이 올라올 때마다 서버에서 자동으로 검사를 돌리는 장치입니다. 여기서 mise의 장점이 한 번 더 나옵니다. CI도 같은 mise.toml·mise.lock으로 도구를 설치하고 같은 태스크 이름으로 검사하면, "내 Mac에선 됐는데"가 사라집니다. mise 공식 GitHub Action(jdx/mise-action)의 README 예시를 따라 만든 워크플로입니다(2026년 9월 기준 README는 actions/checkout@v6, jdx/mise-action@v4를 씁니다).

yaml
# .github/workflows/ci.yml
name: ci
on:
  pull_request:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: jdx/mise-action@v4   # mise.toml·mise.lock대로 도구 설치
      - run: mise run install
      - run: mise run lint
      - run: mise run test

README에 따르면 이 액션은 기본으로 mise install을 실행하고 캐시도 켭니다. 4장에서 만든 mise.lock에 linux-x64 항목이 들어 있었던 걸 기억하세요. 우분투 러너도 맥북과 똑같은 node 24.21.0을 받습니다. 이 워크플로는 실제 GitHub에서 돌려 보지는 않았으니, 처음 올릴 때 Actions 탭에서 결과를 꼭 확인하세요. GitHub Actions 자체가 처음이라면 GitHub Actions 실전 가이드를 참고하세요.

7-3. main 보호: 약속을 구조로

기존 시리즈 6편에서 다룬 브랜치 보호 규칙을 켭니다. 저장소 Settings → Branches(또는 Rulesets)에서 main에 "PR 필수", "승인 필수", "상태 검사(CI) 통과 필수"를 겁니다. 이러면 7-2의 CI가 빨간 X인 PR은 머지되지 않고, main에 직접 push하는 시도는 GitHub가 거절합니다. AGENTS.md의 "main에 직접 push하지 않는다"가 부탁에서 규칙으로 바뀌는 지점입니다.

7-4. Claude Code: push 전에 물어보게

마지막 겹은 에이전트 쪽입니다. 기존 시리즈 7편에서 확인했듯 Claude Code의 auto 모드는 작업 중인 저장소의 브랜치로 push하는 것을 기본적으로 허용합니다. push는 "내 Mac 안의 일"이 "팀이 보는 일"로 바뀌는 순간이니, 프로젝트의 .claude/settings.json에 ask 규칙을 넣어 매번 확인 창을 띄웁니다.

json
{
  "permissions": {
    "ask": [
      "Bash(git push *)"
    ],
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(!.env.example)"
    ]
  }
}

이 파일은 커밋해서 팀 전체가 같은 울타리를 씁니다. 7편에서 짚었듯 Bash 규칙은 명령을 쓰인 그대로 비교하므로 완벽한 장벽은 아닙니다. 그래서 7-3의 main 보호와 함께 거는 것입니다.


8. 새 Mac에서 5분 만에: 다섯 줄 부트스트랩

이제 모든 조각이 저장소 안에 있습니다. 민지가 새로 산 맥 미니를 사무실에 들였다고 해 봅시다. Mac마다 한 번 필요한 준비(도구 설치, 셸 활성화, LFS 필터 등록)를 마친 뒤, 저장소를 되살리는 데 필요한 건 다섯 줄입니다.

bash
git clone https://github.com/minji/cafe-menu.git && cd cafe-menu
mise install
mise run install
cp .env.example .env
mise run test

실제로 완전히 빈 mise 데이터 폴더와 새 홈 폴더로 흉내 낸 "새 Mac"에서 돌린 결과를 순서대로 이어 붙였습니다(clone은 가짜 원격에서, mise 진행 표시 줄은 생략, cp는 출력이 없음).

text
Cloning into 'cafe-menu'...
done.
mise python@3.13.15 Python 3.13.15
mise ✓ python@3.13.15  3.7s  cpython-3.13.15+20260924-aarch64-apple-darwin-install_only_stripped.tar.gz
mise node@24.21.0 v24.21.0
mise node@24.21.0 11.19.0
mise ✓ node@24.21.0    5.7s  node-v24.21.0-darwin-arm64.tar.gz
[install] $ git config core.hooksPath .githooks
[install] $ npm install

up to date, audited 1 package in 340ms

found 0 vulnerabilities
[test] $ npm test

> test
> node --test

✔ 아메리카노 2잔 + 라테 1잔 (0.468083ms)
✔ 없는 메뉴는 에러 (0.134833ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 63.567833

맥북에서 본 것과 같은 node 24.21.0, 같은 python 3.13.15, 같은 테스트 결과입니다. 훅도 연결됐습니다(git config core.hooksPath가 .githooks를 돌려줌). git status --short는 비어 있었습니다. .env는 gitignore됐고 LFS 영상은 진짜 8MB 파일로 받아졌습니다. 같은 다섯 단계를 또 다른 빈 폴더에서 mise install --locked로 처음부터 끝까지 재 보니 7초가 걸렸습니다. 이 저장소가 작고 네트워크가 빨라서 나온 숫자이긴 하지만, 제목의 "5분"은 넉넉한 약속입니다.

각 단계가 저장소의 어떤 파일을 읽는지 눌러 가며 확인해 보세요.

이 다섯 줄은 사람만을 위한 게 아닙니다. 새 worktree를 만든 에이전트에게도 똑같이 통합니다. AGENTS.md의 "처음 한 번" 줄이 바로 이것이고, 3편의 Worktrunk post-start 훅(worktree를 만든 뒤 자동 실행)에 mise install && mise run install을 걸어 두면 에이전트는 첫 명령부터 올바른 환경에서 시작합니다.

✅
"환경 설정 문서"가 필요 없어진 이유 — 예전 같으면 위키에 "node 24 설치, python 3.13 설치, npm install, 훅 복사, .env는 도윤에게 물어볼 것"을 적었을 겁니다. 그 문서는 반드시 낡습니다. 지금은 문서가 곧 실행 파일(mise.toml)이라, 낡으면 mise run test가 먼저 알려 줍니다.

9. 내 저장소 점검하기

마지막으로 여러분의 저장소를 점검해 보세요. 있는 파일을 체크하면, 빠진 파일이 막아 주었을 사고를 보여 줍니다.

완성된 cafe-menu의 파일 구성을 한 번에 보면 이렇습니다. 이번 편에서 새로 생긴 건 모두 작은 텍스트 파일이고, 전부 git으로 함께 움직입니다.

cafe-menu/ — 6편 완성본
AGENTS.md — 모든 에이전트 공용 규칙 (원본)
CLAUDE.md — @AGENTS.md + Claude 전용 2줄
mise.toml · mise.lock — 도구 버전 · 태스크 · .env 읽기
.env.example — 변수 이름 견본 (커밋) · .env — 진짜 값 (gitignore)
.gitignore · .worktreeinclude — 비밀 제외 · worktree에 .env 복사
.gitattributes — *.mp4는 LFS
.githooks/ — pre-commit(우리 것) + LFS 훅 4개
.github/workflows/ci.yml — mise run lint · test
.claude/settings.json — push는 ask, .env 읽기는 deny
index.html · menu.md · src/order.js · test/ · scripts/ · package.json

치트시트

하고 싶은 것명령 / 파일메모
모든 에이전트에게 규칙 알리기AGENTS.mdCodex는 루트→작업 폴더 순으로 합침, 기본 32 KiB
Claude Code도 같은 규칙 읽게CLAUDE.md 첫 줄 @AGENTS.mdCLAUDE.md가 있으면 AGENTS.md는 import해야 읽힘
규칙이 읽혔는지 확인Claude Code /memory · /contextMemory files 목록에 경로가 보여야 함
도구 버전 고정mise.toml [tools] → mise installmise ls로 확인
정확한 버전 잠그기mise lock → mise.lock 커밋설치는 mise install --locked, 올리기는 mise lock --bump
명령 목록 · 실행mise tasks · mise run testAGENTS.md에 "항상 mise run으로"
설정 파일 신뢰mise trust · mise trust --showinstall·run·exec는 일반 모드에서 자동 신뢰
비밀의 자리 남기기.env.example 커밋 · .env gitignorecp .env.example .env
worktree에 .env 복사.worktreeinclude에 .envClaude Code가 만드는 worktree에 적용
큰 파일git lfs install · git lfs track "*.mp4".gitattributes 커밋, 데이터셋은 S3
커밋 직전 검사.githooks/pre-commit + git config core.hooksPath .githooksclone마다 연결 필요 → install 태스크에
최종 관문CI(jdx/mise-action) + main 보호 규칙훅은 건너뛸 수 있으니
push는 사람이.claude/settings.json "ask": ["Bash(git push *)"]main 보호와 함께
새 Mac 부트스트랩git clone → mise install → mise run install → cp .env.example .env → mise run test재현에서 7초

자주 하는 질문(FAQ)

Q1. AGENTS.md와 CLAUDE.md에 같은 내용을 복사해 두면 안 되나요?

동작은 하지만 곧 어긋납니다. 누군가 AGENTS.md의 테스트 명령만 고치고 CLAUDE.md는 잊으면, Codex와 Claude Code가 서로 다른 명령을 쓰게 됩니다. 공통 규칙은 AGENTS.md 한 곳에만 두고 CLAUDE.md는 @AGENTS.md로 가져오세요. Claude Code 공식 문서가 권하는 구조이고, 문서에 따르면 import 방식은 AGENTS.md를 두 번 읽게 하지도 않습니다.

Q2. Claude Code가 AGENTS.md를 직접 읽는다면 CLAUDE.md는 아예 없어도 되나요?

Claude 전용 지시가 없다면 그래도 됩니다. 공식 문서에 따르면 v2.1.277 이상에서 CLAUDE.md·CLAUDE.local.md가 없으면 AGENTS.md를 읽습니다. 다만 문서는 일부 세션(구버전, agents-md 내장 플러그인을 끈 경우, 업그레이드 직후 첫 세션 등)에서는 AGENTS.md를 직접 읽지 못한다고 적고, 그럴 때 CLAUDE.md에서 import하라고 권합니다. 또 누군가 개인용 CLAUDE.local.md를 만들면 그 순간 AGENTS.md를 안 읽게 됩니다. 팀 저장소라면 @AGENTS.md 한 줄짜리 CLAUDE.md를 두는 편이 안전합니다.

Q3. mise 말고 nvm·pyenv·asdf를 써도 되나요?

됩니다. 이 글의 핵심은 mise라는 도구가 아니라 "버전과 명령을 저장소의 파일로 고정한다"는 원칙입니다. .nvmrc, .python-version, package.json의 engines 같은 파일도 같은 역할을 부분적으로 합니다. mise를 고른 이유는 여러 언어의 버전과 태스크, 환경변수, 잠금 파일을 한 파일에서 다루고, CI용 공식 액션까지 있어서 "로컬 = CI"를 만들기 쉽기 때문입니다.

Q4. 에이전트에게 mise install을 맡겨도 되나요?

내 팀 저장소라면 괜찮습니다. 다만 4장에서 봤듯 mise install·mise run은 일반 모드에서 설정 파일을 자동으로 신뢰하고, 태스크는 임의의 셸 명령입니다. 처음 보는 저장소라면 사람이 먼저 mise.toml을 읽어 보세요. 에이전트에게는 "mise.toml에 없는 도구를 새로 추가하지 말고, 필요하면 먼저 물어보라"고 AGENTS.md에 적어 두면 좋습니다.

Q5. pre-commit 훅이 있는데 CI까지 필요한가요?

필요합니다. 훅은 각 clone에서 연결해야 돌고(core.hooksPath는 clone으로 따라오지 않음), git 문서에 적힌 대로 --no-verify로 건너뛸 수도 있습니다. 훅은 "내 Mac에서 빨리 알려 주는 실수 방지", CI와 main 보호는 "누가 어디서 올렸든 통과해야 하는 관문"입니다. 역할이 다릅니다.

Q6. 이미 .env를 커밋해 버렸어요.

.gitignore에 추가해도 이미 기록된 파일은 사라지지 않습니다(기존 2편 8장). 가장 먼저 할 일은 그 키를 무효화하고 새로 발급하는 것입니다. push까지 했다면 기록을 지워도 이미 누군가 받아 갔을 수 있기 때문입니다. 그다음 git rm --cached .env로 추적을 끊고 커밋하세요. 기록에서 완전히 지우는 작업은 팀 전체의 clone에 영향을 주니 도윤 같은 동료와 먼저 상의하세요.


이번 편 요약

주제기억할 것
왜에이전트는 세션마다 빈 머리, Mac은 저마다 다른 버전 → 규칙과 환경을 저장소 파일로
AGENTS.md공용 규칙의 원본. Codex는 루트→작업 폴더로 합침, 폴더당 한 파일, 기본 32 KiB, AGENTS.override.md 우선
CLAUDE.mdCLAUDE.md가 있으면 Claude는 AGENTS.md를 직접 안 읽음 → 첫 줄 @AGENTS.md + Claude 전용 메모. 200줄 이내
mise[tools]로 버전, [tasks]로 명령, mise.lock으로 정확한 버전. AI가 환경을 추측하지 않게
비밀.env.example 커밋, .env gitignore, Read deny 규칙, .worktreeinclude
큰 파일GitHub 100MiB 거부 · LFS로 포인터만 · 데이터셋은 S3, 위치만 코드에
안전장치훅(빠름·건너뛸 수 있음) → CI(같은 mise) → main 보호 → push ask
부트스트랩clone → mise install → mise run install → cp .env.example .env → mise run test

다음 편 예고

본편은 여기까지입니다. 여섯 편 동안 세 원칙을 하나씩 실제로 만들었습니다. 작업 하나에 worktree·브랜치·세션 하나(2·3·5편), Mac 사이는 push와 fetch(4편), 그리고 이번 편의 "개발환경은 저장소 안의 파일로"까지.

마지막 부록 「도구 지도 2026」에서는 이 원칙 위에 얹어 쓸 수 있는 도구들을 둘러봅니다. 여러 에이전트 세션을 한 화면에서 관제하는 GUI, 한 작업 폴더에서 여러 브랜치를 동시에 다루는 GitButler, Jujutsu의 workspace, 자체 호스팅 Forgejo까지, 언제 쓰고 언제 쓰지 않을지를 정리합니다.


출처

  • Claude Code 공식 문서 — How Claude remembers your project (CLAUDE.md 위치·import·AGENTS.md 읽기 규칙·CLAUDE.local.md): code.claude.com
  • Claude Code 공식 문서 — Configure permissions (Read deny 규칙과 적용 범위): code.claude.com
  • Claude Code 공식 문서 — Run parallel sessions with worktrees (.worktreeinclude, LFS 포인터 문제): code.claude.com
  • OpenAI Codex 공식 문서 — AGENTS.md (탐색 순서·override·project_doc_max_bytes 32 KiB): learn.chatgpt.com
  • AGENTS.md 공식 사이트: agents.md
  • mise 공식 문서: mise.jdx.dev · 잠금 파일: mise.jdx.dev · 환경변수(_.file, redact): mise.jdx.dev · trust: mise.jdx.dev
  • jdx/mise-action (GitHub Actions): github.com
  • Git 공식 문서 — githooks (core.hooksPath, pre-commit, --no-verify): git-scm.com
  • GitHub Docs — About large files on GitHub (50MiB 경고·100MiB 거부·저장소 크기 권장): docs.github.com
  • Git LFS: git-lfs.com