Coding Agent의 실수 방지 및 비명령형 태스크 강제 실행을 위한 OSS markgate 개발
요약
Coding Agent가 구현 후 테스트나 빌드와 같은 필수 단계를 누락하거나, 명령어로 정의하기 어려운 태스크를 수행하지 않는 문제를 해결하기 위한 오픈소스 도구 markgate를 소개합니다. markgate는 Agent와 hook 사이에서 강제 레이어(forcing layer) 역할을 수행하여, Agent가 작업을 수행했을 때는 중복 실행을 방지하고 누락했을 때만 작업을 강제 실행합니다.
핵심 포인트
- Coding Agent의 실수(태스크 누락)를 기계적으로 포착하여 강제 실행 가능
- Agent가 이미 작업을 수행한 경우 hook 실행을 스킵하여 이중 실행으로 인한 시간 낭비 방지
- 명령어로 만들기 어려운 비명령형 태스크를 hook을 통해 강제할 수 있음
- 설정 파일을 통해 복수 태스크의 스코프 관리 및 집약적인 검증(verify) 지원
구현 후에 build나 test를 실행하는 Skill을 Coding Agent (Claude Code 등)가 실수로 호출하는 것을 잊어버리고 커밋(commit)해 버리거나, /check-docs와 같은 명령어로 만들 수 없는 AI 태스크를 hook으로 강제할 수 없는 등의 격차를 기계적으로 포착하여 강제 실행하는 OSS 『markgate』를 만들었습니다.
markgate
markgate는 Coding Agent와 hook **사이에 위치하는 강제 레이어 (forcing layer)**로 동작하는 CLI 도구입니다. Coding Agent의 실수나 누락을 기계적으로 포착하고 강제 실행함으로써, 다음과 같은 3가지 실패 패턴에 대응합니다.
| 패턴 | 해결하는 실패 패턴 | 해결 수단 |
|---|---|---|
| 1 | 망각 포착 및 이중 실행 방지 | markgate run |
| 2 | 비명령형 태스크의 강제 실행 | markgate set + markgate verify |
| 3 | 복수 태스크의 스코프 + 집약적 verify | .markgate.yml의 composes |
이하, 각 패턴을 차례대로 살펴보겠습니다.
markgate run
패턴 1: 망각 포착 및 이중 실행 방지 (예를 들어, 구현 skill과 그 이후에 실행하는 체크 skill (/check)이 있다고 가정합니다.)
/check는 구현한 내용에 대해 체크합니다. 예를 들어, test나 typecheck, lint를 비롯해 format이나 build, 문서 정합성 확인, 커버리지 생성, 이미지 스캔 등 프로젝트마다 구현에 대해 보장하고 싶은 내용은 매우 다양할 것입니다.
하지만 Coding Agent에게 "구현 후에는 /check를 실행해줘"라고 말해도, **Coding Agent가 이를 잊어버려 조용히 스킵(skip)**하는 경우가 있습니다. 그럴 경우, 실행하고 싶었던 체크가 돌아가지 않은 채로 커밋(commit)되어 버립니다.
그래서 pre-commit hook을 설정하여 강제적으로 /check에 상당하는 처리를 실행하게 합니다.
하지만 Coding Agent가 항상 잊어버리는 것은 아닙니다. 오히려 잊어버리는 일은 드물며, 평소에는 /check를 잘 실행해 줍니다.
그렇게 되면 "skill로 이미 실행함 → hook에서 또 실행됨"의 이중 실행이 발생하게 됩니다.
이 /check에 의한 처리가 무거운 것이라면, Coding Agent를 통한 신속한 개발 사이클이 오히려 느려지게 됩니다. 가벼운 것이라면 크게 신경 쓰이지 않을 수도 있지만, 빈번하게 실행됨으로써 상당한 시간을 낭비하게 됩니다.
게다가 구현 skill 이후에 체크를 하지 않고 hook에만 체크 처리를 몰아넣으면, 아직 커밋(commit)을 하고 싶지 않을 때 체크 처리를 실행할 수 없습니다. 또한, 파일 변경 시마다 post-edit hook을 실행하는 것도 변경될 때마다 체크 처리가 돌아가기 때문에 시간이 많이 걸리게 됩니다.
markgate run으로 해결
markgate는 /check skill과 hook을 양립하면서, Coding Agent가 /check를 잊었을 경우에만 hook을 발화시키는 것을 실현합니다.
즉, 평소에 Coding Agent가 제대로 /check를 실행해 주는 경우에는 **hook이 스킵(skip)**되므로 이중 실행이 일어나지 않습니다.
사용법
기본적으로는 설정 파일 없이 명령어를 호출하기만 하면 됩니다.
예를 들어, 위 예시의 /check skill이나 pre-commit hook 내에서 pnpm build를 실행하고 있다고 가정해 봅시다. pre-commit hook은 Claude Code에 의한 PreToolUse hook을 통해 git commit* 시점에 발화하도록 설정되어 있다고 가정합니다.
/check skill
- 처음에 실행하는
/checkskill에서markgate run -- <command>명령어를 사용하여pnpm build명령어를 래핑(wrap)합니다.
// ... skill 정의 내에서
- pnpm build
+ markgate run -- pnpm build
2. hook
pre-commit hook 내에서도 마찬가지로 markgate run -- pnpm build로 다시 작성합니다.
{
"hooks": {
"PreToolUse": [{
...
이렇게 하는 것만으로, Coding Agent가 /check를 잊었을 때'만' hook이 발화하여 pnpm build를 실행하는 동작이 됩니다. Coding Agent가 제대로 /check를 실행했을 경우, 후자의 hook은 스킵되며 이중 실행도 발생하지 않습니다.
set + verify
패턴 2: 비명령형 태스크의 강제 실행 (지금까지의 markgate run은 hook 설정 안에 대상 명령어 (pnpm build 등)를 작성하는 방식이었습니다.)
하지만 현실에는 명령어로 만들 수 없는 태스크도 있습니다. 예를 들어 "문서가 src와 일치하는지", "PR description이 diff와 맞는지", "기존 코드와 명명 규칙(naming)의 일관성이 유지되고 있는지" 등은 LLM의 판단이 필요한 리뷰이므로, shell 명령어로 표현할 수 없습니다.
이러한 LLM 태스크는 Coding Agent **자신의 세션 (session)**에서 실행하고 싶습니다. Coding Agent가 쌓아온 컨텍스트 (context) (대화 이력, 열려 있는 파일, 과거의 판단)를 활용하고 싶기 때문입니다. 기술적으로는 hook에서 claude -p를 통해 별도의 세션을 시작하는 것도 가능하지만, 컨텍스트가 공유되지 않기 때문에 이전 작업의 연장선상에서 동작하지 않습니다. 결과적으로 hook 단독으로는 "Coding Agent가 /check-docs와 같은 LLM 태스크를 실행했는지"를 파악할 수 없으며, 이를 강제할 수단이 없다는 뜻입니다.
set + verify로 게이트(gate)를 만들기
그래서 markgate set과 markgate verify를 조합한 **게이트 패턴 (gate pattern)**을 사용합니다. 체크 프로세스는 skill 측에 둔 채로, set을 통해 "체크를 통과한 시점"을 **마커 (marker)**로 기록하고, hook 측은 verify를 통해 "기록된 마커가 현재 시점의 마커와 일치하는지"를 판정하는 **게이트 (gate)**로서 동작하게 하는 방식입니다.
markgate set: 체크를 통과한 시점을 마커로 기록함markgate verify: 기록된 마커가 현재 시점의 마커와 일치하면 exit 0, 일치하지 않으면 exit 1로 중단함
마커는 set을 수행한 시점의 코드 상태를 기록하므로, 그 이후 코드가 변경되면 기록된 마커와 현재 시점의 마커가 불일치하게 되어 verify가 중단시킵니다 (마커의 상세 내용은 후술).
사용법
체크의 실체 (/check-docs skill 등)의 끝부분에서 markgate set을 호출하고, hook 측은 markgate verify를 호출하기만 하면 됩니다.
# /check-docs skill 정의의 끝부분에서
markgate set
// .claude/settings.json (hook 측)
{
"hooks": {
...
일련의 체크 프로세스 정의는 skill 측에 집중된 상태를 유지하면서, hook은 마커를 확인하기만 하는 구조가 됩니다. 마커가 없거나 코드와 일치하지 않을 경우, hook은 exit 1로 차단하며 Coding Agent에게는 에러와 힌트 (run /check-docs)가 전달되므로, 차단되었을 경우의 대응은 Coding Agent에게 맡겨 재루프 (re-loop) 시킬 수 있습니다.
"비명령형 태스크의 첫 실행"과 "망각", 둘 다 포착한다
이 게이트 패턴은 다음 두 가지 케이스 모두에 유효합니다.
- 비명령형 태스크의 첫 실행 케이스: Coding Agent가 애초에
/check-docs의 존재를 파악하지 못하고 commit을 시도함. → 마커가 없으므로 hook이 차단하며, 에러 메시지 (run /check-docs)를 전달함.
)를 읽은 Coding Agent가 처음으로 /check-docs를 실행합니다.
망각 케이스 (ド忘れケース): Coding Agent가 /check-docs를 실행하여 마커(marker)가 설정(set)됩니다. 하지만 그 후 코드를 다시 편집하여 「다시 체크하고 싶은」 타이밍에, Coding Agent가 /check-docs를 재실행하는 것을 잊어버립니다. → 마커가 코드와 일치하지 않게 되므로 hook이 차단하며, Coding Agent에게 재실행을 촉구합니다.
즉, markgate는 최초 미실행도, 망각 후 재실행 누락도, 모두 「마커가 현재 코드 상태와 일치하는가」라는 동일한 로직으로 일괄 포착한다는 의미입니다.
.markgate.yml
composes
)
패턴 3: 스코프(scope)와 집약(aggregation) verify (지금까지는 「skill이 1개, hook도 1개」임을 전제로 해왔으나, 현실에서는 체크가 여러 개 나열되는 경우도 많습니다.
예를 들어 pre-commit에서, 가벼운 체크 (check: typecheck / lint / build / unit test)와 LLM으로 문서 정합성을 확인하는 무거운 체크 (docs)를 실행하고 싶다고 가정해 봅시다.
이때 문제가 되는 것이 바로 헛스윙 (空振り) 입니다. 하나의 게이트(gate)로 묶어버리면, tests만 수정했을 뿐 문서 관련 파일은 아무것도 변하지 않은 커밋에서도 LLM 정합성 체크가 실행되어 버립니다. 코드를 바꿀 때마다 매번 무거운 LLM 체크를 재실행해야 하므로 현실적이지 않습니다.
각 태스크에 자신의 스코프 부여하기
그래서 .markgate.yml을 사용하여 각 체크에 「자신의 스코프 (scope)」 (= include로 지정한 파일군)를 부여합니다. 코드의 변경 사항이 자신의 스코프 내에 포함되는지에 따라 재실행이 필요한지를 판정하는 방식입니다.
나아가 composes를 통해 여러 게이트를 묶는 **집약 게이트 (aggregation gate)**를 만들면, hook 측에서는 단 한 줄의 verify만으로 「묶여 있는 모든 게이트가 현재 코드와 일치하는지」를 판정할 수 있습니다.
사용법
.markgate.yml을 생성합니다 (markgate init 명령으로 템플릿이 생성됩니다).
# .markgate.yml
gates:
check:
...
skill 측에서는 게이트 이름을 인자로 지정하여 각 게이트를 개별적으로 set 합니다.
# /check skill 정의의 끝부분에서
pnpm typecheck && pnpm lint && pnpm test && markgate set check
# /check-docs skill 정의의 끝부분에서
markgate set docs
그리고 pre-commit hook에서는 집약 게이트의 pre-commit을 한 줄로 verify 합니다. 일치하지 않을 경우 markgate status pre-commit을 통해 「어느 게이트가 일치하지 않는지」가 표시되므로, Coding Agent는 이를 보고 해당 skill을 다시 루프(loop)합니다.
// .claude/settings.json
{
"hooks": {
...
}
}
핵심은 편집한 스코프에 따라 필요한 체크만 재실행된다는 점입니다. 각 게이트의 마커는 자신의 include에 포함된 파일군만을 해시(hash) 대상으로 삼기 때문에, 한쪽 스코프만 건드린 커밋에서는 다른 쪽 게이트는 마커가 set 되었을 당시의 상태 그대로 일치하게 되어 재실행이 불필요한 동작이 됩니다.
예를 들어 tests만 수정한 커밋에서는 가벼운 체크만 실행되고, CI 설정 등 include에 포함되지 않는 파일만 수정한 커밋이라면 양쪽 모두 스킵되는 방식으로 전환됩니다.
| 편집 대상 | check | 재실행이 필요한 체크 |
|---|---|---|
tests/** 만 수정 | 불일치 | 일치 |
docs/** / README.md 만 수정 | 일치 | 불일치 |
src/** | 불일치 | 불일치 |
| 위 중 어느 곳에도 해당하지 않음 (CI 설정, 에디터 설정 등) | 일치 | 일치 |
참고로, 이 예시의 pre-commit
집약 게이트(aggregate gate)는 자체적인 명령어를 가지지 않는 구조로 설계되어 있으며, 포함된 모든 게이트가 일치하면 일치, 하나라도 일치하지 않으면 불일치로 판정하는 AND 방식의 판정만을 담당합니다. 자체 명령어를 가지지 않는 이 형태는 markgate run (단일 명령어를 래핑하는 방식)으로는 작성할 수 없으며, 게이트 패턴 (set + verify)으로만 구현할 수 있습니다.
다른 유스케이스 예시
markgate는 위의 기본적인 사용법 외에도 다양한 응용 패턴이 있습니다.
- PR 생성 전에 문서 정합성 체크 (
/check-docs→gh pr createhook) - image push 전에 취약점 스캔 (
trivy image→docker pushhook) - push 전에 커버리지 리포트 생성 (
go test -cover→git pushhook) - PR merge 전에 E2E 테스트의 최신 실행을 보장 (
pnpm test:e2e→gh pr mergehook)
동작 원리: 마커(Marker)로 상태 관리
지금까지 여러 번 언급된 "마커"의 내부를 살펴보겠습니다.
markgate는 대상 명령어(또는 태스크)가 성공한 시점의 **"상태"**를 마커로 저장하고, 다음번에 동일한 명령어(또는 태스크)가 호출되었을 때, 마커가 현재 상태와 일치하면 스킵(skip), 일치하지 않으면 실행 (markgate run) / 차단 (markgate verify) 하는 방식으로 동작합니다.
이 "상태"는 실질적으로 **코드 내용의 해시값 (hash value)**입니다. 해시 대상은 "특정 파일군" (hash: files + include)과 "리포지토리 전체의 git status" (hash: git-tree, 기본값) 두 가지 중에서 선택할 수 있습니다. 어느 쪽이든, 코드 내용이 바뀌지 않는 한 명령어가 스킵된다는 것을 의미합니다.
# 최초 실행: 마커가 없으므로 pnpm build가 실행되고, 성공 시 마커 저장
$ markgate run -- pnpm build
building...
...
마커 파일은 기본적으로 .git 내부 (.git/markgate/default.json)에 저장되므로, 직접 마커용 파일을 만들 필요가 없으며 명시적으로 .gitignore에 추가하지 않아도 git 관리 대상에 포함되지 않습니다. (명시적으로 마커 파일의 경로를 변경하여 CI와 로컬에서 마커를 공유하는 옵션도 마련되어 있으나, 이는 README를 참조해 주세요.)
설치
# Homebrew
brew install go-to-k/tap/markgate
# shell script
...
다른 hook manager에서 사용하기
여기까지는 Claude Code의 hook을 예로 들었지만, markgate는 hook manager에 의존하지 않습니다. husky / lefthook / pre-commit framework 등 어떤 hook manager에서도 markgate run -- <command>를 한 줄 작성하는 것만으로 사용할 수 있습니다.
# .husky/pre-commit
markgate run -- pnpm test
# lefthook.yml
pre-commit:
commands:
...
# .pre-commit-config.yaml (pre-commit framework)
repos:
- repo: local
...
마치며
마치며
개인적으로도 AI 시대에 유용하게 쓰일 도구가 되었다고 생각합니다. 특히, **"AI의 실수나 누락을 기계적으로 포착한다"**는 사고방식은 AI를 활용하는 데 있어 중요한 포인트 중 하나라고 느낍니다.
그 외의 기능(.markgate.yml에서의 exclude / state_dir 설정, 로컬과 CI에서의 마커 공유, requires를 통한 엄격한(strict) 게이트 의존성 등)에 대해서는 GitHub 리포지토리(repository)의 README를 참조해 주세요. 마음에 드셨다면 GitHub 리포지토리에 ⭐를 눌러주시면 감사하겠습니다.
또한, markgate를 실제로 활용하고 있는 프로젝트로, AWS CDK CLI를 풀 스크래치(full-scratch)로 구현하여 폭속으로 만든 cdkd (CDK Direct)가 있습니다. 거의 모든 구현을 Coding Agent에게 맡겨 개발하고 있으며, 그 실행 누락이나 비명령형 태스크(non-command task)를 markgate가 기계적으로 뒷받침하고 있습니다. 이 프로젝트도 꼭 참고해 보시기 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기