5분 AI.
TRENDSHIFT #10 딥다이브 · 2026-08-24 분석

unlazy 딥다이브
— AI에게 “다 했어요”를 증명하게 만드는 법

AI 코딩 에이전트가 일을 대충 끝내고 “완료했습니다”라고 보고하는 문제를, 설득이 아니라 검사로 푸는 저장소입니다. 사람이 미리 적어둔 검증 명령을 실제로 실행해 종료 코드와 출력이 맞아야만 완료로 쳐줍니다. 의존성 0개의 순수 Node.js 코드 약 1,570줄에 자체 테스트 64개가 붙어 있습니다. (저장소: Leonxlnx/unlazy · ⭐1.88k · JavaScript(ESM) · MIT · 2026-08-09 생성 · 2026-08-24 기준)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

unlazy가 정확히 무엇을 하는 물건인가

unlazy는 Claude Code와 Codex CLI에 설치하는 “완료 규율” 스킬입니다. 한 문장으로 줄이면 이렇습니다 — 에이전트가 “끝냈습니다”라고 말하는 것과, 끝났다는 사실이 실제로 참인 것은 다른 일이라는 전제 위에 만들어진 도구입니다.

작동 방식은 의외로 단순합니다. 작업을 시작할 때 GATES.md라는 파일에 “이 일이 끝났다면 이 명령이 이렇게 나와야 한다”를 미리 적어 둡니다. 그리고 에이전트가 완료를 선언하려 할 때, gate-check.mjs라는 CLI가 그 명령을 진짜로 실행합니다. 종료 코드가 0이고 출력이 기대한 패턴과 맞아야만 그 항목이 “met(충족)”으로 바뀝니다.

TERM
게이트(gate)
통과해야 지나갈 수 있는 관문. 여기서는 GATES.md에 적힌 검증 항목 하나를 뜻합니다. 각 게이트에는 실행할 명령(CHECK:)과 기대하는 출력(EXPECT:)이 붙어 있습니다.
TERM
원장(ledger)
회계 장부처럼 “무엇이 아직 안 끝났는지”를 한 곳에 적어 두는 파일. unlazy에서는 GATES.md가 원장이고, 대화 기록이 아니라 파일이 진실의 원천입니다. 대화창이 초기화돼도 원장은 남습니다.
한 문장 비유

“다 청소했어요”라고 말하는 아이에게, 방 사진을 찍어 보내라고 하는 것

말로 하는 보고는 검증할 수 없습니다. 그런데 “책상 위에 물건이 0개인 사진”이라는 조건을 일을 시키기 전에 못 박아 두면, 보고가 참인지 거짓인지를 사람의 판단 없이도 가릴 수 있습니다.

unlazy가 하는 일이 딱 이것입니다. 다만 사진 대신 셸 명령의 종료 코드와 출력을 증거로 씁니다. 그리고 그 조건은 일이 시작되기 전에 확정돼야 합니다 — 나중에 조건을 낮추면 아무 의미가 없기 때문입니다.

실제 사용은 이렇게 생겼습니다.

# 1) 스킬 설치
npx skills add Leonxlnx/unlazy

# 2) 에이전트에게 지시 (트리 깊이 5로 분해해서 하라는 뜻)
/unlazy tree 5 결제 모듈을 리팩터링하고 모든 마이그레이션 경로를 검증해줘

# 3) 게이트 상태만 확인 — 아무것도 실행하지 않음
node scripts/gate-check.mjs --status GATES.md

# 4) 사람이 검토한 뒤 승인 + 실제 실행
node scripts/gate-check.mjs --approve GATES.md

--status--approve가 나뉘어 있다는 점이 중요합니다. --status는 원장을 읽기만 하고 어떤 명령도 실행하지 않습니다. 실행은 사람이 명시적으로 승인한 뒤에만 일어납니다. 왜 이렇게 나눴는지는 4장에서 다룹니다.

2왜 주목받는가

2주 만에 별 1,800개 — 경쟁 접근법과 무엇이 다른가

이 저장소는 2026년 8월 9일에 만들어졌습니다. 이 글을 쓰는 8월 24일 기준 15일 만에 ⭐1,875 · 포크 110개입니다. 코드는 스크립트 5개, 총 1,570줄 남짓입니다. 무엇이 이 속도를 만들었을까요.

배경: “에이전트 게으름”은 실재하는 문제다

AI 코딩 에이전트를 써 본 사람이면 익숙한 장면이 있습니다. 파일 10개를 고쳐야 하는데 3개만 고치고 “주요 변경을 완료했습니다”라고 하거나, 테스트를 돌리지도 않고 “통과할 것으로 예상됩니다”라고 하거나, 어려운 부분에 // TODO: 나중에 구현을 남기고 끝내는 경우입니다.

