들어가며
37차시는 **섹션 8(Git 통합 워크플로우)**의 토대를 깔았다. 변경을 읽는 세 명령(status·diff·log), 변경을 분석하는 단계, 그리고 그 위를 받치는 안전 원칙까지. 한 문장으로 줄이면 37차시는 읽기와 안전이었다. 아직 저장소에 아무것도 쓰지 않았다.
38차시부터 달라진다. 이번이 그 토대 위에서 처음으로 쓰는 작업이다 — 커밋 생성. 분석한 변경을 영구 기록(37차시의 permanent history)에 묶어 남기는 첫 단계다.
커밋을 “마지막에 한 번 치는 명령” 정도로 보기 쉽다. 그런데 에이전트 시대에는 오히려 반대다. 커밋이 더 중요해진다. 이유는 단순하다.
- Claude는 커밋을 자주, 많이 만든다. 사람이 하루에 몇 번 하던 커밋을, 에이전트 워크플로우는 작업 단위마다 쌓는다. 영구 기록에 무엇이 어떻게 남느냐가 곧 미래의 협업 비용이 된다.
- 커밋 메시지는 장식이 아니라 미래의 독자에게 보내는 글이다. 그 독자는 미래의 나, 동료, 그리고 이 저장소를 읽을 다음 에이전트다 — 37차시에서 봤듯 Claude는
git log를 코드의 내력을 묻는 출처로 쓴다. 오늘 쓴 메시지가 내일 Claude가 읽는 맥락이 된다.
그래서 이 차시는 “그냥 커밋하는 법”이 아니라 좋은 커밋을 자동으로 만드는 법을 다룬다 — 스마트 메시지 생성의 절차, Conventional Commits 형식, 본문을 위한 HEREDOC, 그리고 기여를 정직하게 표시하는 Co-Authored-By 트레일러까지.
먼저 흔한 오해 하나를 걷어내고 시작한다.
/commit 같은 건 없다 — 커밋은 자연어로 부른다
커밋을 /commit 같은 전용 슬래시 명령으로 떠올리기 쉽다. 하지만 현재 Claude Code의 내장 명령 목록에 /commit은 없다. Git과 엮인 내장은 변경을 보는 /diff(커밋 안 된 변경 뷰어), /code-review, /security-review, GitHub 연동용 /install-github-app 정도다.
커밋은 명령이 아니라 요청으로 시작한다. 자연어로 부탁하면 된다.
변경사항 커밋해줘
그러면 Claude가 뒤에서 Bash 도구로 커밋 절차를 돈다. 베스트 프랙티스 문서의 4단계 워크플로우(Explore → Plan → Implement → Commit)에서도 마지막 Commit 스텝의 프롬프트는 이렇게 자연어다.
설명적인 메시지로 커밋하고 PR을 열어줘
이건 37차시의 원칙과도 정확히 맞물린다 — 커밋·푸시는 요청해야만 한다. Claude는 변경을 만들었다고 알아서 커밋으로 넘어가지 않는다. “커밋해줘”라고 명시적으로 말해야 이 절차로 들어간다.
커밋은 “명령”이 아니라 “절차”다
사람도 좋은 커밋을 만들 때 그냥 git commit만 치지 않는다. 먼저 git status·git diff로 뭐가 바뀌었나 보고, 기존 로그를 흘끗 보고 스타일을 맞춘 다음, 메시지를 쓴다. Claude Code의 커밋 자동화도 똑같은 절차를 명시적으로 밟는다.
| 단계 | 명령 | 무엇을 하나 |
|---|---|---|
| 1 | git status | 무엇이 바뀌었나 — 수정·추가·삭제, 스테이징 여부 |
| 2 | git diff (+ --staged) | 어떻게 바뀌었나 — 줄 단위 변경 |
| 3 | git log | 이 저장소의 기존 커밋 스타일 |
| 4 | (메시지 작성) | 변경의 의도를 한 줄로, 필요하면 본문까지 |
| 5 | git add | 관련된 파일만 스테이징 |
| 6 | git commit | HEREDOC으로 실행 |
| 7 | git status | 커밋이 제대로 됐는지 확인 |
1~3번은 전부 읽기 전용 git이다. 37차시에서 봤듯 권한 프롬프트 없이 자동으로 돌고, 그래서 Claude는 종종 셋을 한 번에 병렬로 읽는다 — 공짜이고 안전하니까.
여기서 강조할 게 하나 있다. “스마트 커밋 메시지 생성”의 절반은 3번, git log다. 저장소마다 커밋 컨벤션이 다르다 — 어떤 팀은 feat:을 쓰고, 어떤 팀은 한 줄로만 끝내며, 어떤 팀은 본문에 변경 목록을 붙인다. Claude는 커밋 메시지를 쓰기 전에 git log로 기존 스타일을 읽고 거기에 맞춘다. 37차시에서 git log를 “이 저장소는 어떻게 일해 왔나”를 묻는 명령이라 부른 이유가 바로 여기서 쓰인다.
좋은 커밋 메시지 — “무엇”이 아니라 “왜”
커밋 메시지를 쓸 때 가장 흔한 실수는 무엇이 바뀌었는지를 그대로 옮겨 적는 것이다. 그런데 잘 생각해 보면, git diff가 이미 “무엇”을 정확히 보여준다. 어떤 줄이 더해지고 빠졌는지는 diff에 다 있다. 메시지가 그걸 또 반복하면 정보가 0이다.
좋은 메시지는 diff가 말해주지 않는 것 — 왜를 담는다. 이 변경이 어떤 문제를 풀려는 건지, 어떤 의도인지.
| 나쁜 예 | 무엇이 문제인가 |
|---|---|
update code | 무엇도 왜도 없다 |
change login.js | diff를 그대로 옮겼다(“무엇”의 반복) |
fix bug | 어떤 버그인지, 왜 그게 버그였는지 없다 |
| 좋은 예 | 무엇이 좋은가 |
|---|---|
feat: 로그인 폼에 비밀번호 표시 토글 추가 | 무엇을 했고(토글 추가) 어디에(로그인 폼) 명확 |
fix: 세션 만료 후 토큰 갱신 실패 수정 | 증상과 맥락(“세션 만료 후”)이 드러난다 |
이게 바로 “스마트 메시지 생성”의 정체다. Claude는 diff 안에 흩어진 줄 변경을 읽어 하나의 의도로 묶는다 — 37차시에서 본 변경 분석(“이건 로그인 폼에 비밀번호 토글을 더한 변경”)의 직접적인 연장이다. 그 한 줄 요약이 커밋의 제목이 된다.
요약 줄의 관례도 간단하다. 짧고, 명령형(imperative)으로. “추가했음”이 아니라 “추가”. 그리고 자명하지 않은 변경이라면, 빈 줄 하나 뒤에 왜를 설명하는 본문을 붙인다(뒤의 HEREDOC 절에서 다룬다).
Conventional Commits 형식
요약 줄을 일관된 구조로 쓰는 가장 널리 쓰이는 규약이 Conventional Commits다. 형식은 이렇다.
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
type은 이 커밋이 무슨 종류인지를 한 단어로 못 박는다. 대표적인 둘과 나머지는 이렇다.
| 타입 | 의미 | SemVer |
|---|---|---|
feat | 새 기능 추가 | MINOR |
fix | 버그 수정 | PATCH |
docs | 문서만 변경 | — |
refactor | 동작 변화 없는 구조 개선 | — |
test | 테스트 추가·수정 | — |
perf | 성능 개선 | — |
build·ci·chore·style | 빌드·CI·잡무·포맷 | — |
스펙이 못 박는 핵심 규칙 몇 가지를 그대로 옮기면 이렇다.
커밋은 반드시 타입(
feat,fix등)으로 시작해야 한다. 설명(description)은 타입/스코프 뒤의 콜론과 공백 바로 다음에 와야 한다. 본문(body)은 설명에서 빈 줄 하나를 둔 뒤 시작해야 한다.
**파괴적 변경(BREAKING CHANGE)**은 두 방식으로 표시한다 — 타입 뒤에 !를 붙이거나(feat!:), 푸터에 BREAKING CHANGE: <설명>을 적는다. 둘 다 SemVer의 MAJOR에 해당한다.
feat!: 인증 토큰 형식을 JWT로 교체
BREAKING CHANGE: 기존 세션 토큰은 더 이상 유효하지 않다. 재로그인이 필요하다.
그런데 왜 에이전트에게 이런 컨벤션이 특히 잘 맞을까? 컨벤션은 사람과 기계가 함께 읽는 계약이기 때문이다. feat·fix 같은 접두사가 일관되면, changelog 생성기나 semantic-release 같은 도구가 커밋만 보고 버전을 올리고 릴리스 노트를 만든다. Claude는 이런 규칙을 일관되게 지키는 데 강하다.
다만 중요한 단서가 있다. Claude는 당신 저장소의 컨벤션을 따른다. 절차 3번(git log)에서 기존 스타일을 읽기 때문이다. 저장소가 Conventional Commits를 쓰면 맞추고, 안 쓰면 굳이 들이밀지 않는다. 그러니 반드시 이 형식을 쓰게 하려면, 매번 말하는 대신 CLAUDE.md에 한 줄 박아 두면 된다(21차시).
# 커밋 규칙
- 커밋 메시지는 Conventional Commits 형식을 따른다
베스트 프랙티스 문서도 CLAUDE.md에 넣을 것으로 “Repository etiquette (branch naming, PR conventions)” 를 꼽는다 — 커밋·PR 컨벤션이 정확히 그 자리다.
본문과 HEREDOC
요약 한 줄로 충분한 커밋도 많지만, 자명하지 않은 변경에는 본문을 붙인다 — 왜 그렇게 했는지, 그리고 핵심 변경 목록. 문제는, 여러 줄짜리 메시지를 명령행에서 만들 때다. 따옴표·개행·셸 변수($)·백틱이 뒤엉키기 쉽다.
그래서 Claude는 멀티라인 커밋을 HEREDOC으로 만든다.
git commit -m "$(cat <<'EOF'
feat: 로그인 폼에 비밀번호 표시 토글 추가
- 비밀번호 필드에 eye 아이콘 추가
- 클릭 시 표시/숨김 토글
- 접근성 aria-label 추가
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
여기서 포인트는 **따옴표로 감싼 'EOF'**다. 구분자를 작은따옴표로 감싸면 셸이 본문 안의 $변수나 백틱을 해석하지 않고 글자 그대로 둔다. 그래서 메시지에 $NODE_ENV 같은 문자열이나 백틱 든 코드 조각이 들어가도 안전하게 보존된다. 멀티라인·특수문자·정확한 포맷을 한 번에 해결하는 관용구다.
Co-Authored-By — 기여를 정직하게 표시한다
위 예시 마지막 줄을 보자.
Co-Authored-By: Claude <noreply@anthropic.com>
이건 Git의 **트레일러(trailer)**다. 커밋 메시지 맨 끝에 Key: Value 형태로 붙는 메타데이터로, GitHub이 Co-Authored-By를 인식해 해당 커밋의 공동 작성자(co-author) 목록에 Claude를 표시한다. 활성 모델에 따라 모델명이 함께 들어가기도 한다(예: Claude Opus, Claude Sonnet, Claude Haiku).
이 트레일러는 37차시의 한 원칙과 정확히 맞닿아 있다 — Claude는 git config를 건드리지 않는다. 즉 커밋의 작성자(author) 정보는 사람 그대로 둔다. 그러면서 Claude의 기여는 작성자를 위조하지 않고 트레일러로 덧붙여 표시한다. 작성자는 당신, 공동 기여자는 Claude — 이게 정직한 표시 방식이다.
참고로 커밋에는 Co-Authored-By 트레일러가, PR에는 별도의 푸터가 붙는다.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
PR 쪽 푸터는 39차시에서 다룬다. 여기서는 커밋 = 트레일러, PR = 푸터로 자리만 잡아 두면 된다.
기여 표시를 끄거나 바꾸기 — attribution 설정
팀 정책에 따라 이 기여 표시를 원치 않을 수 있다. 끄거나 바꾸는 건 설정 한 줄이다.
예전에는 includeCoAuthoredBy로 켜고 껐다. 공식 문서가 이렇게 설명한다(현재는 deprecated).
Deprecated:
attribution을 대신 쓰라. git 커밋과 PR에co-authored-by Claudebyline을 넣을지 여부(기본값:true).
지금 권장되는 방식은 attribution 설정이다(v2.0.62에서 도입). 커밋과 PR을 따로 커스터마이즈할 수 있고, 각 값을 빈 문자열("")로 두면 그 위치의 표시가 꺼진다.
기여 표시를 완전히 끄려면 settings.json에 이렇게 둔다.
{
"attribution": {
"commit": "",
"pr": ""
}
}
문구를 바꾸려면 원하는 텍스트를 넣는다 — 예컨대 커밋에는 byline을, PR에는 아무것도 안 붙이는 식이다.
{
"attribution": {
"commit": "🤖 Generated with Claude Code",
"pr": ""
}
}
기본값은 표시가 켜져 있다는 점만 기억하면 된다. 끄고 싶다면 명시적으로 비워 둔다.
37차시의 안전 원칙은 커밋 단계에 그대로 적용된다
커밋은 쓰는 작업이라, 37차시에서 본 모든 안전 원칙이 여기서 살아난다. 커밋 맥락으로 다시 짚으면 이렇다.
- 요청해야만 커밋한다. 자동으로 커밋되지 않는다. 푸시는 더더욱 — 요청 없이 원격에 올리지 않는다.
- 기본 브랜치면 먼저 브랜치를 판다.
main·master위라면 커밋 전에 새 브랜치부터 만든다. - 훅을 스킵하지 않는다.
--no-verify로 pre-commit 훅(29·30차시)을 건너뛰지 않는다. 린트·테스트·포맷이 막히면 게이트를 끄는 게 아니라 고친다. --amend보다 새 커밋. 마지막 커밋을 갈아치우는 amend는 신중히 쓰고, 쓸 땐 authorship을 확인한다(남의 커밋을 덮어쓰지 않기 위해).- 민감 파일은
add에서 거른다..env·*.key는 스테이징 단계에서 제외한다 — 변경 분석으로 걸러내고,Read(.env)deny 같은 규칙으로 접근 자체도 막을 수 있다.
그리고 이 선들은 말이 아니라 **권한 시스템(17차시)**으로 강제할 수 있다. 37차시에서 본 그 예시가 정확히 이 차시의 주제를 담는다 — 커밋은 자동 허용, 푸시는 차단.
{
"permissions": {
"allow": [
"Bash(git commit *)"
],
"deny": [
"Bash(git push *)"
]
}
}
비대화형 배치(헤드리스)에서도 같은 원리다. 베스트 프랙티스 문서의 fan-out 예시는 편집과 커밋만 열어 둔다 — --allowedTools "Edit,Bash(git commit *)". 커밋까지는 자율, 푸시는 사람의 손에 — 이 비대칭이 안전한 자동화의 기본형이다.
흔한 함정
/commit슬래시 명령을 찾는다. 그런 내장 명령은 없다. “커밋해줘”라는 자연어로 부르고, Claude가 Bash로 절차를 돈다.- 메시지에 “무엇”만 쓴다.
git diff가 이미 무엇을 보여준다. 메시지엔 왜를 쓴다 —update code말고fix: 세션 만료 후 토큰 갱신 실패 수정. - Conventional Commits가 항상 강제된다고 믿는다. Claude는
git log로 저장소 기존 스타일을 읽어 맞춘다. 반드시 쓰게 하려면 CLAUDE.md에 명시한다(21차시). - 멀티라인 메시지를
-m "..."여러 개로 욱여넣는다. 본문 있는 커밋은 HEREDOC("$(cat <<'EOF' … EOF)")으로. 작은따옴표'EOF'가 셸 해석을 막아 준다. Co-Authored-By가 작성자를 바꾼다고 오해한다. 작성자(author)는 그대로다. 트레일러는 공동 기여만 덧붙인다. Claude는git config를 건드리지 않는다(37차시).- 트레일러를 못 끄는 줄 안다.
attribution설정(구includeCoAuthoredBy)으로commit·pr을 각각 비우거나 바꾼다. - 테스트가 막힌다고
--no-verify로 넘긴다. pre-commit 훅은 팀의 품질 게이트다. 우회가 아니라 고치는 게 맞다. - 한 커밋에 여러 갈래를 다 몰아넣는다. 관련된 변경끼리 묶어 의미 있는 단위로 커밋한다 — 그래야
git log도, 나중의 리뷰·되돌리기도 깔끔하다. - 커밋이 자동으로 됐다고 기대한다(혹은 그 반대). Claude는 요청해야 커밋한다. 자동화하려면
allow Bash(git commit *)를 명시하되, 푸시는deny Bash(git push *)로 막아 두는 편이 안전하다.
정리
핵심 요점
- 커밋은 명령이 아니라 절차다 —
status(무엇이)→diff(어떻게)→log(기존 스타일)→메시지→add→commit→status(확인). 앞 셋은 읽기 전용 git이라 자동·병렬로 돈다. 스마트의 절반은git log로 저장소 컨벤션에 맞추는 데 있다. - 좋은 메시지는 “무엇”이 아니라 “왜”다.
diff가 이미 무엇을 보여주므로, 메시지는 의도를 담는다. 흩어진 변경을 하나의 의도로 묶는 것(37차시 변경 분석의 연장)이 “스마트 메시지 생성”의 정체다. - Conventional Commits =
<type>[scope]: <description>+ 본문 + 푸터.feat→MINOR,fix→PATCH,BREAKING CHANGE(!또는 푸터)→MAJOR로 SemVer와 맞물린다. 단 Claude는 당신 저장소의 컨벤션을 따르므로, 강제하려면 CLAUDE.md에 적는다. - 본문 있는 멀티라인은 HEREDOC으로.
"$(cat <<'EOF' … EOF)"에서 작은따옴표'EOF'가 셸의$·백틱 해석을 막아 메시지를 글자 그대로 보존한다. Co-Authored-By: Claude <noreply@anthropic.com>트레일러로 정직하게 표시한다. GitHub이 공동 작성자로 인식하고(모델명이 들어갈 수 있다),git config는 건드리지 않은 채 작성자는 사람·기여는 트레일러로 둔다. 끄거나 바꾸려면attribution설정(구includeCoAuthoredBy).- 37차시 안전 원칙은 커밋에 그대로 적용된다 — 요청해야만 커밋, 기본 브랜치면 먼저 브랜치, 훅 스킵(
--no-verify) 금지,--amend보다 새 커밋, 민감 파일 제외.allow Bash(git commit *)+deny Bash(git push *)로 못 박는다.
다음 단계
38차시가 하나의 커밋을 잘 만드는 법이었다면, 39차시는 그 커밋들을 모아 PR로 올리는 법이다. 핵심 차이가 하나 있다 — PR은 최신 커밋 하나가 아니라 브랜치의 모든 커밋을 분석해 제목과 본문을 쓴다. 그리고 이 차시에서 본 커밋의 Co-Authored-By 트레일러가, PR에서는 🤖 Generated with Claude Code 푸터로 이어지고, gh CLI로 실제 PR이 만들어진다. 38차시에서 “좋은 커밋”을 봤으니, 39차시에서 그 커밋들이 어떻게 좋은 PR이 되는지로 이어진다.
참고 자료
- 베스트 프랙티스 — Explore→Plan→Implement→Commit 워크플로우, “commit with a descriptive message and open a PR”, CLAUDE.md에 담을 repository etiquette
- 일반 워크플로우 — 변경 요약·PR 생성 프롬프트 레시피,
gh pr create세션 링크 - 설정 —
attribution(커밋·PR 기여 표시 커스터마이즈)과 deprecatedincludeCoAuthoredBy(defaulttrue) - Conventional Commits v1.0.0 —
<type>[scope]: <description>구조,feat/fix와 SemVer,BREAKING CHANGE - 명령 레퍼런스 — 내장 명령 목록(
/diff·/code-review등,/commit은 없음)