안전 가드만은 의도적으로 참조하지 않는다. 단일 부모 리포지토리로 7개 리포지토리를 지휘하는 Claude Code 설계
요약
AI와 함께 개발하는 여러 리포지토리에서 스킬과 안전 가드가 개별적으로 성장하며 불일치 문제가 발생합니다. 이 글은 이러한 문제를 해결하기 위해, 모든 하위 리포지토리를 하나의 부모 리포지토리로 통합 관리하는 설계 방식을 제시합니다. 이를 통해 AI가 제어할 수 있는 '정본'을 중앙화하여 개발 과정의 일관성을 확보하는 것이 핵심 목표입니다.
핵심 포인트
- AI 스킬과 안전 가드가 여러 리포에서 개별적으로 성장하며 불일치 문제가 발생함.
- 개발 절차의 차이는 코드 줄 수와 기능적 누락(예: web vs admin)으로 나타남.
- Claude Code의 '안전 가드'는 원격 DB 변경이나 force push를 막아주는 중요한 역할을 함.
- 모든 리포지토리의 '정본'을 하나의 부모 리포지토리에서 관리하는 것이 핵심 해결책임.
😩 "웹에서 수정한 스킬을 admin과 service에도 복사해야 한다..."
🤔 "어? admin의 create-pr은 이런 절차였지?"
😱 "이 리포의 가드가 PR 병합을 막고 있지 않네...?"
AI와 함께 개발하는 리포지토리가 늘어나면서, 이런 일이 발생하지 않나요?
하나의 리포에서 키운 스킬이나 설정을 복사하여 배포하는 순간부터 어긋나기 시작합니다.
개인적으로 개발한 커뮤니티 서비스 '아지피타(あじぴた)'는 현재 7개의 리포지토리로 구동되고 있습니다.
그중 3개의 리포에서는 Claude Code의 스킬과 안전 가드가 각기 다른 방식으로 성장했습니다😇
이 글에서는,
- 같아야 할 스킬과 가드가 얼마나 어긋났는지 - 스킬은 Claude Code의 플러그인으로 만들어 한 곳에서 참조하게 만든 이야기 - 안전 가드만은 의도적으로 복사하여 PR로 배포하기로 결정한 이유 - 다른 리포지토리들을 하나의 부모 리포(親リポ)에서 지휘할 때의 함정
에 대해 다룹니다🧭
먼저, 아지피타의 리포지토리 구성을 보여드립니다.
| 리포지토리 | 역할 | 코드 | |
|---|---|---||
| web | 앱 본체. DB 스키마도 여기서 관리 | 있음 |
| ... | 부모(親) | 다른 6개를 묶어 지휘함 (이 글의 주인공) | 없음 |
AI와 함께 개발 절차를 진행하는 것은 web, admin, service 세 가지입니다.
Issue를 정리하고, 구현하고, PR을 만들고, 리뷰에 대응합니다. 이러한 절차들을 Claude Code의 스킬로 만듭니다📝
스킬이 가장 잘 성장한 곳은 가장 자주 접하는 web이었습니다.
리뷰에서 지적받을 때마다 절차를 수정했고, admin과 service에는 기억나면 수동으로 복사하여 적용했습니다. 결과적으로, 이름은 같지만 다음과 같이 스킬들이 달라졌습니다.
| 스킬 | web | admin | service |
|---|---|---|---|
| create-pr | 193행 | 151행 | 127행 |
| work-on-issue | 163행 | 87행 | 86행 |
| extract-component | 259행 | 103행 | 103행 |
줄 수의 차이는 곧 절차의 차이입니다.
예를 들어, web의 create-pr에는 승인 조건을 한 건씩 '만족함/미흡함/이 PR로는 불가능'으로 나누어 처리한 후 PR을 만드는 절차가 있었습니다. 하지만 admin과 service에는 그 과정이 없습니다. service에는 Copilot의 리뷰는 자동으로 붙지만, 수동으로 추가해야 하는 오래된 절차까지 남아있었습니다😅
스킬보다 더 곤란했던 것이 안전 가드였습니다.
Claude Code의 hook을 사용하여 커맨드를 실행하기 직전에 문자열을 검사하고, .env 파일에 대한 쓰기나 force push를 막고 있습니다. 이 역시 3개 리포에서 개별적으로 성장했습니다.
| 리포지토리 | 막는 규칙 수 | 예시 |
|---|---|---|
| web | 8 | main / develop으로의 직접 push, PR 병합, supabase db push 등 원격 DB에 대한 변경 |
| ... | .env ・Claude Code 설정 파일 ・force push ・git reset --hard 등만 가능 |
web에만 있는 규칙이 있고, admin에만 있는 규칙도 있었습니다. 어떤 리포에서 작업하느냐에 따라 AI가 막을 수 있는 조작이 다른 상태였습니다🙈
해야 할 일은 '정본(正本)'을 한 곳에 두는 것입니다.
그 위치는 나머지 6개 리포를 묶는 부모 디렉토리로 정했습니다. 원래 모든 리포를 모아두었던 장소를 부모 리포로서 Git으로 관리하기 시작한 것입니다. 다만, 무엇이든 같은 방법으로 배포한 것은 아니었습니다.
⚠️ [IMG:N] 형식 토큰은 이미지 placeholder 입니다. 번역하지 말고 원래 위치에 그대로 유지하세요.
| 공유 항목 | 원본 위치/저장소 | 각 리포지토리로 전달하는 방법 | 그 방법을 사용하는 이유 |
|---|---|---|---|
| 공통 스킬 (7개) | 부모 리포지토리 | 플러그인으로 참조 (복사하지 않음) | 수정하면 다음 세션부터 모든 리포지토리에 적용되기를 원함 |
| 안전 가드 | 부모 리포지토리 | 복사를 동기화 PR로 배포 | 부모가 없는 환경에서도 작동시키고 싶음. 변경은 사람이 확인한 후에 적용하고 싶음 |
| 규약/디자인 토큰 | web | 복사를 동기화 PR로 배포 | 토큰이나 로고는 코드가 import 함. 규약은 리포지토리 단독으로 열린 AI도 읽을 수 있음 |
분리 기준은 **'각 리포지토리에 파일 실물이 필요한가'**와 '변경 사항이 임의로 적용되어 문제가 생기는가' 두 가지입니다💡
스킬은 절차서(手順書)이므로, 수정하면 즉시 적용되기를 원합니다.
가드는 사고를 막는 장치이므로, 변경 사항은 반드시 사람이 확인한 후에 적용하고 싶습니다.
이러한 차이가 전달 방식의 차이를 만들었습니다.
Claude Code에는 스킬이나 hook을 모아서 배포하는 플러그인(plugin) 메커니즘이 있습니다.
플러그인은 '마켓플레이스'에서 가져올 수 있지만, 로컬 디렉토리를 마켓플레이스로 지정할 수도 있습니다🔌
부모 리포지토리에는 마켓플레이스 정의를 두었습니다.
// .claude-plugin/marketplace.json (부모 리포지토리 발췌)
{
"name": "ajipita",
...
사용자 설정(~/.claude/settings.json의 extraKnownMarketplaces)에서 이 디렉토리를 마켓플레이스로 등록합니다.
각 리포지토리에서는 플러그인을 활성화하기만 하면 됩니다.
// .claude/settings.json (web・admin・service)
{
"enabledPlugins": { "aj@ajipita": true }
...
로컬 디렉토리를 등록했기 때문에, 부모의 스킬을 수정하면 재설치나 복사 과정 없이 다음 세션부터 모든 리포지토리에 전달됩니다✨
확인용 스킬을 부모에 추가하여 web과 admin에서 호출해 보았습니다.
내용을 수정하면 다음에 실행되는 세션에서는 수정된 내용이 반환됩니다.
플러그인의 캐시(~/.claude/plugins/cache/)는 이전 상태로 남아있어 변경되지 않았기 때문에, 캐시가 아닌 원본 디렉토리를 읽고 있는 것 같습니다🔍
플러그인의 스킬은 /aj:create-pr와 같은 접두사(prefix)를 붙여 호출할 수 있습니다.
이름이 다른 항목과 겹치지 않는다면, /create-pr 그대로 호출하는 것도 확인했습니다. 습관이 된 커맨드를 바꾸지 않아도 됩니다.
다만, 스킬을 하나로 통합하면 문제가 생길 때가 있습니다.
리포지토리마다 다른 값이 존재하기 때문입니다. PR의 병합 대상 브랜치, 검증 명령어, 독립 리뷰(문맥을 가지지 않은 서브 에이전트에 의한 리뷰)에서 보는 관점 등이 그렇습니다.
그래서 스킬 본문에서 리포지토리에 고유한 값을 제거하고, 각 리포지토리의 .claude/profile.md에 작성하기로 했습니다.
<!-- Before (web의 create-pr/SKILL.md・발췌) -->
### 3. lint와 build를 확인한다
npm run lint
...
예를 들어 service는 테스트 명령어가 web과 다르기 때문에, profile에는 다음과 같이 작성합니다.
<!-- .claude/profile.md (service) -->
## 검증 명령어
- npm run lint
...
스킬은 ## 검증 명령어와 같은 제목을 단서로 profile에서 값을 읽어옵니다.
해당되지 않는 항목도 제목은 지우지 않고 '없음'이라고 작성하여, 누락과 정말 없는 것을 구별합니다.
profile 자체가 없을 때는, 추측으로 값을 채워 넣지 않고 멈추도록 했습니다🔍
결과는 다음과 같습니다.
| Before | After | |
|---|---|---|
| 스킬 본문 | 3개 리포지토리에서 총 약 2,500줄 (오차 있음) | 부모에 1,117줄 (단 하나만) |
| ... |
스킬 개선은 대부분 각 리포지토리에서 작업하는 도중에 떠오릅니다.
그래서 각 리포지토리의 세션에서 부모 스킬을 편집할 수 있도록 했습니다.
// .claude/settings.json (각 리포지토리)
{
"permissions": {
...
핵심은 수정 가능한 범위를 skills/에만 국한시킨 것입니다.
같은 부모 리포지토리에 있는 가드(Guard)의 원본은 각 리포 설정에서 편집을 거부(deny)하도록 했으며, 가드 자체도 Bash를 통한 수정을 막고 있습니다.
가드는 개별 리포지토리 세션에서는 수정할 수 없습니다. 이유는 다음 절에 설명합니다🔒
가드를 플러그인으로 참조하게 한다면 복사본은 필요하지 않습니다.
하지만 그럼에도 불구하고 복사본을 배포하기로 한 이유는 2가지입니다.
첫 번째는, 부모 디렉토리가 없는 환경에서도 작동시키고 싶기 때문입니다.
클라우드 실행 환경 등에서 리포지토리를 단독으로 열 경우, ../plugins/ 경로는 존재하지 않습니다.
이때 스킬(Skill)만 없다면 문제가 되지 않지만, 가드가 없으면 곤란합니다. 그래서 각 리포의 .claude/hooks/ 경로에 복사본을 배치합니다.
두 번째는, 가드의 변경 사항은 사람이 검토한 후에 적용하고 싶기 때문입니다.
스킬과 마찬가지로 '수정하면 다음부터 작동한다'는 방식이라면, 가드를 완화하는 변경도 아무도 보지 못하는 사이에 적용되어 버릴 수 있습니다😨
복사본은 부모 리포의 GitHub Actions가 배포합니다.
부모 리포의 main에서 가드가 변경됨
↓
① 부모에서 가드의 회귀 테스트(regression test)를 실행함
...
동기화 PR은 병합되지 않는 한 반영되지 않습니다.
여기서 사람이 차이점(diff)을 보고, '막고 싶은 작업이 막히지는 않는지, 평소 작업에 지장이 없는지'를 확인합니다👀
복사본이 AI에 의해 직접 수정되는 것을 막기 위해, 각 리포 설정에서 .claude/hooks/의 편집을 금지했습니다.
수정하려면 부모의 원본을 수정하고, PR로 전달된 것을 기다려야 합니다. 가드를 변경하는 경로는 하나로 통일했습니다.
공통 가드는 3개 리포의 규칙을 모두 합친 9가지 규칙으로 만들었습니다.
web 보호 브랜치의 규칙과 admin 우회 방지 대책이 어떤 리포에서도 작동하게 됩니다.
다만, 모든 것을 합치자 문제가 되는 규칙이 하나 있었습니다. **보호 브랜치에 대한 직접 푸시(direct push)**입니다.
web・admin・service는 PR을 통한 방식이 정규 운영이지만, growth나 articles는 직접 커밋이 정규 운영입니다.
그래서 푸시 대상 리포를 origin의 리포 이름으로 판별하여, 리포마다 다르게 적용하고 있습니다.
repo_has_protected_branches() {
name=$(git -C "$1" remote get-url origin 2>/dev/null | sed -E 's#.*/##; s/\.git$//')
case "$name" in
...
리포지토리를 알 수 없을 때는, 막는 쪽으로 기본 설정합니다.
회귀 테스트는 102건입니다. 그중 33건은 '막아서는 안 되는 명령을 막지 않는 것'을 확인하는 것입니다.
가드는 너무 많이 막으면 평소 작업 전체가 멈추기 때문입니다🧪
원본을 부모에 두었으니, 작업 요청도 부모에서 받도록 했습니다.
'즐겨찾기를 목록으로 보고 싶다', '오늘의 기사를 써줘'와 같은 잡다한 말들을 /do라는 스킬로 받습니다.
/do가 하는 일은 판단과, 각 리포의 스킬로 연결하는 역할만 합니다.
- 어떤 리포에서 무엇을 할지 코드를 읽고 Issue를 읽어 결정함
- 요청 말로부터,
어디서 멈출지를 결정합니다 - 각 리포의 규칙과 스킬을 읽고, 그 절차대로 진행합니다
멈추는 지점은 요청 말에 의해 결정됩니다.
| 요청 말 |
|---|---|
| ~을 추가하고 싶다 / ~을 고치고 싶다 | Issue 생성 |
| ... |
지시받은 지점보다 앞으로는 진행하지 않습니다.
PR 병합, 운영 환경 적용, 사용자에게 전달되는 배포는 어떤 요청이든 제 손으로 직접 합니다🔑
부모 디렉토리 자체도 Git으로 관리하기 시작했기 때문에, 서브 리포로 cd를 잊어도 git은 오류가 나지 않습니다.
부모 리포에 대해 커밋이나 푸시가
성공해 버립니다😱
대책은 Bash를 매번 cd <리포의 절대 경로> && …로 시작하는 것입니다.
/do의 절차와 서브 에이전트(sub-agent)에 대한 지시 모두 여기에 작성되어 있습니다.
가드는 명령어 내부의 cd를 추적하여, 푸시 대상 리포지토리를 판별합니다.
그런데 cd $R/ajipita-web && git push origin main처럼 변수로 cd를 하면, 목적지를 알 수 없습니다.
공통화 이전, 부모 가드의 초기 구현에서는 모르는 경우 직전 위치 그대로로 판별했습니다.
성장(growth) 브랜치(직접 커밋이 정식인 곳)에 있는 상태에서 이것을 실행하면, web의 main으로 푸시가 통과되어 버립니다🕳️
# 이전: 해결할 수 없으면, 직전 위치(growth) 그대로 판정 → 통과
# 이후: 변수를 포함하는 경로는 '리포지토리 미확인'으로 처리하여, 멈추는 쪽으로 변경
case "$p" in *'$'*) printf '%s' "unresolved_dir"; return ;; esac
이것은 부모(parent)에서 여러 리포지토리를 다루게 되면서 처음 발견한 허점이었습니다.
각 리포지토리에서 부모의 스킬을 바로 사용할 수 있도록, additionalDirectories에 ../plugins/aj/skills/를 추가했습니다.
그런데 처음에는 CLI가 '이 설정을 무시한다'는 경고를 내보냈습니다😵
원인은 해당 폴더를 아직 신뢰하지 않았기 때문입니다.
대화 모드(interactive mode)에서 한 번 폴더를 열어 신뢰하면 적용되었습니다. 설정 파일만 배포해서는 작동하지 않으므로, 새로운 PC나 리포지토리에서는 처음에 한번 여는 절차가 필요합니다.
부모 디렉토리에서 Claude Code를 실행했을 때, 제 환경에서는 서브 리포지토리에 있는 .claude/의 스킬, 권한(permission), hooks가 작동하지 않았습니다.
서브 리포지토리의 스킬을 호출하면 Unknown skill이 됩니다. CLAUDE.md 역시 실행 시에는 로드되지 않습니다. 따라서 /do에서는 작업 전에 대상 리포지토리의 규칙을 직접 읽고, 스킬은 SKILL.md를 읽어 절차대로 진행하기로 결정했습니다.
부모에서 지휘할 때는, 서브 리포지토리의 '당연한 것'이 작동하지 않는다는 전제하에 움직여야 합니다📌
처음에는 동기화(synchronization) Action을 매일 실행하도록 설정했습니다. 하지만 규약이나 가드(guard)는 매일 변하지 않습니다. 대부분의 실행은 '차이점 없음'으로 끝날 것이기 때문입니다💸
그래서, 한 번도 정기 실행이 돌지 않는 사이에 설정을 제외하고, 부모 리포에서 가드가 변경되었을 때와, web의 develop 브랜치에서 동기화 대상 파일이 변경되었을 때만 작동하도록 했습니다.
web 쪽에, 변경된 파일로 범위를 좁혀서 부모의 Action을 실행하는 workflow를 배치해 두었습니다.
web으로부터의 자동 실행은 실제로 작동하는지 아직 확인하지 못했습니다(동기화 PR 자체는 Action을 수동으로 실행하여 도착하는 것은 확인했습니다).
-
스킬이나 설정은 복사해서 배포할 경우, 배포한 순간부터 어긋나기 시작합니다. 같은 create-pr이 193행, 151행, 127행이었다는 점처럼 - 원본은 한 곳에만 있어야 합니다. 다만 배포 방식은, 각 리포지토리에 파일 실물이 필요한지 / 변경 사항이 멋대로 적용되어 문제가 되는지에 따라 나눕니다 - 스킬은 Claude Code의 플러그인으로 만들어 참조하게 합니다. 리포지토리별 차이는 profile로 분리합니다 - 안전 가드는 복사본을 동기화 PR로 배포합니다. 변경 사항은 한 번 사람이 확인한 후에 적용시킵니다.
-
스킬은
-
가드는 각 리포지토리의 규칙을 모두 합치고, 리포마다 다른 규칙은 origin의 리포 이름을 기준으로 전환합니다. 모르는 것이 있으면 멈추는 쪽으로 합니다 - 부모에서 지휘할 때는,
cd를 잊지 말고, 서브 리포지토리 설정이 작동하지 않는다는 점에 주의해야 합니다
리포지토리가 두 번째가 되었을 때, '어떤 것이 원본인지'를 정해두었으면 좋았겠다는 생각이 듭니다.
여러 리포지토리에서 AI와 개발하는 분들의 참고가 되기를 바랍니다🙏
이 시리즈에서는 개인 개발 서비스인 '아지피타(あじぴた)'의 설계를 주제별로 작성하고 있습니다.
서비스 전체 개요나 다른 글 목록은 허브 기사에 모아두었습니다👇
この記事と関係の深い記事です。
🔗 신입이 첫날 헤매는 것은 AI도 매번 헤맵니다. 그래서 암묵적인 규칙을 모두 말로 만들었다 (하나의 리포지토리에서 스킬, hooks, 메모를 만든 이야기)
🔗 CI가 전록(全緑)이라도 머지해서는 안 되는 PR이 있었다. GitHub Actions는 '어디서 멈출지'로 설계한다 (PR의 병합을 사람에게 남기는 생각 방식)
설계의 내용으로 읽고 싶은 분들을 위해 해부 문서도 공개하고 있습니다🔬
🔗 아지피타 내부(ajipita-inside)
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기