TERM
조기 완료(premature completion) · 언더싱킹(underthinking)
모델이 실제로는 작업이 남았는데도 종료 신호를 내는 현상, 그리고 충분히 따져보지 않고 첫 번째로 떠오른 답으로 마무리하는 현상. 2025~2026년에 이 주제를 다룬 논문이 여러 편 나왔고 unlazy의 README도 이들을 인용합니다.

기존 해법들과의 차이

접근방식한계
“끝까지 해” 프롬프트사람이 계속 재촉사람이 매번 지켜봐야 함. 모델이 또 거짓 완료를 하면 다시 반복
Plan 모드 · TodoWrite계획을 먼저 쓰게 함계획을 세우는 것과 계획대로 했는지 확인하는 것은 별개. 체크박스는 모델이 스스로 체크함
멀티에이전트 프레임워크
(BMAD-METHOD, Claude-Flow류)
검토자 에이전트를 따로 둠프레임워크를 통째로 도입해야 함. 검토자도 결국 LLM이라 같은 방식으로 속을 수 있음
unlazy검증 명령을 실제 프로세스로 실행검증 명령을 사람이 잘 써야 함 (뒤에서 다룸)

핵심 차이는 증거의 출처입니다. 앞의 세 가지는 모두 최종 판단을 언어 모델에게 맡깁니다. unlazy는 판단을 운영체제에게 맡깁니다. npm test가 exit 1을 내면 그건 협상의 여지가 없는 사실입니다.

Stop hook — 프롬프트를 무시해도 막히는 층

더 흥미로운 건 선택 기능인 Stop hook입니다. Claude Code에는 세션이 끝나려 할 때 외부 프로그램에게 물어보는 훅이 있는데, unlazy는 여기에 scripts/stop-hook.mjs를 꽂습니다. 게이트가 아직 안 끝났으면 {"decision":"block"}을 돌려줘서 세션 종료 자체를 플랫폼 레벨에서 거부합니다 — 모델이 지침을 무시하기로 결심해도 소용이 없다는 뜻입니다.

비유

프롬프트가 “퇴근 전에 정리하고 가세요”라는 메모라면, Stop hook은 출입문 잠금장치입니다. 메모는 무시할 수 있지만 문은 안 열립니다.

이례적인 정직함

이 저장소가 개발자들 사이에서 화제가 된 또 하나의 이유는 문서 태도입니다. 초기 버전의 README에는 “6회 비교 실험으로 효과를 입증했다”는 주장이 있었는데, 저자가 스스로 그 주장을 철회했습니다. 원자료가 남아 있지 않아 재현할 수 없다는 이유였습니다.

GOOD PRACTICE
research/validation-protocol.md

철회로 끝내지 않고, 앞으로 어떻게 실험해야 재현 가능한지를 문서로 남겼습니다. 인용한 arXiv 논문들에 대해서도 “이들은 문제가 실재한다는 근거이지, unlazy가 그 문제를 해결한다는 근거가 아니다”라고 선을 긋습니다.

AI 도구 마케팅 문서 대부분이 검증되지 않은 수치를 인용하는 상황에서, 이 태도 자체가 신뢰 신호로 읽혔습니다.

커뮤니티 반응도 실제로 코드에 반영됐습니다. 외부 기여자 최소 5명의 PR이 머지됐고, 총 커밋 31개 중 상당수가 Windows 호환성·타임아웃 처리 같은 실사용 버그 수정입니다.

3기술 스택 전체 지도

의존성 0개 — 이게 무슨 뜻이고 왜 중요한가

이 저장소의 package.json에는 dependencies 필드 자체가 없습니다. 실수가 아니라 설계입니다.

{
  "name": "unlazy-skill",
  "version": "2.1.0",
  "private": true,
  "type": "module",
  "engines": { "node": ">=16" }
  // dependencies · devDependencies 둘 다 없음
}
왜 중요한가

이 도구는 남의 컴퓨터에서 셸 명령을 실행하는 프로그램입니다. npm 패키지 하나를 끌어오면 그 패키지의 의존성, 또 그 의존성의 의존성이 전부 같은 권한으로 돌아갑니다. 공급망 공격의 표적이 되기 딱 좋은 자리입니다.

그래서 저자는 파일 잠금·원자적 쓰기·프로세스 그룹 종료 같은 까다로운 기능까지 전부 Node 표준 라이브러리로 직접 구현했습니다. 이 선택이 4장에서 볼 코드의 성격을 거의 다 결정합니다.

3-1. 런타임 · 언어

항목메모
언어JavaScript 100%약 122KB. TypeScript 아님, 빌드 단계 없음
모듈 방식ESM (.mjs)"type":"module" + 확장자 .mjs로 이중 명시
런타임Node.js 16 이상engines.node에 명시. CI가 16/20/24 세 버전 검증
런타임 의존성0개표준 모듈만: node:fs node:child_process node:worker_threads node:crypto node:path node:os node:url
패키지 이름unlazy-skill@2.1.0private:true — npm 레지스트리에 올라가지 않음
라이선스MIT (2026, Leonxlnx)상업적 사용·수정·재배포 자유

3-2. 배포 · 설치 경로

npm이 아니라 npx skills add로 배포됩니다. 이건 vercel-labs가 만든 “AI 에이전트 스킬” 설치 CLI로, GitHub 저장소를 에이전트의 스킬 디렉토리에 직접 심어 줍니다.

npx skills add Leonxlnx/unlazy          # 현재 프로젝트에 설치
npx skills add Leonxlnx/unlazy -g       # 사용자 전역
npx skills add Leonxlnx/unlazy --all    # 감지된 모든 에이전트에

# 수동 설치 시 위치
~/.claude/skills/unlazy   # Claude Code
~/.codex/skills/unlazy    # Codex CLI

3-3. CI · 테스트

.github/workflows/test.yml3(OS) × 3(Node 버전) = 9개 조합을 돌립니다. Windows를 진지하게 지원한다는 신호이고, 실제로 코드 곳곳에 Windows 전용 분기가 있습니다.

OSubuntu-latest · windows-latest · macos-latest
Node16 · 20 · 24
액션 고정actions/checkout@3d3c42e...처럼 커밋 SHA로 핀 고정 (태그는 옮겨질 수 있으므로)

테스트 프레임워크도 안 씁니다. Jest도 Mocha도 없이 tests/ 아래 .mjs 러너 4개가 child_process.execFile로 실제 CLI를 서브프로세스로 띄워 검증합니다.

스위트테스트 수확인하는 것
run-tests.mjs26파싱·승인·실행 기본 동작
hardening-tests.mjs19공격적 입력에 대한 방어 (ReDoS, 경로 탈출 등)
stress-tests.mjs10동시 실행·잠금 경합
self-check.mjs9저장소 자체 무결성 (템플릿·문서 일관성)
합계 64개클론 후 npm test 실행 시 64/64 통과 확인

3-4. 저장소는 어디에?

데이터베이스가 없습니다. 상태는 전부 파일시스템에 있습니다.

경로내용
GATES.md원장. 사람이 읽고 쓰는 Markdown
~/.unlazy/approved/*.json승인 기록 (해시 기반, 5-2절 참고)
.unlazy/<scope>/병렬 작업 단위별 상태
.unlazy/locks/*.lease파일 소유권 리스(임대) 잠금

4아키텍처 심화 분석

1,570줄에 들어 있는 설계 결정 다섯 가지

4-0. 전체 흐름도

┌─────────────────────────────────────────────────────────────┐ │ 사람 / 에이전트 │ └───────────────┬─────────────────────────────────────────────┘ │ ① "/unlazy tree 5 ..." 지시 ▼ ┌─────────────────────────────────────────────────────────────┐ │ SKILL.md (에이전트가 읽는 지침 · frontmatter가 트리거) │ │ └─ references/*.md 로 상세 규칙 분리 (토큰 절약) │ └───────────────┬─────────────────────────────────────────────┘ │ ② templates/gates-leaf.md 복사해 원장 작성 ▼ ┌─────────────────────────────────────────────────────────────┐ │ GATES.md │ │ - ID: build │ │ CHECK: npm run build │ │ EXPECT: /built in \d+ms/ │ │ OWNS: src/** │ └───────────────┬─────────────────────────────────────────────┘ │ ┌──────────┴──────────┐ │ │ ▼ ③ --status ▼ ④ --approve ┌─────────────┐ ┌──────────────────────────────────────┐ │ 파싱만 함 │ │ scripts/gate-check.mjs │ │ 실행 0회 │ │ 1. oracle() → SHA-256 서명 │ └─────────────┘ │ 2. ~/.unlazy/approved/ 대조 │ │ 3. 리스 확보 (locks/*.lease) │ │ 4. child_process 로 CHECK 실행 │ │ 5. Worker 로 EXPECT 정규식 매칭 │ │ (250ms 타임아웃) │ │ 6. 원자적 쓰기로 met/unmet 기록 │ └──────────────┬───────────────────────┘ │ ⑤ 결과 반영 ▼ ┌─────────────────────────────────────────────────────────────┐ │ scripts/stop-hook.mjs (선택 설치) │ │ 세션 종료 시도 → 미충족 게이트 있으면 {"decision":"block"} │ │ 단, 원장 해시가 6번 연속 그대로면 스스로 해제 │ └─────────────────────────────────────────────────────────────┘

4-1. 관대하지 않은 파서 — fail-closed Markdown DSL

scripts/lib/gates.mjsparseGates()GATES.md를 읽습니다. 여기서 저자는 흔한 유혹을 거절했습니다 — 정규식으로 CHECK:를 찾아 긁어오지 않습니다.

왜 정규식으로 긁으면 안 되는가
문서 속 예시가 진짜 명령이 된다

GATES.md 안에 “이렇게 쓰면 됩니다”라며 코드블록으로 CHECK: rm -rf / 예시를 넣었다고 해봅시다. 단순 정규식 파서는 이 예시를 실제 게이트로 인식합니다.

그래서 parseGates()는 CommonMark의 코드 펜스 규칙(펜스 문자 종류·길이·들여쓰기가 열림/닫힘에서 일치해야 함)을 직접 구현해, 코드블록 안쪽을 확실히 건너뜁니다.

거부 규칙도 엄격합니다. 다음 중 하나라도 걸리면 파싱 에러로 전체를 거부합니다.

TERM
fail-closed / fail-open
뭔가 잘못됐을 때 막는 쪽으로 실패하면 fail-closed, 통과시키는 쪽으로 실패하면 fail-open. 보안 도구는 fail-closed여야 합니다 — 파싱이 애매하면 “일단 통과”가 아니라 “일단 거부”.

4-2. 승인을 “전체 조건의 해시”에 묶기

여기가 이 저장소에서 가장 배울 게 많은 부분입니다. 사용자가 게이트 하나를 승인하면, 그 승인은 명령 문자열이 아니라 실행 조건 전체의 SHA-256 해시에 묶입니다.

// scripts/gate-check.mjs
function oracle(file, gate) {
  const cwd = resolvedGateCwd(gate, file);
  return { schema: 1, check: gate.check, expect: gate.expect, cwd, shell,
    timeoutMs: timeoutSeconds * 1000, maxOutputBytes: MAX_OUTPUT_BYTES,
    regexTimeoutMs: REGEX_TIMEOUT_MS, platform: process.platform, path: pathValue };
}
function signature(file, gate) { return sha256(JSON.stringify(oracle(file, gate))); }

해시에 들어가는 항목을 보세요. 명령(check)과 기대값(expect)뿐 아니라 작업 디렉토리, 셸, 타임아웃, 플랫폼, 그리고 PATH 환경변수까지 들어갑니다. 하나라도 바뀌면 서명이 달라지고, 이전 승인은 자동으로 무효가 됩니다.

비유

“이 열쇠로 이 문을 열어도 좋다”가 아니라 “이 열쇠로, 이 건물의, 이 층의, 이 시간대에, 이 문을 열어도 좋다”고 승인하는 것입니다. 건물이 바뀌면 승인이 안 따라옵니다.

TERM
TOCTOU (Time-Of-Check to Time-Of-Use)
“검사한 시점”과 “사용하는 시점” 사이에 조건이 바뀌어 생기는 취약점. 예: 승인할 때는 안전한 npm test였는데, 실행 직전에 PATH가 바뀌어 다른 npm이 잡히는 경우. unlazy는 PATH를 서명에 포함시켜 이 창을 닫았습니다.

4-3. 정규식을 죽일 수 있는 스레드에 가두기

EXPECT:에는 /pattern/flags 형태의 임의 정규식을 쓸 수 있습니다. 편리하지만 위험합니다 — 어떤 정규식은 특정 입력에서 사실상 영원히 안 끝납니다.

TERM
ReDoS (Regular expression Denial of Service)
(a+)+b 같은 정규식에 aaaaaaaaaaaaaaaaaaaa를 넣으면 백트래킹이 폭발해 CPU가 몇 분씩 묶입니다. 정규식은 취소할 수 없기 때문에, 일단 시작하면 같은 스레드에서는 멈출 방법이 없습니다.

해법은 우아합니다. 매 검사마다 워커 스레드를 새로 하나 띄우고, 250ms 타이머와 경쟁시킵니다. 타이머가 이기면 worker.terminate()로 스레드를 통째로 죽입니다. 워커 코드는 전부 9줄입니다.

// scripts/lib/regex-worker.mjs — 파일 전체
import { parentPort } from "node:worker_threads";
parentPort.once("message", ({ source, flags, output }) => {
  try { parentPort.postMessage({ matched: new RegExp(source, flags).test(output) }); }
  catch (error) { parentPort.postMessage({ error: error.message }); }
});
패턴
“취소할 수 없는 연산은 죽일 수 있는 곳에 넣는다”

정규식뿐 아니라 무한 루프 가능성이 있는 사용자 제공 코드, 외부 라이브러리의 동기 파싱 등에 그대로 응용됩니다. 워커 생성 비용(수 ms)이 아깝지 않은 이유는, 그 대안이 “프로세스가 영원히 멈춤”이기 때문입니다.

4-4. DB 없이 만드는 분산 잠금 — 리스(lease) 파일

병렬 서브에이전트 여러 개가 같은 파일을 동시에 고치면 서로 덮어씁니다. unlazy는 게이트에 OWNS: glob(파일 패턴)을 붙여 소유권을 선언하게 하고, .unlazy/locks/*.lease 파일로 조율합니다.

기법구현
배타적 생성fs.open(path, "wx")wx는 파일이 이미 있으면 실패 — 이 실패가 곧 뮤텍스
원자적 쓰기writeAtomic(): 임시파일 → fsyncrename쓰다가 죽어도 원장이 반쪽 상태로 남지 않음
Windows 재시도EBUSY/EACCES/EPERM에 지수 백오프백신·인덱서가 핸들을 잠깐 잡는 현상 대응
동기 슬립Atomics.wait() on SharedArrayBuffer동기 함수 안에서 진짜로 블로킹해야 할 때 쓰는 정공법

겹침 판정도 보수적입니다. globsOverlap()은 두 패턴이 확실히 서로소임을 증명할 수 있을 때만 통과시킵니다. 소스 주석이 태도를 그대로 보여줍니다.

// scripts/lib/gates.mjs
// Prove disjointness only when literal path segments disagree. Everything else
// conflicts, including mid-segment pairs such as a* and ab*.
export function globsOverlap(left, right) { ... }

a*ab*는 사실 겹치지만, 겹치는지 아닌지 판정이 복잡합니다. 애매하면 충돌로 본다 — 잘못된 병렬 실행보다 불필요한 대기가 낫다는 판단입니다.

4-5. 자기 자신을 풀어주는 서킷브레이커

Stop hook은 강력한 만큼 위험합니다. 게이트가 영원히 충족되지 않으면 사용자는 세션을 끝낼 수 없게 됩니다. scripts/stop-hook.mjs는 이걸 원장 내용 해시로 감지합니다.

// scripts/stop-hook.mjs
const MAX_BLOCKS = 6;
// ...
if (sessionState.blocks > MAX_BLOCKS) {
  allow("unlazy: releasing after " + MAX_BLOCKS + " blocks without gate progress" + where + "...");
}

차단할 때마다 원장의 해시를 기록해 두고, 6번 연속 해시가 그대로면 “진행이 없다”고 판단해 스스로 차단을 풉니다. 강제 장치가 사용자를 인질로 잡지 않게 하는 안전핀입니다.

TERM
서킷브레이커(circuit breaker)
전기 차단기에서 온 말. 같은 실패가 반복되면 그 경로를 자동으로 끊어 시스템 전체가 마비되는 걸 막는 패턴. 여기서는 “차단 기능 자체”를 차단합니다.

5디렉토리 구조 해부

어느 파일부터 열어야 하는가
unlazy/ ├── SKILL.md ★ 에이전트가 읽는 진입점 │ frontmatter 의 description 이 곧 트리거 문구 ├── SECURITY.md 위협 모델 (CHECK 실행 · 승인 · hook · 리스) ├── CONTRIBUTING.md ├── CHANGELOG.md 현재 "Unreleased, target 2.1.0" ├── LICENSE MIT ├── package.json deps 0개 · engines.node >=16 │ ├── agents/ │ └── openai.yaml 스킬 UI 메타데이터 (표시명 등) │ ├── references/ ★ SKILL.md 에서 떼어낸 상세 규칙 │ ├── gates.md 원장 포맷 전체 명세 (엄격 파싱 규칙) │ ├── method.md Depth Tree 분해 방법론 │ ├── orchestration.md 상태머신: WAITING → READY → IN-FLIGHT │ │ → VERIFIED / ABANDONED │ ├── parallel.md scope · lease 동시성 규칙 │ └── token-economy.md 컨텍스트 · 토큰 절약 원칙 │ ├── research/ │ └── validation-protocol.md 구버전 벤치마크 철회 + 재현 프로토콜 │ ├── templates/ 복사해서 쓰는 뼈대 │ ├── PLAN.md │ ├── gates-leaf.md 잎 노드용 (실제 작업 단위) │ └── gates-node.md 중간 노드용 (하위 게이트 묶음) │ ├── scripts/ ★ 실제 코드는 전부 여기 │ ├── gate-check.mjs (690줄) CLI 본체: 파싱→승인→실행→리스→쓰기 │ ├── install-hooks.mjs(164줄) settings.json 에 Stop hook 설치/제거 │ ├── stop-hook.mjs (132줄) Stop hook 본체 │ └── lib/ │ ├── gates.mjs (575줄) 공유 파서 · 원자적 쓰기 · 파일락 · 리스 │ └── regex-worker.mjs(9줄) ReDoS 격리용 워커 │ ├── tests/ 64개 (프레임워크 없음) │ ├── run-tests.mjs (26) │ ├── hardening-tests.mjs(19) │ ├── stress-tests.mjs (10) │ └── self-check.mjs (9) │ └── .github/workflows/test.yml 3 OS × Node 16/20/24 매트릭스

5-1. 읽는 순서 추천

  1. README.md → 무엇을 주장하고 무엇을 주장하지 않는지
  2. references/gates.md → 원장 포맷. 이걸 알아야 코드가 읽힘
  3. scripts/lib/regex-worker.mjs → 9줄. 몸풀기
  4. scripts/stop-hook.mjs → 132줄. 훅 계약이 어떻게 생겼는지
  5. scripts/lib/gates.mjs → 575줄. 파서와 잠금의 본체
  6. scripts/gate-check.mjs → 690줄. 마지막에
  7. SECURITY.md → 코드를 본 뒤에 읽으면 “아 그래서 그렇게 짰구나”가 됨

5-2. references/ 를 따로 뺀 이유

SKILL.md는 에이전트가 매번 컨텍스트에 싣는 파일입니다. 여기에 모든 규칙을 다 넣으면 토큰을 크게 잡아먹습니다. 그래서 핵심 지침만 SKILL.md에 두고, 상세 규칙은 references/로 빼서 필요할 때만 읽게 합니다.

TERM
점진적 공개(progressive disclosure)
필요한 정보만 먼저 보여 주고 나머지는 요청이 있을 때 꺼내는 UI 원칙. 에이전트 스킬 설계에서는 “토큰 예산 관리”와 같은 말입니다. references/token-economy.md가 이 프로젝트의 규칙을 명문화해 두었습니다.

6학습 포인트 (기술별)

이 1,570줄에서만 건질 수 있는 것들
POINT 01

의존성 0개로 크로스플랫폼 CLI 만들기

npm 패키지 없이 파일 잠금·원자적 쓰기·프로세스 그룹 종료를 구현한 실전 예제입니다. 특히 타임아웃 시 자식 프로세스를 죽이는 부분이 교육적입니다.

// scripts/gate-check.mjs
const stopChild = () => {
  try {
    if (process.platform === "win32") child.kill("SIGKILL");
    else process.kill(-child.pid, "SIGKILL");  // 음수 PID = 프로세스 그룹 전체
  } catch { try { child.kill("SIGKILL"); } catch {} }
};

process.kill(-pid)의 음수 PID는 “이 프로세스가 만든 자식들까지 통째로”를 뜻합니다. npm test를 죽였는데 그 안의 jest가 살아남는 흔한 버그를 이렇게 막습니다. Windows에는 프로세스 그룹 개념이 달라 분기가 필요합니다.

POINT 02

“AI가 명령을 실행하는 프로그램”의 권한 설계

SECURITY.md의 한 문장이 이 저장소 전체를 요약합니다 — “승인은 동의이지 샌드박스가 아니다.”

4-2절의 오라클 해시 승인 모델은 MCP 서버, 코드 실행 도구, CI 러너 등 “LLM이 뭔가를 실행하는” 모든 프로젝트에 그대로 옮길 수 있는 설계입니다. 핵심은 승인 대상을 명령 문자열이 아니라 실행 컨텍스트 전체로 잡는 것입니다.

POINT 03

Windows 파일시스템의 함정 대응

대부분의 Node 프로젝트가 대충 넘어가는 부분을 정직하게 처리합니다. 백신·검색 인덱서가 파일 핸들을 잠깐 쥐면 renameEBUSY로 실패하는데, unlazy는 EBUSY/EACCES/EPERM을 구분해 지수 백오프로 재시도합니다.

그리고 동기 함수 안에서 진짜 슬립이 필요할 때 SharedArrayBuffer + Atomics.wait()를 쓰는 이유 — setTimeout은 비동기라 동기 흐름을 못 멈추고, busy-wait 루프는 CPU를 태웁니다.

POINT 04

“패턴 매칭 말고 파싱하라”의 실물 예제

Markdown에서 값을 뽑을 때 /^CHECK:\s*(.+)$/m 한 줄로 끝내고 싶은 유혹이 큽니다. parseGates()는 CommonMark 펜스 규칙을 직접 구현해 그 유혹을 이겼습니다.

설정 파일·프론트매터·로그 파서를 쓸 일이 있다면, 이 함수가 “어디까지 엄격해야 하는가”의 좋은 기준선이 됩니다.

POINT 05

Claude Code 훅 계약을 코드로 읽기

Stop hook은 stdin으로 JSON을 받고 stdout으로 JSON을 내보내는 단순한 계약입니다. stop-hook.mjs 132줄이 그 계약의 살아 있는 문서 역할을 합니다. install-hooks.mjssettings.json을 안전하게 수정·복원하는 법(멱등 설치, --uninstall)을 보여 줍니다.

POINT 06

주장을 철회하는 문서 쓰기

기술이 아니라 태도지만, 배울 값어치가 있습니다. research/validation-protocol.md는 “우리 주장은 재현 불가능하다”를 명시하고, 인용 논문이 무엇을 뒷받침하고 무엇을 뒷받침하지 않는지를 구분합니다. 사내 기술 문서나 성능 개선 보고서를 쓸 때 참고할 만한 형식입니다.

7하드웨어 / 시스템 요구사항

노트북 한 대면 충분합니다
항목요구사항근거
런타임Node.js 16 이상package.jsonengines.node, CI가 16/20/24 실증
OSWindows · macOS · LinuxCI 매트릭스에서 3종 모두 통과
메모리사실상 무관짧게 사는 자식 프로세스 + 검사당 워커 스레드 1개
GPU불필요모델 추론을 하지 않는 순수 검증 도구
디스크수백 KB저장소 전체가 약 122KB의 JS + 문서
컨테이너불필요Dockerfile 없음 (확인함)
전제 도구Claude Code 또는 Codex CLI스킬로 쓰려면 필요. node만 있으면 gate-check.mjs 단독 실행 가능

내부 상한값

상수의미
REGEX_TIMEOUT_MS250msEXPECT: 정규식 매칭 제한. 넘으면 워커 강제 종료
MAX_OUTPUT_BYTES1 MiB검사당 캡처하는 출력 상한. 로그 폭탄 방어
MAX_BLOCKS6진행 없이 연속 차단 횟수. 넘으면 서킷브레이커 발동
WINDOWS 주의
기본 셸이 cmd.exe입니다

Windows에서는 process.env.ComSpec(보통 cmd.exe)이 기본 셸이라 grep·tail·tr 같은 유닉스 도구가 없습니다. CHECK:에 이런 명령을 쓰면 실패합니다 — 문서가 이 함정을 명시적으로 경고합니다.

또 Git Bash에서 승인하고 PowerShell에서 실행하면 PATH가 달라 승인이 무효화됩니다. 이건 버그가 아니라 4-2절의 의도된 동작입니다.

8직접 해볼 수 있는 실습 과제

난이도순 4단계
LV.1  입문 30분

게이트 하나 만들어 통과·실패 시켜 보기

아무 프로젝트에나 GATES.md를 만들고 게이트 하나를 넣습니다.

# GATES.md
- ID: tests
  CHECK: npm test
  EXPECT: /(\d+) passing/

그다음 순서대로 해봅니다: ① --status로 파싱만 확인 → ② 일부러 테스트를 깨뜨리고 --approve → ③ 고친 뒤 --reverify. 목표는 “실행 없는 상태 조회”와 “승인 후 실행”이 어떻게 다른지 몸으로 아는 것입니다.

LV.2  초중급 1~2시간

승인 무효화를 직접 유발해 보기

게이트 하나를 승인한 뒤, 다음을 각각 바꿔 보고 승인이 유지되는지 깨지는지 기록합니다.

그리고 ~/.unlazy/approved/를 열어 파일 이름이 어떻게 생겼는지 봅니다. 목표: 해시 기반 권한 모델이 실제로 어떤 변화를 감지하는지 체감.

LV.3  중급 반나절

ReDoS 방어를 직접 깨뜨려 보기

EXPECT:에 파국적 백트래킹 정규식을 넣고, CHECK:가 그 패턴을 자극하는 긴 출력을 내게 만듭니다.

- ID: redos
  CHECK: node -e "console.log('a'.repeat(40))"
  EXPECT: /(a+)+b/

250ms 뒤에 워커가 죽는지 확인하고, 그다음 REGEX_TIMEOUT_MS를 크게 늘려서 어떻게 되는지 봅니다. 여유가 되면 regex-worker.mjs워커 없이 동기 실행하도록 고쳐 프로세스가 멈추는 걸 재현해 보세요. 목표: 격리가 없을 때 무슨 일이 벌어지는지 직접 목격.

LV.4  고급 1~2일

같은 아이디어를 다른 도구에 이식하기

gate-check.mjs의 오라클 해시 승인 모델만 떼어 내, 본인이 쓰는 도구에 붙여 봅니다. 후보:

어려운 지점은 해시에 무엇을 넣을지 고르는 일입니다. 너무 적게 넣으면 TOCTOU가 열리고, 너무 많이 넣으면 승인이 계속 깨져서 아무도 안 씁니다. 목표: 보안과 사용성 사이의 실제 트레이드오프를 손으로 겪기.

9관련 기술 심화 학습 로드맵

4주 코스 — 이 저장소를 발판으로
주차주제할 일
1주차 Node 표준 라이브러리 정복 node:fs의 플래그(wx·a·r+) 차이, fsync가 왜 필요한지, child_processspawn/exec/execFile 차이. 작은 원자적 쓰기 유틸을 직접 구현
2주차 동시성과 잠금 뮤텍스·리스·낙관적 잠금의 차이. worker_threadsAtomics 실습. “DB 없이 조율하기”의 한계가 어디인지 정리 (references/parallel.md가 좋은 출발점)
3주차 파싱과 신뢰 경계 CommonMark 스펙의 코드 펜스 절 읽기. ReDoS 실습(regex101의 백트래킹 카운터 활용). “신뢰할 수 없는 입력”의 목록을 본인 프로젝트에서 뽑아 보기
4주차 에이전트 스킬·훅 설계 Claude Code 훅 종류(PreToolUse·PostToolUse·Stop 등)와 각 계약. 본인 워크플로에 맞는 스킬 하나를 SKILL.md + references/ 구조로 직접 작성

함께 보면 좋은 것

10핵심 키워드 사전

이 문서에 나온 용어 한 번에 정리
용어
게이트 (gate)GATES.md의 검증 항목 하나. ID·CHECK·EXPECT·OWNS로 구성
원장 (ledger)미완 작업을 기록하는 파일. 대화가 아니라 파일이 진실의 원천
오라클 (oracle)“정답이 무엇인지 알려주는 것”. 여기서는 명령·기대값·cwd·셸·플랫폼·PATH를 묶은 실행 조건 전체
fail-closed애매하면 막는 쪽으로 실패. 보안 도구의 기본 자세
TOCTOU검사 시점과 사용 시점 사이에 조건이 바뀌어 생기는 취약점
ReDoS백트래킹이 폭발하는 정규식으로 CPU를 묶는 공격
워커 스레드Node에서 별도 스레드로 JS를 실행하는 기능. terminate()로 강제 종료 가능한 게 핵심
원자적 쓰기임시파일 → fsyncrename. 중간에 죽어도 반쪽 파일이 안 남음
리스 (lease)기한이 있는 소유권. 여기서는 파일 패턴에 대한 배타적 작업 권한
서킷브레이커같은 실패가 반복되면 그 경로를 자동으로 끊는 패턴
Stop hookClaude Code 세션 종료 시 외부 프로그램에게 허용 여부를 묻는 훅
Depth Tree작업을 N단계로 쪼개는 unlazy의 분해 방법론 (references/method.md)
점진적 공개핵심만 먼저 싣고 상세는 필요할 때 읽게 하는 토큰 절약 설계
ESM / .mjsimport/export를 쓰는 최신 JS 모듈 방식과 그 확장자

11참고 링크

원본과 그 주변

저장소

배경 지식

현재 상태 (2026-08-24 확인)

스타 / 포크⭐1,875 · 🍴110 · 열린 이슈 10
생성일2026-08-09
최근 커밋754d9a6 “fix: keep hook lock waits within host timeout” (2026-08-23)
커밋 수31개 · 외부 기여자 최소 5명
릴리스없음 — 태그 0개. README가 “정확한 커밋을 pin하라”고 권고
버전package.json 2.1.0 (CHANGELOG상 Unreleased)
쓰기 전에 알아둘 것
한계 세 가지

① 샌드박스가 아닙니다. CHECK: 명령은 호출자의 권한·자격증명·네트워크를 그대로 물려받습니다. 검토 없이 --approve하면 위험합니다. 신뢰할 수 없는 코드에는 컨테이너 같은 별도 격리를 쓰라고 문서가 권고합니다.

② 효과는 미검증입니다. “깊이에 비례해 효과가 곱해진다”는 초기 주장은 저자가 철회했습니다. 게이트가 실행된다는 사실은 확실하지만, 그것이 결과물 품질을 얼마나 올리는지는 증명되지 않았습니다.

③ 아주 최근 저장소입니다. 생성 15일, 정식 릴리스 없음. 급성장은 사실이지만 장기 검증된 트랙 레코드는 아닙니다. 저장소 설명(topics)에는 아직 철회된 구버전 문구가 남아 있어 본문 문서와 미묘하게 어긋납니다.