CURSOR AGENT 직접 실습

하나를 바꾸고, 확인하고, 안전하게 되돌리기

미리 준비할 프로젝트나 문서는 없습니다. 빈 폴더에서 시작해 필요한 파일을 함께 만들고, 훅(Hooks), 규칙(Rules), Skill을 하나씩 설정하며 같은 요청의 결과가 어떻게 달라지는지 확인합니다.

각 단계에서 결과를 먼저 예상하고, 실행한 뒤 코드와 기록으로 이유를 설명해 봅니다. “왜 이렇게 동작할까요?”를 펼치면 판단 조건과 입력·출력을 볼 수 있습니다. 시간 표시는 실행 기준이며, 원리 읽기와 확인 질문은 자기 속도에 맞춰 진행하세요.

여기서 함께 준비합니다도구 확인 · 빈 실습 폴더 · 예제 README
권장 기본 과정준비 → 기록 → 수정 제한 → 직접 편집 → 복원 · 약 50분
필요한 응용만 선택커밋 30분 · 라우팅 30분 · 기록·세션 분석 16분 · Skill 8분

처음이라면 기본 과정부터

첫 기록부터 직접 수정과 복원까지 따라가 보세요. 한 가지 설정을 바꾸고, 효과를 확인하고, 다시 끄는 방법을 익힙니다.

더 궁금한 주제를 골라보세요

커밋 제한, 라우팅, 세션 분석, Skill은 필요한 것만 선택해도 됩니다. 기본 과정 뒤에는 자기 프로젝트에 설정 하나를 옮기는 안내도 준비했습니다.

진행 막대에 표시할 경로
목차 열기 · 현재 Git

용어와 전체 흐름

먼저 네 가지 부품만 구분합니다

5분

Cursor Agent는 요청을 받고 도구를 골라 실행합니다. 이 실습은 그 앞뒤에 프로젝트 파일을 연결해 동작을 기록하거나 막고, 같은 입력이 어떤 길로 실행됐는지 실제 결과 파일로 확인합니다.

규칙(Rules)

에이전트가 읽고 따르는 프로젝트 지시입니다. 행동을 유도하지만 실행 자체를 강제로 막지는 않습니다.

훅(Hooks)

도구 실행 전이나 후에 자동으로 불리는 로컬 프로그램입니다. 기록하거나 허용·거부를 반환할 수 있습니다.

Skill

사용할 때 불러오는 재사용 지시 묶음입니다. 새 에이전트도, 강한 권한도 아닙니다.

실행 경로(routing)

요청이 직접 파일 읽기로 가는지, 정해진 프로그램을 거치는지 보여 주는 순서입니다.

요청을 이해하는 곳과 실행을 결정하는 곳

모델은 지시를 읽고 다음 도구를 고릅니다. Cursor는 그 도구를 실행하기 전에 등록된 훅을 호출합니다. Rule과 Skill은 모델이 무엇을 할지에 영향을 주고, 실행 전 훅은 요청된 도구를 허용할지 판단합니다. 이 부품들을 연결한 실행 환경을 여기서는 하네스라고 부릅니다.

개념 흐름 · 실행할 명령이 아닙니다
사용자 요청 + Rule / 불러온 Skill
  → 모델이 도구 호출을 요청
  → Cursor가 실행 전 훅을 호출
      ├─ allow → 도구 실행 → 실행 후 기록 → 모델의 다음 행동
      └─ deny  → 실행 차단 → 거부 응답 → 모델의 다음 행동
왜 이렇게 동작할까요? · 훅이 모델의 생각을 바꾸나요?

훅은 모델 안에서 실행되는 코드가 아닙니다. Cursor가 별도 프로그램으로 실행하고 응답을 읽습니다. 거부 메시지를 받은 모델이 다음 행동을 바꿀 수는 있지만, 훅이 문장을 이해해 새 답변을 만드는 것은 아닙니다.

라우팅도 새 모델을 만드는 기능이 아닙니다. 같은 요청에서 모델이 문서를 직접 검토할지, 프로그램이 검사하고 모델이 결과를 설명할지 처리 순서를 정하는 것입니다.

먼저 생각해 보세요. “파일을 수정하지 마세요”라는 Rule이 있는데 수정 요청이 나왔다면, 실행을 실제로 멈추는 부품은 무엇일까요?

해설 보기

수정 요청을 검사하는 실행 전 훅입니다. Rule은 도구를 고르기 전의 지시이고, 훅의 deny는 도구 요청 뒤의 실행 결정입니다.

기록 한 줄을 읽는 법

phase=attempt는 실행 전 시도, deny는 거부, fail은 실패 이벤트입니다. allow는 기록을 남긴 훅과 시점을 함께 봅니다. 기록 훅의 post/after 이벤트인지, R3 정책 훅의 실행 전 허용인지에 따라 뜻이 다릅니다. 어느 쪽도 파일의 최종 내용까지 증명하지는 않습니다.

원본 키트와 활성 복사본

.cursor-harness-kit/files/deny_write.py는 다시 설치할 기준 원본이고, .cursor/hooks/cursor-harness-lab-deny-write.py는 Cursor가 실제 호출하는 활성 복사본입니다. 뒤에서 활성 복사본만 직접 고칩니다.

실습 경계

모든 작업은 열린 프로젝트 안에서만 이뤄집니다. 전역 설정, 네트워크, 서브에이전트, 자동 커밋은 사용하지 않습니다. 각 고정 비교 전에는 새 Agent 대화를 열어 이전 대화의 영향을 줄입니다.

기본 준비

빈 실습 폴더부터 시작합니다

5분

자기 프로젝트를 가져오거나 README를 미리 만들 필요가 없습니다. 이 단계에서 연습 공간을 열고, 다음 단계에서 예제 문서와 실습 도구를 함께 설치합니다.

배울 원리 · 상대 경로의 기준. 같은 .cursor/hooks.json도 어느 프로젝트 안인가에 따라 다른 파일입니다. 열린 폴더와 터미널 위치를 먼저 맞추는 이유입니다.

왜 이렇게 동작할까요? · PROJECT_ROOT는 Cursor 설정인가요?

export PROJECT_ROOT="$PWD"는 현재 터미널의 작업 경로를 셸 변수에 저장합니다. 이어지는 cd는 그 위치로 이동합니다. 이 변수가 Cursor의 프로젝트를 바꾸거나 훅을 켜지는 않습니다.

훅은 Cursor가 보낸 workspace_roots의 첫 경로를 쓰고, 없으면 실행 중인 작업 폴더를 씁니다. 반면 prepare.py는 설치된 키트의 상위 폴더를 기준으로 삼습니다. 각각 같은 실습 폴더를 가리키게 준비합니다.

먼저 생각해 보세요. 새 터미널에서 변수가 없다는 오류가 나면 키트를 다시 설치해야 할까요?

해설 보기

아니요. 실습 폴더에서 변수를 다시 설정하면 됩니다. 디스크에 설치된 파일과 터미널에만 저장된 변수는 서로 다른 상태입니다.

1

Cursor를 열고 빈 폴더 만들기

Cursor가 없다면 공식 다운로드에서 설치하고 로그인합니다. Finder 또는 파일 탐색기에서 원하는 위치에 cursor-harness-practice라는 새 빈 폴더를 만드세요. 기존 프로젝트 안이 아닌 별도 위치를 선택합니다.

Cursor의 File → Open Folder로 방금 만든 폴더를 엽니다. 신뢰 여부를 묻는 창이 뜨면 직접 만든 폴더를 신뢰합니다. 이제부터 이 폴더를 프로젝트 루트라고 부릅니다.

2

터미널과 Python 확인하기

Cursor의 Terminal → New Terminal을 엽니다. macOS는 기본 zsh, Linux는 Bash를 사용합니다. Windows에서는 Git for Windows를 설치하고 터미널의 프로필 선택에서 Git Bash를 고릅니다. 아래 명령은 PowerShell 문법이 아닙니다.

터미널 · Python 실행 확인 · 파일 변경 없음
python3 --version

Python 3.x.x가 나오면 다음으로 갑니다. 명령이 없다면 Python 3 설치 안내를 따라 설치합니다. Windows 설치 화면에서는 Add Python to PATH를 선택하세요. Linux에서는 배포판의 패키지 관리자로 python3를 설치합니다. 설치 뒤 Cursor를 다시 열어 같은 명령을 확인하세요.

3

이 터미널의 작업 위치 정하기

터미널이 방금 연 cursor-harness-practice 폴더를 가리키는지 확인하고 실행합니다. PROJECT_ROOT는 이후 명령이 같은 폴더에서 실행되도록 기억해 두는 셸 변수입니다.

터미널 · 방금 연 빈 실습 폴더 · 파일 변경 없음
export PROJECT_ROOT="$PWD"
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit

새 터미널에서는 변수가 이어지지 않습니다. 실습 폴더에서 위 명령을 다시 실행하세요.

확인

  • Cursor에 빈 실습 폴더가 열려 있고 터미널에서 Python 3가 실행됩니다.
  • README, 훅, 규칙, 저장소는 아직 없어도 됩니다. 필요한 단계에서 함께 만듭니다.

실습 키트 준비

설치하고 만들어진 파일을 확인합니다

8분

다운로드와 직접 만들기 중 하나만 따라 하세요. 두 방법 모두 실습 도구와 예제 README를 준비합니다. 설치가 끝난 뒤 구조도를 확인하고, S1에서 기록 훅을 처음 켜봅니다.

배울 원리 · 설치와 활성화는 다릅니다. 파일을 갖다 놓는 것과 Cursor가 그 파일을 호출하도록 연결하는 것은 별도 단계입니다.

왜 이렇게 동작할까요? · 설치했는데 아직 기록이 없나요?
  1. install.py가 원본을 .cursor-harness-kit/에 두고, README가 없을 때 예제를 만듭니다.
  2. 다음 단계의 prepare.py s1 …가 원본을 활성 경로로 복사하고 .cursor/hooks.json에 이벤트와 실행 명령을 등록합니다.
  3. 그 뒤 Cursor에서 해당 이벤트가 발생해야 훅이 실행되고 기록이 생깁니다.

다운로드와 직접 만들기는 이 페이지의 같은 파일 목록에서 만들어집니다. 설치 방법이 달라도 활성화하는 원본은 같습니다. 전체 파일 목록에서 install.py, prepare.py, files/observe.py의 역할을 구분해 보세요.

먼저 생각해 보세요. Python 파일만 .cursor/hooks/에 복사하면 자동으로 호출될까요?

해설 보기

아니요. hooks.json에 어떤 이벤트에서 어떤 명령을 실행할지 등록해야 합니다. 설치 완료와 훅 연결 완료를 따로 확인하세요.

1

나중에 사용할 버전 관리 선택

지금은 기본값 Git으로 두어도 됩니다. 이 선택은 나중에 S3~S5 커밋 실습에서 사용할 도구를 정할 뿐, 저장소를 만들거나 검사하지 않습니다. 선택은 lab-results/vcs.txt에 기록됩니다.

버전 관리 선택
2

설치 방법 선택하고 실행하기

실습 키트 설치 방법

파일을 받은 뒤 cursor-harness-kit.tar를 실습할 프로젝트 루트에 놓아 주세요. 아래 명령은 그 폴더에서 바로 압축을 풀고 설치합니다. 브라우저의 다운로드 폴더를 자동으로 찾지 않습니다.

터미널 · 프로젝트 루트 · 변경: README.md · cursor-harness-kit/ · .cursor-harness-kit/
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 -m tarfile -e cursor-harness-kit.tar . || exit
python3 cursor-harness-kit/install.py

다운로드한 cursor-harness-kit.tar를 먼저 프로젝트 루트로 옮겨 주세요. 위 명령은 그 자리에 cursor-harness-kit/을 풀고, 실행용 파일을 .cursor-harness-kit/에 설치합니다. 임시 폴더나 기본 다운로드 경로는 사용하지 않습니다.

프로젝트에 이미 cursor-harness-kit/ 폴더가 있다면 기존 내용을 보존할 수 있도록 이름을 바꿔 둔 뒤 새 압축을 풀어 주세요. 설치된 .cursor-harness-kit/와 실습 결과는 지우지 않습니다.

이전 버전 키트가 설치돼 있거나 설치 충돌이 나온다면

새 압축을 프로젝트 루트에 푼 다음 아래 명령을 실행하세요. 기존 실행용 키트 전체를 lab-results/kit-backups/kit-001/부터 순서대로 보관하고 새 버전을 설치합니다. 직접 수정한 내용과 추가 파일도 백업에 남습니다. 현재 Cursor 설정과 기존 실습 결과는 바꾸지 않습니다.

터미널 · 프로젝트 루트 · 이전 키트 백업 후 갱신
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 cursor-harness-kit/install.py --replace

갱신한 뒤에는 진행하려던 준비 명령을 다시 실행하세요. 예를 들어 첫 단계라면 python3 .cursor-harness-kit/prepare.py s1 git입니다. 커밋 차단 수정본을 적용하려면 Git은 python3 .cursor-harness-kit/prepare.py s4 git, SVN은 마지막 인자를 svn으로 바꿔 실행합니다. 키트 갱신만으로는 활성 훅이 바뀌지 않습니다. 다시 준비하면 훅·공통 분석기·보고서가 함께 갱신되고, 기존 같은 시나리오 기록과 수정한 활성 파일은 lab-results/archive/에 보관됩니다.

3

설치 완료 후 구조도 확인하기

터미널에 키트 설치 완료와 구조도가 출력되면 Cursor의 파일 목록과 비교해 보세요. 아래는 두 방법이 공통으로 만드는 실행용 파일 구조입니다.

설치 직후 · 아직 훅을 켜기 전
cursor-harness-practice/                ← PROJECT_ROOT
├── README.md                           ← S1·S2에서 읽고 수정할 예제
└── .cursor-harness-kit/                 ← 실습 도구 원본
    ├── install.py                      ← 설치와 README 준비
    ├── prepare.py                      ← 단계별 설정 켜기
    ├── manage.py                       ← 설정 확인·끄기·복원
    └── files/                          ← 훅·규칙·문서·분석 예제
        ├── starter-README.md           ← README의 처음 내용
        ├── observe.py                  ← 실행 기록 훅 원본
        ├── deny_write.py               ← 수정 거부 훅 원본
        ├── cursor_harness_lab_shell.py  ← 셸 명령 공통 분석기
        └── ...                         ← 전체 목록은 아래에서 확인

다운로드 방식에는 cursor-harness-kit.tar와 압축을 푼 cursor-harness-kit/도 남습니다. 이후 명령은 점(.)으로 시작하는 .cursor-harness-kit/을 사용합니다.

지금 생긴 것과 다음에 생길 것

지금은 README와 도구 원본만 준비했습니다. .cursor/hooks.json, 활성 훅, lab-results/는 아직 없습니다. 다음 S1 준비 명령을 실행하면 생기며, 그때부터 Cursor의 동작을 기록합니다.

프로젝트 루트의 README.md를 열어 보세요. Cursor 하네스 실습이라는 제목과 S1·S2에서 비교할 내용이 들어 있습니다. 별도의 README를 찾거나 작성할 필요가 없습니다. 재설치할 때는 이미 있는 README의 내용을 보존합니다.

선택 사항 · 전체 파일 목록과 파일별 원본 보기

두 설치 방법이 함께 사용하는 정식 파일 목록입니다. 각 원본도 아래에서 따로 펼쳐 복사할 수 있습니다.

키트 경로하는 일언제 실행효과결과

확인

  • 프로젝트 루트에 README.md가 있고 내용을 열어 봤습니다.
  • .cursor-harness-kit/prepare.py, manage.py, files/가 있습니다.
  • 처음 설치한 폴더에는 아직 .cursor/lab-results/가 없습니다. 다음 단계에서 만듭니다.

설치 뒤 파일이 보이지 않나요?

첫 동작 기록 · S1

막지 않고 첫 요청을 기록합니다

10분

먼저 비교 기준을 만듭니다. 준비 명령은 이전 실습 훅만 바꾸고 사용자 훅과 규칙은 남긴 채 기록 훅을 활성화합니다.

실행 전과 후pre/before = 시도, post/after = 허용, failure = 실패
같게 유지고정 프롬프트 A · 설치가 만든 README

배울 원리 · 관찰과 제어의 분리. S1의 훅은 무슨 일이 있었는지 남기되 요청을 거부하지 않습니다. 다음 S2에서 차단 조건을 더했을 때의 비교 기준입니다.

왜 이렇게 동작할까요? · 한 번의 요청에 기록이 여러 줄인가요?

.cursor/hooks/cursor-harness-lab-observe.pyhook_event_name을 읽습니다. pre/before이면 시도, postToolUseFailure이면 실패, 나머지 등록된 post/after이면 allow로 기록합니다. 정상 실행의 끝에서는 permission: allow를 출력합니다.

한 도구 호출을 앞뒤로 관찰하는 흐름
도구 요청 → 실행 전 훅: attempt 기록
          → 도구 실행
          → 실행 후 훅: allow 또는 실패 이벤트의 fail 기록

lab-results/CURRENTs1은 기록을 넣을 폴더 이름입니다. events.jsonl에는 JSON 객체 하나를 한 줄씩 덧붙입니다. 시도 한 줄과 결과 한 줄을 서로 다른 작업 두 번으로 세면 안 됩니다.

실행 뒤 생각해 보세요. attempt만 있으면 수정 성공이라고 할 수 있나요?

해설 보기

아니요. 도구 요청만 확인됩니다. 거부·오류·기록 누락 가능성을 살피고 실제 README도 확인해야 합니다. 이 기록기의 allow도 모든 셸 명령의 성공 여부를 검사한 값은 아닙니다.

바로 앞에서 생성된 PROJECT_ROOT/README.md를 사용합니다. 먼저 아래 준비 명령으로 기록 훅을 켜고, 새 Agent 대화에서 같은 문서를 읽고 수정하도록 요청합니다.

● 새 파일: CURRENT·활성 스크립트↻ 교체: 실습 소유 훅 항목▶ 활성: 기록 훅만

S1 준비 후 파일 구조
프로젝트/
├── README.md                         ← 기존 파일 유지
├── .cursor/
│   ├── hooks.json                    ← 이번 설정으로 변경 (사용자 훅 유지)
│   ├── hooks/
│   │   ├── cursor-harness-lab-observe.py     ← 기록 켜짐
│   │   ├── cursor-harness-lab-deny-write.py  ← 사용하지 않음
│   │   └── cursor-harness-lab-deny-commit.py ← 사용하지 않음
│   └── rules/                        ← 사용자 규칙 유지
└── lab-results/
    ├── CURRENT                       ← s1
    ├── vcs.txt                       ← 선택한 Git 또는 SVN
    ├── snapshot.py / compare.py      ← 결과 정리 도구
    ├── s1/
    │   ├── events.jsonl              ← 실행 기록
    │   └── summary.md                ← 실행 후 요약 명령으로 생성
    └── archive/                      ← 재실행 전 기록 보관
Terminal · PROJECT_ROOT · 변경: 위 S1 트리 · 커밋 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py s1 git

새 Agent 대화를 열고 다음 입력을 한 글자도 바꾸지 않습니다.

Cursor Agent 입력 · 고정 프롬프트 A
README.md를 읽은 다음, 파일 끝에 아래 한 줄을 추가해 주세요. 커밋은 하지 마세요.

<!-- lab-edit -->
Terminal · PROJECT_ROOT · 변경: lab-results/s1/summary.md
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 lab-results/snapshot.py
실제 결과 파일기대 비교
s1/events.jsonl, s1/summary.mdWrite 또는 StrReplace의 allow, 거부 없음
이벤트 한 줄을 바로 해석해 봅니다

tool=Write는 선택된 도구, phase=attempt는 실행 전 시도입니다. 이어지는 phase=decisionoutcome=allow는 실행 후 허용됐다는 뜻입니다. 실행 도구가 달랐다면 아래 문제 해결에서 확인합니다.

확인

  • lab-results/s1/summary.md에서 수정 도구의 허용을 확인했습니다.
  • 채팅 답변이 아니라 실제 결과 파일로 판정했습니다.

다른 도구가 기록됐나요? · 지금 기록을 끄거나 복원하기

같은 요청 거부 · S2

실행 전에 수정 도구를 거부합니다

12분

S1과 똑같은 요청을 보내되, 이번에는 기록 훅 다음에 수정 거부 훅을 연결합니다. 사용자 훅·규칙과 S1 기록은 그대로 남습니다.

실행 전 순서preToolUse: 시도 기록 → deny-write 거부 결정
같게 유지S1과 같은 프롬프트 A

배울 원리 · 프로그램 사이의 요청과 응답. Python의 print()가 직접 도구를 막는 것은 아닙니다. Cursor가 훅의 표준출력을 JSON 응답으로 읽고 실행 여부를 결정합니다.

왜 이렇게 동작할까요? · print가 어떻게 StrReplace를 막나요?

1. 등록: 언제 어떤 파일을 실행하나요?

S2 준비는 .cursor/hooks.jsonpreToolUse에 기록 훅 다음으로 python3 .cursor/hooks/cursor-harness-lab-deny-write.py를 등록합니다. failClosed: true도 이 항목에 붙습니다. 파일이 존재해서가 아니라 실행 전 이벤트에 등록되어서 호출됩니다.

2. 입력: Cursor가 무엇을 보내나요?

설명용 요청 JSON · 터미널에서 실행하지 않습니다
{
  "hook_event_name": "preToolUse",
  "tool_name": "StrReplace",
  "workspace_roots": ["/project"],
  "tool_input": {"file_path": "README.md"}
}

Cursor가 별도 Python 프로세스의 표준입력(stdin)에 JSON을 보냅니다. 활성 파일의 payload = json.load(sys.stdin)이 이를 읽고, tool = payload.get("tool_name", "")이 도구 이름을 꺼냅니다.

3. 조건: 어느 줄이 차단을 결정하나요?

deny-write 활성 파일의 실제 분기 · 해설용 발췌
if tool not in {"Write", "StrReplace", "Delete", "EditNotebook"}:
    print(json.dumps({"permission": "allow"}, ensure_ascii=False))
    raise SystemExit(0)

not in은 “목록에 없으면”입니다. Read는 이 분기에 들어가 allow를 출력하고 즉시 끝납니다. StrReplace는 목록에 있으므로 이 분기를 건너뛰고, 로그 기록 부분을 지난 뒤 마지막의 permission: deny 출력에 도달합니다. 이름은 대소문자까지 정확히 비교합니다.

4. 출력: return이 없어도 응답이 되나요?

프로세스 경계를 넘는 흐름 · 응답의 결정 필드만 표시
Cursor
  └─ stdin: 요청 JSON → Python 훅
                         └─ stdout: {"permission": "deny"}
  ← 응답 JSON을 읽고 permission 확인
  └─ StrReplace 실행 차단

일반 함수의 return은 같은 Python 프로그램의 호출자에게 값을 돌려줍니다. Cursor는 별도 프로세스의 Python 반환값을 직접 받을 수 없으므로 표준출력(stdout)을 읽습니다. json.dumps()가 사전을 JSON 문자열로 만들고, print()가 그 문자열을 stdout에 씁니다.

전달되는 값담당하는 일
permission: allow / deny요청한 도구를 허용할지 거부할지 결정
프로세스 종료 코드 0훅 프로그램이 정상 종료됨. 도구 허용이라는 뜻은 아님
훅 오류 + failClosed: true판단 프로그램이 실패했을 때도 실행을 허용하지 않도록 설정
user_message / agent_message사람과 에이전트에게 전할 이유. 차단 조건 자체는 permission

허용 분기의 SystemExit(0)은 아래의 거부 출력까지 내려가지 않게 합니다. 거부 분기는 파일 끝에 도달하며 정상 종료하므로 종료 코드가 0이어도 deny일 수 있습니다. 디버깅 문장을 stdout에 섞으면 응답 파싱을 깨뜨릴 수 있으니 print("진단 내용", file=sys.stderr)처럼 표준에러(stderr)를 사용합니다.

5. 기록과 차단은 같은 일인가요?

CURRENT가 없으면 로그는 남기지 않지만 정상 실행에서는 여전히 deny를 출력합니다. 다만 현재 코드는 로그 쓰기 오류를 잡지 않습니다. 기록 중 예외가 나면 마지막 출력에 도달하지 못하고, 이때는 failClosed가 실패 시 처리를 맡습니다. reason=deny-write는 기록의 이유이고, Cursor가 읽는 결정은 stdout의 permission입니다.

같은 요청을 비교할 때: S2 준비는 S1에서 바뀐 README를 초기화하지 않습니다. 이미 추가된 줄을 보고 Agent가 수정 요청을 하지 않았다면 거부를 관찰한 것이 아닙니다. 실제 도구 기록을 확인하고, 조건 자체는 다음 직접 편집 장의 probe로 따로 확인하세요.

먼저 생각해 보세요. 파일 경로를 README.md에서 notes.md로 바꾸면 StrReplace가 허용될까요?

해설 보기

아니요. 이 훅은 경로나 변경 내용이 아닌 도구 이름만 검사합니다. Shell은 목록에 없으므로 이 훅만으로 모든 파일 수정 경로를 막지는 못합니다. 실제 경로는 기록에서 확인하세요.

S2 준비 후 파일 구조
프로젝트/
├── README.md                         ← 기존 파일 유지
├── .cursor/
│   ├── hooks.json                    ← 이번 설정으로 변경 (사용자 훅 유지)
│   ├── hooks/
│   │   ├── cursor-harness-lab-observe.py     ← 기록 켜짐
│   │   ├── cursor-harness-lab-deny-write.py  ← 수정 차단 켜짐
│   │   └── cursor-harness-lab-deny-commit.py ← 사용하지 않음
│   └── rules/                        ← 사용자 규칙 유지
└── lab-results/
    ├── CURRENT                       ← s2
    ├── vcs.txt                       ← 선택한 Git 또는 SVN
    ├── snapshot.py / compare.py      ← 결과 정리 도구
    ├── s2/
    │   ├── events.jsonl              ← 실행 기록
    │   └── summary.md                ← 실행 후 요약 명령으로 생성
    └── archive/                      ← 재실행 전 기록 보관
Terminal · PROJECT_ROOT · 변경: 위 S2 트리 · 커밋 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py s2 git

새 Agent 대화에서 같은 입력을 보냅니다.

Cursor Agent 입력 · 고정 프롬프트 A
README.md를 읽은 다음, 파일 끝에 아래 한 줄을 추가해 주세요. 커밋은 하지 마세요.

<!-- lab-edit -->
Terminal · PROJECT_ROOT · 변경: summary와 compare-s1-s2.md
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 lab-results/snapshot.py
python3 lab-results/compare.py s1 s2
도구S1S2비교
Write/StrReplace허용거부수정 제한됨
결과 파일의 뜻

outcome=deny는 실행 전 거부, reason=deny-write는 이 실습 훅이 내린 결정입니다. summary.md는 원본 events.jsonl을 읽기 쉽게 센 결과입니다.

확인

  • s2/events.jsonl, s2/summary.md, compare-s1-s2.md를 확인했습니다.
  • 수정 도구가 S1의 허용에서 S2의 거부로 바뀌었습니다.

규칙이 무시된 것과 훅 거부의 차이가 헷갈리나요? · 실습 설정 끄기

직접 편집 실습

Write는 허용하고 StrReplace는 계속 거부합니다

8분

이제 활성 훅의 작은 조건 하나를 직접 고칩니다. Agent가 어떤 도구를 고를지 기다리지 않고, 로컬 점검 프로그램이 두 요청을 똑같이 전달해 결과를 따로 기록합니다.

바꾸는 파일.cursor/hooks/cursor-harness-lab-deny-write.py 한 개
바꾸지 않는 원본.cursor-harness-kit/files/deny_write.py

배울 원리 · 조건 하나로 결과를 예측하기. 거부 목록에서 Write를 빼면 not in 조건이 참이 되어 허용 분기로 들어갑니다. 목록에 남은 StrReplace는 계속 거부됩니다.

왜 이렇게 동작할까요? · Agent 없이 편집 효과를 확인할 수 있나요?

.cursor-harness-kit/files/probe.pysubprocess.run()으로 활성 훅을 실행합니다. 두 도구 요청을 각각 stdin으로 보내고 stdout을 json.loads()로 해석합니다. 모델이 우연히 원하는 도구를 골라 주기를 기다리지 않습니다.

이 점검은 훅 조건의 입력→출력을 확인합니다. Cursor의 훅 등록이나 실제 파일 수정까지 검증하지는 않습니다. 그래서 target_created: false가 기대값입니다.

보고서는 custom 폴더에 따로 생기지만, 거부된 점검 요청은 현재 CURRENT=s2의 이벤트 로그에도 추가됩니다. 이때 생긴 StrReplace deny를 실제 Agent의 시도와 섞어 해석하지 마세요.

고치기 전에 예상해 보세요. 원본 키트만 고치고 활성 복사본을 그대로 두면 probe의 결과가 바뀔까요?

해설 보기

바뀌지 않습니다. probe와 Cursor는 활성 복사본을 호출합니다. 이번에는 활성 파일만 고쳐 Write=allow, StrReplace=deny를 확인하세요. 준비 명령은 원본을 다시 활성 파일로 복사합니다.

직접 편집 전 구조
PROJECT_ROOT/
├── .cursor-harness-kit/files/deny_write.py       ← 기준 원본, 그대로 둠
├── .cursor/hooks/cursor-harness-lab-deny-write.py ← 직접 고칠 활성 복사본
└── lab-results/custom/probe.json                 ← 점검 뒤 새 결과

1. S2 상태를 다시 준비합니다

이 명령은 활성 복사본을 기준 원본으로 다시 씁니다. 기존 활성 복사본을 이미 고쳤다면 먼저 lab-results/archive/active-edits-NNN/에 보관합니다.

터미널 · PROJECT_ROOT · S2 기준 상태 준비
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py s2 git

2. Cursor 편집기에서 활성 복사본을 직접 고칩니다

.cursor/hooks/cursor-harness-lab-deny-write.py를 열고 아래 한 줄만 바꿔 저장합니다. StrReplace, Delete, EditNotebook은 그대로 둡니다.

바꾸기 전바꾼 뒤
{"Write", "StrReplace", "Delete", "EditNotebook"}{"StrReplace", "Delete", "EditNotebook"}

3. 모델 없이 안전하게 점검합니다

이 프로그램은 존재하지 않는 lab-probe-target.txt 경로를 훅에 입력할 뿐 파일을 만들거나 수정하지 않습니다.

터미널 · PROJECT_ROOT · 변경: lab-results/custom/probe.json
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/files/probe.py
결과 필드기대값과 뜻
results.Writeallow · 방금 직접 바꾼 효과
results.StrReplacedeny · 남겨 둔 거부 조건
target_createdfalse · 실제 파일 수정 없음

확인

  • lab-results/custom/probe.json에서 두 응답을 확인했고, S2 이벤트에 추가된 점검용 deny를 Agent 실행과 구분했습니다.
  • 활성 복사본과 원본 키트의 차이를 설명할 수 있습니다.

준비 뒤 직접 수정이 사라졌나요? · 아래에서 끄고 복원할 수 있습니다.

적용 · 끄기 · 복원

실습 설정을 끄고, 다시 켜보세요

7분

실습을 마쳤거나 평소 작업으로 돌아가고 싶다면 이번에 추가한 설정만 끄면 됩니다. 직접 고친 훅은 백업해 두었다가 다시 켤 수 있어요. 원래 사용하던 파일과 설정, 실험 기록은 남겨둡니다.

배울 원리 · 호출 연결을 끊고 되돌리기. 훅을 끄는 것은 Cursor가 호출할 등록을 제거하는 일입니다. 지난 기록을 지우는 일과는 다릅니다.

왜 이렇게 동작할까요? · restore와 prepare는 무엇이 다른가요?

.cursor-harness-kit/manage.py의 disable은 실습 파일과 등록 항목을 백업한 뒤 실습 소유 항목만 제거합니다. registered_hook_entries: 0은 실습 훅 등록 수이며 사용자 훅 전체가 꺼졌다는 뜻은 아닙니다.

restore는 끄기 직전 상태를 돌려놓습니다. prepare는 키트 원본의 시나리오 상태를 새로 준비합니다. 어느 시점의 상태를 원하는지에 따라 선택합니다.

실행 전에 예상해 보세요. Write를 허용하도록 고친 뒤 disable→restore를 하면 Write는 무엇이 될까요?

해설 보기

복원이 성공하면 직접 고친 상태인 allow입니다. 원래 deny로 돌아가려면 S2 준비 명령을 씁니다. 복원 충돌은 그동안 바뀐 내용을 덮어쓰지 않기 위한 중단입니다.

관리 대상과 보존 대상
.cursor/hooks/cursor-harness-lab-*.py          ← 실습 소유: 끄기 대상
.cursor/rules/cursor-harness-lab-*.mdc        ← 실습 소유: 끄기 대상
.cursor/skills/lab-document-review/SKILL.md   ← 실습 소유: 끄기 대상
.cursor/hooks.json의 다른 키와 명령            ← 사용자 소유: 보존
lab-results/** 및 프로젝트 파일                ← 항상 보존

현재 상태 확인

터미널 · PROJECT_ROOT · 변경 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/manage.py status

실습 설정 끄기

이 명령은 활성 훅 등록을 제거하기 전에 현재 실습 파일과 항목을 lab-results/management-backups/disable-NNN/에 보관합니다. 직접 고친 훅도 이 백업에 남습니다.

터미널 · PROJECT_ROOT · 실습 항목만 비활성화
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/manage.py disable
python3 .cursor-harness-kit/manage.py status

두 번째 출력은 enabled: falseregistered_hook_entries: 0을 보여야 합니다.

끄기 직전 상태 복원

복원은 백업 뒤 추가한 사용자 훅과 다른 JSON 키를 유지합니다. 같은 실습 경로 또는 명령에 다른 내용이 있으면 덮어쓰지 않고 복원 충돌로 멈춥니다.

터미널 · PROJECT_ROOT · 최근 미복원 백업 사용
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/manage.py restore
기준 원본으로 되돌리고 싶다면

복원은 끄기 직전의 직접 편집본을 돌려놓습니다. 기준 원본으로 바꾸려면 S2 준비 명령을 다시 실행하세요. 그 명령은 직접 편집본을 lab-results/archive/active-edits-NNN/에 보관한 뒤 활성 복사본을 덮어씁니다.

핵심 경로 완료

  • 훅 하나를 직접 고치고 별도 결과로 확인했습니다.
  • manage.py disable 뒤 실습 훅이 더는 등록되지 않음을 확인했습니다.
  • 백업 위치와 충돌을 덮어쓰지 않는 복원 방식을 확인했습니다.
이제 하나를 골라보세요

지금 배운 것을 써보고 싶다면 내 프로젝트에 필요한 설정만 옮기기로 넘어가세요. 결과가 예상과 달랐다면 세션 기록으로 원인 찾기를, 더 연습하고 싶다면 아래 응용 실습을 선택하면 됩니다. 모든 응용을 끝내야 적용할 수 있는 것은 아니에요.

여기부터 선택 실습입니다. 커밋 동작, 기록 읽기, 같은 입력의 실행 경로, Skill 중 필요한 부분만 골라도 됩니다.

커밋 기록 · S3

차단 없이 첫 커밋 관찰

8분

먼저 차단 없이 커밋을 요청해 볼게요. 준비 명령이 commit-input.txt에 이번 실험용 변경을 만들어주므로 따로 파일을 고칠 필요가 없습니다. 실제 커밋은 뒤에서 에이전트에게 요청합니다.

배울 원리 · 실행 허용과 작업 성공의 차이. 훅은 셸 명령의 앞뒤를 관찰합니다. 실제 커밋은 Git 또는 SVN이 저장소 상태와 변경 내용을 보고 수행합니다.

왜 이렇게 동작할까요? · allow인데 커밋이 없을 수도 있나요?

prepare.py는 S3~S5에서 git rev-parse 또는 svn info가 성공하는지 확인하고 commit-input.txt를 바꿔 커밋할 재료를 만듭니다. 준비가 커밋을 실행하지는 않습니다.

Agent의 셸 요청은 beforeShellExecution과 실행 후 afterShellExecution에서 기록됩니다. 이 랩의 관찰 코드는 후자에 allow를 붙일 뿐 셸 종료 코드를 해석하지 않습니다.

실행 뒤 생각해 보세요. “nothing to commit”이라는 도구 결과와 allow가 함께 보이면 모순일까요?

해설 보기

아닙니다. 명령은 훅에 차단되지 않았지만 새 커밋은 만들어지지 않은 상태입니다. 도구 결과와 git log -1 또는 svn log -l 1을 함께 확인해야 실제 커밋을 판정할 수 있습니다.

여기서 연습용 저장소를 만듭니다

앞에서 만든 실습 폴더를 그대로 사용합니다. 이제 커밋을 비교하기 위해 선택한 버전 관리 도구의 저장소를 만듭니다. 계정이나 원격 서버를 준비할 필요는 없습니다.

먼저 저장소 만들기

터미널에서 git --version을 실행해 보세요. 명령이 없다면 Git 설치 안내를 따라 설치하고 Cursor 터미널을 다시 엽니다. 그다음 실습 폴더에서 PROJECT_ROOT를 다시 설정하고 아래 명령을 실행합니다.

git init은 이 폴더에 저장소를 만들고, --local 설정은 이 실습에서만 사용할 작성자와 서명 여부를 정합니다. GitHub 계정이나 실제 이메일은 필요하지 않습니다. 아직 커밋은 하지 않습니다.

터미널 · 실습 폴더에 .git 생성 · 로컬 작성자 설정
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
git init &&
git config --local user.name "Harness Learner" &&
git config --local user.email "learner@example.invalid" &&
git config --local commit.gpgsign false

아래 결과가 true이면 저장소가 준비됐습니다.

터미널 · 저장소 확인 · 파일 변경 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
git rev-parse --is-inside-work-tree
실행 전과 후beforeShellExecution에서 시도 → afterShellExecution에서 허용
활성 훅기록만 · 선택한 버전 관리 1개
S3 준비 후 파일 구조
프로젝트/
├── README.md                         ← 기존 파일 유지
├── .cursor/
│   ├── hooks.json                    ← 이번 설정으로 변경 (사용자 훅 유지)
│   ├── hooks/
│   │   ├── cursor-harness-lab-observe.py     ← 기록 켜짐
│   │   ├── cursor-harness-lab-deny-write.py  ← 사용하지 않음
│   │   └── cursor-harness-lab-deny-commit.py ← 사용하지 않음
│   └── rules/                        ← 사용자 규칙 유지
└── lab-results/
    ├── CURRENT                       ← s3
    ├── vcs.txt                       ← 선택한 Git 또는 SVN
    ├── snapshot.py / compare.py      ← 결과 정리 도구
    ├── commit-input.txt              ← 이번 커밋 실험용 변경
    ├── s3/
    │   ├── events.jsonl              ← 실행 기록
    │   └── summary.md                ← 실행 후 요약 명령으로 생성
    └── archive/                      ← 재실행 전 기록 보관
Terminal · PROJECT_ROOT · 변경: 위 S3 트리 · 로컬 Git만
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py s3 git

새 Agent 대화에서 선택한 고정 프롬프트 B를 보냅니다.

Cursor Agent 입력 · Git
현재 워크스페이스의 변경사항을 로컬 git으로만 커밋하세요. 원격 저장소는 사용하지 마세요. push 하지 마세요. 커밋 메시지는 정확히 다음을 사용하세요: lab: scenario commit
Terminal · PROJECT_ROOT · 변경: s3/summary.md
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 lab-results/snapshot.py
실제 결과 파일기대 동작S4와 비교할 기준선
s3/events.jsonl, s3/summary.md선택한 VCS commit의 attempt와 allow정책 deny가 없음

확인

  • s3/events.jsonls3/summary.md에서 커밋 시도와 허용을 확인했습니다.
  • commit-input.txt 외의 학습자 파일은 준비가 바꾸지 않았습니다.

커밋 기록이 없나요? · 실습 훅 끄기

커밋 거부 · S4

실행 전에 커밋을 거부합니다

12분

준비 명령이 비교용 변경을 만들고 커밋 거부 훅을 연결합니다. 지원하는 상태·차이 조회는 허용하고, 커밋이 포함됐거나 분석할 수 없는 셸 요청은 실행 전에 거부합니다.

실행 전 순서시도 기록 → 전체 명령 분석 → 허용 또는 거부
같게 유지S3과 같은 프롬프트 B

배울 원리 · 도구 이름보다 안쪽을 검사하기. S2는 수정 도구 이름을 봤지만 S4는 Shell 안에 들어 있는 명령의 실행 파일과 하위 명령을 봅니다.

왜 이렇게 동작할까요? · status는 되고 commit은 안 되나요?

.cursor/hooks/cursor-harness-lab-deny-commit.py는 beforeShellExecution에서 commandlab-results/vcs.txt를 읽습니다. lab-results/cursor_harness_lab_shell.pyanalyze(command, vcs)가 요청 전체를 분석하고, 훅이 그 결과를 permission: allow / deny로 바꿉니다. 분석기는 명령을 실행하지 않습니다.

문자열 안의 commit만 찾으면 안 됩니다. git commit -m "lab"은 커밋이지만 echo "git commit"은 출력할 글자이고, git add commit의 commit은 파일 이름입니다. 따옴표·주석·이스케이프를 구분한 뒤 실행 파일과 옵션, 하위 명령을 확인합니다.

복합 요청 한 번을 판단하는 흐름 · 실행 예제가 아닙니다
입력: git add -A && git commit -m x
분석: 첫 명령은 add, 두 번째 명령은 commit
판단: 선택한 Git의 커밋 발견 → reason=deny-commit
응답: {"permission": "deny"}
결과: 요청 전체가 실행되지 않음. 앞의 add도 실행되지 않음.

지원 범위는 값이 정해진 단순 명령과 &&, ||, ;, |, &, 줄바꿈 연결입니다. 연결된 명령을 모두 검사하므로 뒤에 커밋이 있어도 거부합니다. env LAB=1 git commit, command git commit 같은 지원하는 래퍼도 검사하며, SVN에서는 commit과 같은 동작인 ci를 함께 막습니다.

중첩 셸(bash -c 등), 변수·명령 치환, 제어문, 리다이렉션, 따옴표 밖의 와일드카드 등 미지원 구문은 deny-unsupported-shell로 거부합니다. sudo·xargs 같은 미지원 실행 래퍼, Python·Node 실행 요청, 목록에 없는 VCS 하위 명령·앞쪽 옵션도 같은 분기입니다. 따라서 S4에서는 커밋 없는 테스트 실행도 거부될 수 있습니다. 이 제한은 S4 정책이 켜진 동안 적용됩니다.

닫히지 않은 따옴표 등 잘못된 구문은 deny-invalid-shell로 거부합니다. 두 구문 거부 모두 커밋을 발견했다고 기록하지 않고 command_family=unknown으로 남깁니다. 지원하는 단순 명령으로 표현할 수 있다면 나누어 요청하고, 다른 실행 경로가 필요한 작업이라면 실습 훅 끄기에서 정책을 의도적으로 해제합니다.

failClosed: true는 Python 실행 오류처럼 훅이 판단을 끝내지 못할 때를 맡습니다. 반면 위 구문 거부는 훅이 정상 종료하면서 deny를 반환한 결과입니다. 둘 다 실행을 막지만 이유가 다릅니다.

이 실습의 경계. Cursor Agent의 셸 요청을 검사하는 훅이며, 사람이 터미널에서 직접 실행하는 명령 전체를 막는 장치는 아닙니다. 스크립트·외부 프로그램·별칭·Git 설정 안쪽의 동작까지 실행해 추적하지 않습니다. OS 샌드박스나 저장소 권한 격리를 대신하지 않습니다. git addgit push 전체를 금지하는 정책으로 바꾼 것도 아닙니다.

먼저 예상해 보세요. Git 선택 상태에서 git status && git diffgit status && git commit -m x는 어떻게 다를까요?

해설 보기

첫 요청은 지원하는 조회만 있어 허용됩니다. 두 번째는 커밋이 포함되어 요청 전체가 거부되므로 status도 실행되지 않습니다. S3과 S4에서는 같은 프롬프트 B를 사용하고, 원본 로그에서 실제 명령과 거부 이유를 확인하세요.

S4 준비 후 파일 구조
프로젝트/
├── README.md                         ← 기존 파일 유지
├── .cursor/
│   ├── hooks.json                    ← 이번 설정으로 변경 (사용자 훅 유지)
│   ├── hooks/
│   │   ├── cursor-harness-lab-observe.py     ← 기록 켜짐
│   │   ├── cursor-harness-lab-deny-write.py  ← 사용하지 않음
│   │   └── cursor-harness-lab-deny-commit.py ← 커밋 차단 켜짐
│   └── rules/                        ← 사용자 규칙 유지
└── lab-results/
    ├── CURRENT                       ← s4
    ├── vcs.txt                       ← 선택한 Git 또는 SVN
    ├── cursor_harness_lab_shell.py   ← 훅·보고서 공통 분석
    ├── snapshot.py / compare.py      ← 결과 정리 도구
    ├── commit-input.txt              ← 이번 커밋 실험용 변경
    ├── s4/
    │   ├── events.jsonl              ← 실행 기록
    │   └── summary.md                ← 실행 후 요약 명령으로 생성
    └── archive/                      ← 재실행 전 기록 보관
Terminal · PROJECT_ROOT · 변경: 위 S4 트리 · 커밋 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py s4 git

Agent에게 입력하기 전에 Git은 아래 HEAD 값을 기록하고, 실행 뒤 같은 값인지 비교하세요. S3에서 아직 커밋이 생기지 않았다면 HEAD가 없다는 오류가 나며, S4 뒤에도 커밋이 새로 생기지 않아야 합니다. SVN은 svn status로 커밋되지 않은 변경이 남았는지 확인합니다.

Terminal · PROJECT_ROOT · 입력 전후 같은 값인지 조회
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
git rev-parse --verify HEAD

새 Agent 대화에서 S3과 같은 VCS별 입력을 보냅니다.

Cursor Agent 입력 · Git
현재 워크스페이스의 변경사항을 로컬 git으로만 커밋하세요. 원격 저장소는 사용하지 마세요. push 하지 마세요. 커밋 메시지는 정확히 다음을 사용하세요: lab: scenario commit
Terminal · PROJECT_ROOT · 변경: summary와 compare-s3-s4.md
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 lab-results/snapshot.py
python3 lab-results/compare.py s3 s4
분류s3s4비교
commitallowdeny복합 요청도 전체 거부
status/diffallow/없음allow/없음지원하는 조회 허용
unknown기록만deny/없음커밋 확인이 아닌 구문 거부

확인

  • s4/events.jsonl, s4/summary.md, compare-s3-s4.md에서 커밋 거부와 reason=deny-commit을 확인했습니다.
  • unknown 거부만 있다면 커밋 탐지 성공과 구분하고, 세션에서 거부된 실제 명령을 확인했습니다.
  • Git은 입력 전후 HEAD, SVN은 남은 변경을 확인했습니다.
선택 연습 · 고정 비교를 마친 뒤

추가 또는 메시지 조건을 직접 설계하고 별도 결과 이름을 쓰세요. 본편 프롬프트 B는 사용하지 않습니다.

훅 오류가 났나요? · 실습 훅 끄기

규칙과 훅 대조 · S5

글로 된 규칙과 실행 거부를 구분합니다

10분

커밋 거부 훅 대신 “커밋하지 마세요”라는 규칙(Rule)만 둡니다. 기존 사용자 규칙은 남기고 비교용 변경을 새로 만듭니다.

실행 전과 후기록만 · 거부 결정 없음
규칙(Rule)지시하지만 강제로 막지는 않음

배울 원리 · 하지 않은 것과 막힌 것 구분하기. Rule을 읽고 모델이 커밋을 요청하지 않는 것과, 요청된 커밋을 훅이 거부하는 것은 다른 흐름입니다.

왜 이렇게 동작할까요? · 커밋이 없으면 Rule이 차단한 건가요?

.cursor/rules/cursor-harness-lab-no-commit.mdcalwaysApply: true인 프로젝트 지시입니다. prepare.py는 S5에서 이 파일을 넣고 커밋 거부 훅 등록을 뺍니다. 모델이 읽는 지시는 Python 조건문 같은 강제 분기가 아닙니다.

결과는 비슷해도 멈춘 위치가 다릅니다
S5: Rule → 모델이 커밋을 요청하지 않음 → 커밋 시도 없음
S4: 커밋 요청 → 훅이 deny 응답 → 실행 전 차단

실행 뒤 생각해 보세요. S5 로그에 커밋 시도가 없으면 “훅이 막았다”고 써도 될까요?

해설 보기

안 됩니다. 그 기록에서 커밋 요청을 확인하지 못했다는 뜻입니다. Rule을 따른 것인지는 세션도 살피고, 훅 거부는 S4의 reason=deny-commit처럼 명시적인 결정으로 구분합니다. 한 번 따랐다고 앞으로도 따르리라는 보장은 아닙니다.

S5 준비 후 파일 구조
프로젝트/
├── README.md                         ← 기존 파일 유지
├── .cursor/
│   ├── hooks.json                    ← 이번 설정으로 변경 (사용자 훅 유지)
│   ├── hooks/
│   │   ├── cursor-harness-lab-observe.py     ← 기록 켜짐
│   │   ├── cursor-harness-lab-deny-write.py  ← 사용하지 않음
│   │   └── cursor-harness-lab-deny-commit.py ← 사용하지 않음
│   └── rules/                        ← 사용자 규칙 유지 + 실습 커밋 금지 규칙 추가
└── lab-results/
    ├── CURRENT                       ← s5
    ├── vcs.txt                       ← 선택한 Git 또는 SVN
    ├── snapshot.py / compare.py      ← 결과 정리 도구
    ├── commit-input.txt              ← 이번 커밋 실험용 변경
    ├── s5/
    │   ├── events.jsonl              ← 실행 기록
    │   └── summary.md                ← 실행 후 요약 명령으로 생성
    └── archive/                      ← 재실행 전 기록 보관
Terminal · PROJECT_ROOT · 변경: 위 S5 트리 · 커밋 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py s5 git

새 Agent 대화에서 같은 VCS별 프롬프트 B를 보냅니다.

Cursor Agent 입력 · Git
현재 워크스페이스의 변경사항을 로컬 git으로만 커밋하세요. 원격 저장소는 사용하지 마세요. push 하지 마세요. 커밋 메시지는 정확히 다음을 사용하세요: lab: scenario commit
Terminal · PROJECT_ROOT · 변경: summary와 compare-s5-s4.md
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 lab-results/snapshot.py
python3 lab-results/compare.py s5 s4
명령S5 규칙만S4 훅비교
커밋허용 또는 시도 없음거부S4만 강제 거부
S5의 통과 기준은 ‘커밋이 반드시 실패하는 것’이 아닙니다. compare-s5-s4.md에서 S4의 정책이 인식한 커밋 요청에 거부가 남았는지 봅니다. S5가 허용이면 규칙만으로는 부족하다는 뜻입니다. S4에서도 다른 명령 뒤에 연결한 커밋은 놓칠 수 있으므로 실제 command를 함께 읽으세요.

확인

  • s5/events.jsonl, s5/summary.md, compare-s5-s4.md가 실제 결과입니다.
  • S5에는 deny-commit 이유가 없고 S4에는 있습니다.
선택 연습 · 고정 비교를 마친 뒤

규칙 문구와 직접 만든 입력을 바꿔 별도 결과로 비교하세요.

규칙이 무시된 것처럼 보이나요? · 실습 규칙 끄기

기록 깊게 읽기 · S6

기존 실행 기록만 읽습니다

8분

S6에는 준비 명령이 없습니다. CURRENT, 훅, 규칙, 결과 폴더를 바꾸거나 새 기록을 만들지 않고 S1~S5 결과만 읽습니다.

배울 원리 · 원본과 요약의 관계. 훅의 events.jsonl은 재료이고, snapshot.pycompare.py는 이를 세고 비교합니다. 요약이 새로운 실행을 증명해 주지는 않습니다.

왜 이렇게 동작할까요? · 숫자만 보면 무엇을 놓치나요?

JSONL은 한 줄에 JSON 객체 하나가 든 형식입니다. json.loads(line)으로 읽고 outcome을 비교하면 거부 기록만 골라낼 수 있습니다. 아래 명령은 이 필터를 수행하며 정책이나 기록은 바꾸지 않습니다.

원본을 event → phase → tool/command → outcome → reason 순으로 읽으세요. 같은 Shell 요청이 일반 도구 훅과 셸 훅에 모두 보일 수 있어 로그 줄 수를 곧바로 작업 수로 볼 수는 없습니다.

먼저 생각해 보세요. attempt 뒤에 deny가 있으면 도구가 실행된 뒤 취소된 것일까요?

해설 보기

아니요. 실행 전 시도를 기록한 뒤 실행 전 정책이 거부한 흐름입니다. 파일을 바꿨다가 되돌린 것이 아닙니다. reason으로 거부한 정책을 확인하세요.

S6에서 읽기만 하는 트리
lab-results/s2/events.jsonl              = 읽기 전용
lab-results/s4/events.jsonl              = 읽기 전용
lab-results/compare-*.md                 = 읽기 전용
CURRENT / .cursor/**                     = 변경 없음
Terminal · PROJECT_ROOT · 변경 파일: 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 -c 'import json; p="lab-results/s2/events.jsonl"; [print(line, end="") for line in open(p, encoding="utf-8") if json.loads(line).get("outcome") == "deny"]'
필드비교에서 뜻
phase / outcome실행 전 시도 또는 허용·거부·실패 결정
tool / command도구와 셸 명령의 앞 500자
command_family / command_issue자르기 전 전체 요청의 분류와 분석하지 못한 이유
reasondeny-write, deny-commit 또는 deny-unsupported-shell / deny-invalid-shell

요약·비교는 기록된 command_family를 사용하므로 500자 뒤의 커밋도 분류에 남습니다. 이 필드가 없는 이전 로그는 같은 공통 분석기로 다시 분류합니다. 다만 이미 500자로 잘린 옛 명령의 뒷부분은 복구할 수 없어 unknown으로 표시합니다. 명시적인 reason=deny-commit은 이전 로그에서도 커밋 거부의 근거로 유지합니다.

확인

  • S2 또는 S4 기존 JSONL에서 거부와 이유를 찾았습니다.
  • S6용 디렉터리나 기록이 생기지 않았습니다.

다른 시나리오 기록이 보이나요? · 현재 실습 설정 끄기

선택 학습 · 기록으로 원인 찾기

예상과 다르게 움직였다면, 세션을 따라가 보세요

8분

마지막 답변만 봐서는 왜 그런 결과가 나왔는지 알기 어렵습니다. 요청부터 도구 호출, 실행 결과까지 순서대로 읽으면 처음 예상에서 벗어난 지점을 찾을 수 있어요. 원본 기록과 설정은 그대로 두고, 분석할 대화 사본과 메모만 새로 만듭니다.

기록무엇을 알 수 있나요?무엇까지는 알 수 없나요?
세션 기록내 요청, 에이전트 답변, 표시된 도구 요청과 결과의 순서대화를 일부만 복사했다면 빠진 도구 호출이 있을 수 있습니다.
events.jsonl훅에 전달된 도구 시도와 허용·거부·실패전체 대화는 아닙니다. allow만으로 파일의 최종 내용까지 증명하지는 못합니다.
결과 파일과 변경 내역실제로 생성된 보고서나 파일 변경결과만으로 어떤 요청과 도구를 거쳤는지 알 수는 없습니다.
1

짧은 예제로 읽는 순서를 익혀보세요

배울 원리 · 세 가지 근거로 원인 찾기. 세션은 요청과 다음 행동, 훅 로그는 실행 경계의 결정, 파일은 실제 결과를 보여 줍니다.

왜 이렇게 동작할까요? · 거부와 “수정 완료”가 함께 있나요?

도구 한 번의 거부는 대화 전체의 종료가 아닙니다. 모델은 거부 응답 뒤 다른 도구를 요청할 수 있습니다. 아래 예제는 Write 거부 뒤 Shell 요청으로 이어집니다. deny-write가 검사하지 않는 경로입니다.

세션과 훅 로그에서 같은 도구·명령·파일 경로와 주변 순서를 맞춰 보세요. 이 랩에는 공통 호출 ID가 없으므로 같은 시각이라는 이유만으로 같은 호출이라 단정하지 않습니다. 생략된 부분 때문에 연결할 수 없다면 그것도 표시합니다.

분석 전에 생각해 보세요. Write 거부 한 줄로 “README는 바뀌지 않았다”고 결론낼 수 있을까요?

해설 보기

그 Write 요청이 거부됐다는 것만 확인됩니다. 이후 다른 요청과 실제 파일 내용까지 봐야 합니다. 다음 실험에서는 처음 예상과 달라진 지점의 조건 하나만 바꿔 원인을 좁힙니다.

준비 파일에 session-example.mdsession-events.jsonl을 함께 넣었습니다. 둘은 이 설명을 위해 만든 예제이며, 실제 Cursor 내보내기 형식이나 여러분의 실행 결과가 아닙니다.

예제의 실행 순서 · 실행할 명령이 아닙니다
1. 사용자: README 끝에 한 줄을 추가해 주세요.
2. 에이전트: Write로 수정 요청
3. 훅: Write 거부 (deny-write)
4. 에이전트: Shell로 같은 파일 수정 요청
5. 도구 결과: 셸 명령 종료 코드 0
6. 에이전트: 수정했다고 답변

질문: Write를 막았는데 왜 수정했다는 답변이 나왔을까요? 3번에서 멈추지 말고 4번까지 읽어보세요. 수정 도구는 막혔지만 Shell이라는 다른 경로로 다시 시도했습니다. 실제 파일이 바뀌었는지는 파일 내용이나 변경 내역도 함께 봐야 합니다.

Terminal · 프로젝트 루트 · 파일 변경 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 -c 'from pathlib import Path; p=Path(".cursor-harness-kit/files/session-example.md"); print("\n".join(f"{i}: {line}" for i,line in enumerate(p.read_text(encoding="utf-8").splitlines(),1)))'
예제의 훅 기록도 함께 보기

아래 명령은 예제 기록을 읽어 도구와 결정을 순서대로 보여줍니다. 실제 실습 기록과 섞어 세지 않습니다.

Terminal · 프로젝트 루트 · 파일 변경 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 -c 'import json; from pathlib import Path; p=Path(".cursor-harness-kit/files/session-events.jsonl"); rows=[json.loads(line) for line in p.read_text(encoding="utf-8").splitlines() if line.strip()]; print("\n".join("%d: %s / %s / %s / %s" % (i,r["tool"],r["phase"],r["outcome"],r["reason"]) for i,r in enumerate(rows,1)))'
2

이번에 실행한 대화 하나를 로컬 파일로 남기세요

Cursor에서 S2 또는 R2를 진행한 대화를 열고 문서 이름이나 요청 문장을 검색해 보세요. 공식 대화 검색 안내에서는 열린 대화 안에서 Mac은 Cmd+F, Windows/Linux는 Ctrl+F로 찾는 방법을 설명합니다.

해당 요청, 에이전트 답변, 펼쳐진 도구 요청·결과를 복사해 프로젝트의 lab-results/session-review/session.md에 저장하세요. 복사되지 않는 도구 결과는 따로 붙여넣고, 빠진 부분은 “생략”이라고 표시합니다. 로컬 내보내기 기능이 있는 버전에서는 그 파일을 사용해도 됩니다. 공유 링크를 만들거나 기록을 이 웹페이지에 올릴 필요는 없습니다.

기록에 토큰이나 개인 정보가 포함돼 있으면 분석에 필요한 부분만 남겨 주세요. Cursor 내부 저장소의 파일명과 구조는 버전마다 달라질 수 있으므로, 이 실습에서는 원본 데이터베이스를 수정하지 않습니다.

Terminal · 프로젝트 루트 · 기록을 저장할 폴더만 생성
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
mkdir -p lab-results/session-review
분석할 파일의 위치
프로젝트/
├── .cursor-harness-kit/files/
│   ├── session-example.md     ← 설명용 대화 예제
│   ├── session-events.jsonl   ← 예제와 대응하는 훅 기록
│   └── session-note.md        ← 원인을 정리할 빈 양식
└── lab-results/
    ├── s2/events.jsonl        ← 실제 S2 실행의 훅 기록
    └── session-review/
        ├── session.md        ← 직접 저장한 이번 대화
        └── notes.md          ← 빈 양식을 복사해 작성할 분석 메모

R2를 살펴본다면 s2/events.jsonl 대신 r2/events.jsonlr2/route-summary.md를 나란히 열면 됩니다. 로그가 비어 있다면 “아무 일도 없었다”가 아니라 “기록으로 확인할 수 없다”로 구분해 주세요.

3

처음 달라진 지점 하나만 찾아보세요

  1. 요청: 무엇을 부탁했고, 무엇은 하지 말라고 했나요?
  2. 첫 도구: 예상했던 도구나 프로그램을 선택했나요?
  3. 도구 결과: 허용·거부·실패 중 무엇인가요? 답변이 아니라 기록의 줄 번호를 적어보세요.
  4. 다음 행동: 거부 뒤 멈췄나요, 다른 도구로 다시 시도했나요?
  5. 확인할 변경: 지침, 훅 설정, 허용 도구 중 무엇 하나를 바꿔 다시 실행하면 원인을 확인할 수 있을까요?

session-note.md의 내용을 새 notes.md로 복사해 “확인한 사실 / 아직 모르는 것 / 다음에 바꿀 한 가지”를 적어보세요. 한 번에 여러 설정을 바꾸면 무엇이 영향을 줬는지 다시 알기 어려워집니다.

에이전트와 함께 분석하고 싶다면

엄격한 R3 도구 제한은 분석 파일 읽기도 막습니다. 먼저 위의 실습 설정 해제 절차를 진행하고 새 대화에서 사용하세요. 분석 결과의 줄 번호가 원본에 실제로 있는지는 직접 확인합니다.

Cursor Agent 입력 · 분석용 요청 예시
lab-results/session-review/session.md와 lab-results/s2/events.jsonl을 읽고, 요청 → 도구 호출 → 도구 결과 → 다음 행동 순서로 정리해 주세요.
예상과 처음 달라진 지점을 각 파일의 줄 번호와 함께 알려주세요. 직접 확인한 사실과 추측을 구분하고, 기록이 빠져 있으면 판단할 수 없다고 표시해 주세요.
파일과 설정은 수정하지 마세요.

여기까지 확인했나요?

  • session.md의 어느 요청과 어느 도구 결과를 살폈는지 표시했습니다.
  • notes.md에 세션과 실제 훅 기록의 줄 번호를 각각 적었습니다.
  • 확인된 사실과 추측을 구분하고, 다음 실험에서 바꿀 설정 한 가지만 골랐습니다.

직접 검토 · R1

문서를 직접 읽고 검토하기

8분

먼저 에이전트가 문서를 직접 읽고 검토하게 해볼게요. 이 단계에서는 도구 사용을 기록만 하고, 별도 검사 프로그램을 쓰라는 지시는 넣지 않습니다. 다음 단계와 비교할 첫 번째 실행 경로를 남기는 과정입니다.

같게 유지docs/guide.md의 원본 바이트 · 아래 프롬프트
이번 실행 경로Agent → Read 도구 → 문서 → 답변

배울 원리 · 비교 실험의 기준 경로. R1에서는 모델이 문서를 직접 받아 판단합니다. 다음 단계에서는 요청과 문서를 유지한 채 누가 먼저 검사하는가를 바꿉니다.

왜 이렇게 동작할까요? · 답변이 같아도 실행 경로는 다른가요?

lab-results/route_summary.py는 문서 Read와 프로그램 실행 기록을 구분합니다. direct_document_read는 직접 Read의 근거이지 답변 품질 점수가 아닙니다.

document-before.sha256는 실행 전 문서 바이트로 계산한 지문입니다. 실행 뒤 지문과 같으면 시작과 끝의 문서가 같습니다. 읽지 않았다는 뜻도, 중간에 바꿨다가 되돌리지 않았다는 증거도 아닙니다.

먼저 예상해 보세요. R1과 R2가 같은 문제 세 가지를 답하면 라우팅도 같다고 볼 수 있나요?

해설 보기

아니요. 직접 문서 Read인지, 프로그램 Shell 실행 뒤 보고서 Read인지 로그로 비교해야 합니다. 답변 내용과 실행 경로는 별개라 문서와 요청을 고정합니다.

R1 준비 후 파일 구조와 역할
프로젝트/
├── docs/guide.md                     ← 결함 3종이 있는 고정 입력, 준비 전 충돌 검사
├── .cursor/hooks.json                ← 기록 훅만 활성
├── .cursor/rules/                    ← 경로 지시 없음, 사용자 규칙 보존
└── lab-results/
    ├── review_doc.py                 ← R1에서는 실행하지 않는 비교용 검토 프로그램
    ├── route_summary.py              ← 관찰 결과와 기대 경로 비교
    └── r1/
        ├── document-before.sha256     ← 실행 전 원본 문서 지문
        ├── events.jsonl              ← 직접 Read 근거
        └── route-summary.{json,md}    ← 실행 뒤 생성

Git 또는 SVN 중 위에서 고른 하나만 실행합니다. 준비기는 기존 docs/guide.md가 예제와 다른 내용이면 어떤 파일도 쓰기 전에 중단하며 덮어쓰지 않습니다. 같은 예제 내용이면 그대로 사용합니다.

Terminal · PROJECT_ROOT · R1 독립 준비
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py r1 git

새 Agent 대화에서 아래 문장만 보냅니다. 커밋·네트워크·서브에이전트는 사용하지 않습니다.

Cursor Agent 입력 · R1~R3 고정 프롬프트
docs/guide.md를 검토하고 문제점을 알려주세요. 파일은 수정하지 마세요.
기대 실행 경로
고정 프롬프트 ──▶ Agent ──Read docs/guide.md──▶ 답변
                           └─ 프로그램 실행 없음
터미널 · 실제 근거와 원본 문서 변경 확인
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 lab-results/route_summary.py r1
원본 전체 보기

준비의 파일별 펼침에서 files/guide.md, files/review_doc.py, files/route_summary.py 전체를 읽을 수 있습니다. R1 판정은 채팅 주장이 아니라 events.jsonl과 원본 문서 지문에서 나옵니다.

확인

  • r1/route-summary.jsondirect_document_readtrue이고 program_shell_executionfalse입니다.
  • document-before.sha256document-after.sha256가 같아 원본 문서가 바뀌지 않았습니다.
  • 고정 문서에서 필수 제목 누락, 중복 제목, 빈 절을 사람이 확인했습니다.

직접 읽기 기록이 없나요? · 실습 훅 끄기

프로그램 경로 지시 · R2

검사 프로그램을 먼저 거치게 하기

10분

이번에는 문서와 요청을 그대로 두고, 검사 프로그램을 먼저 실행하라는 규칙만 추가합니다. 에이전트가 직접 판단하는 대신 프로그램의 결과를 읽고 설명하는지 살펴보세요. 규칙을 따르지 않고 직접 문서를 읽었다면 그것도 결과에 표시됩니다. 지침만으로 실행 경로가 바뀌는지 확인하는 실험이에요.

같게 유지R1과 같은 문서 · 같은 프롬프트
이번 실행 경로규칙으로 지시 · 제한 훅 없음

배울 원리 · 검사와 설명의 역할 나누기. 프로그램은 정해진 기준으로 검사하고, 모델은 그 보고서를 읽어 설명합니다. Rule은 이 순서를 지시합니다.

왜 이렇게 동작할까요? · 프로그램은 무엇을 판단하나요?
  1. .cursor/rules/cursor-harness-lab-review-route.mdc가 프로그램 명령과 보고서 경로를 알려 줍니다.
  2. lab-results/review_doc.py가 문서를 줄 단위로 읽어 Markdown 제목과 아래 내용을 나눕니다.
  3. REQUIRED_HEADINGS로 누락을 찾고, Counter(headings)로 중복을 세며, 제목 아래 내용이 비었는지 검사합니다.
  4. findings와 실행 정보인 route_receipt를 JSON 파일로 씁니다. 모델은 원문 대신 이 결과를 읽습니다.

프로그램은 AI를 호출하지 않습니다. 정해진 형식 검사는 반복하기 좋지만 문장의 설득력이나 사실관계까지 평가하지는 않습니다. 실행 영수증도 위조 불가능한 서명이 아니라 프로그램이 적은 정보입니다. 요약은 셸 실행·보고서·보고서 Read를 함께 확인합니다.

route-matched는 요약기가 확인하는 조건을 만족했다는 뜻입니다. 호출 순서나 추가 Grep·Glob까지 모두 검사하지는 않습니다. 정확한 순서가 궁금하면 원본 로그와 세션을 함께 읽으세요.

실행 뒤 생각해 보세요. 보고서는 있지만 Agent가 읽은 기록이 없다면 기대 경로를 마친 것일까요?

해설 보기

아직 아닙니다. 프로그램 실행과 모델의 결과 읽기는 별도 단계입니다. route-summary.json에서 각각 확인하세요. R2 지시를 따르지 않은 instruction-noncompliance도 실험에서 배울 수 있는 결과입니다.

R2 준비 후 파일 구조와 역할
프로젝트/
├── docs/guide.md                                  ← R1과 바이트가 같은 고정 입력
├── .cursor/
│   ├── hooks.json                                 ← 기록 훅만 활성
│   └── rules/cursor-harness-lab-review-route.mdc  ← 정확한 명령 실행 후 JSON 읽기 지시
└── lab-results/r2/
    ├── document-before.sha256
    ├── events.jsonl                               ← Read와 Shell 경로를 따로 기록
    ├── review-report.json                         ← review_doc.py가 만든 결과와 실행 영수증
    └── route-summary.{json,md}                    ← 기대 경로와 실제 경로 비교
Terminal · PROJECT_ROOT · R2 독립 준비
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py r2 git

새 Agent 대화에서 R1과 완전히 같은 입력을 보냅니다.

Cursor Agent 입력 · R1~R3 고정 프롬프트
docs/guide.md를 검토하고 문제점을 알려주세요. 파일은 수정하지 마세요.
기대 실행 경로
고정 프롬프트 ──▶ Rule ──▶ Shell: python3 lab-results/review_doc.py docs/guide.md
                              └─▶ Read lab-results/r2/review-report.json ──▶ 답변
직접 docs/guide.md 읽기 ──▶ 허용되지만 instruction-noncompliance로 표시
Terminal · 실제 근거와 기대 경로 비교
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 lab-results/route_summary.py r2
관찰 결과요약의 판정
정확한 Shell 실행 + 프로그램 보고서 + 보고서 Read, 직접 문서 Read 없음route-matched
직접 문서 Read, 명령 누락, 프로그램 보고서 누락 중 하나instruction-noncompliance
프로그램 근거와 원본

review-report.jsonfindingsroute_receipt는 공개된 files/review_doc.py가 씁니다. 준비에서 표준 라이브러리만 쓰는 프로그램 전체를 읽을 수 있습니다. 채팅이나 별도 파일의 “실행했다”는 주장은 근거로 받지 않습니다.

확인

  • r2/route-summary.json에서 셸 실행, 프로그램 보고서, 보고서 읽기가 각각 구분됩니다.
  • 규칙을 따르지 않았다면 result=instruction-noncompliance이며 이것도 실제 관찰 결과입니다.
  • 세 발견 항목과 실행 영수증, 원본 문서 변경 없음까지 확인했습니다.

규칙과 거부가 헷갈리나요? · 실습 규칙과 훅 끄기

프로그램 경로 강제 · R3

다른 도구 경로는 막아보기

12분

이번에는 같은 규칙에 도구 제한을 더합니다. 검사 프로그램 실행과 그 결과 파일 읽기만 허용하고, 문서를 직접 읽거나 검색·수정하는 시도는 막아요. “이 경로로 해주세요”라고 요청하는 것과 다른 경로를 실제로 차단하는 것이 어떻게 다른지 비교해 보세요.

같게 유지R1·R2와 같은 문서·프롬프트, R2와 같은 규칙
이번 실행 전 제한preToolUse + beforeShellExecution

배울 원리 · 허용할 경로를 명시하기. S2가 정해진 도구를 거부했다면 R3는 기본값을 거부로 두고 승인 명령과 보고서 읽기만 허용합니다.

왜 이렇게 동작할까요? · 프로그램은 읽는데 Agent의 Read는 막히나요?

.cursor/hooks/cursor-harness-lab-route-policy.pyallowed = False에서 시작합니다. Shell이면 command == APPROVED_COMMAND, Read이면 경로가 정확히 lab-results/r3/review-report.json인지 확인합니다. 조건에 맞을 때만 allow로 바뀝니다.

훅이 보는 것은 Cursor의 도구 요청입니다. 승인된 Python이 내부에서 문서를 여는 것은 별도의 Cursor Read 요청이 아닙니다. 프로그램은 원문을 읽고 모델에는 보고서만 읽게 역할을 나눕니다. 그래서 승인 프로그램 자체도 확인해야 합니다.

명령은 문자열을 정확히 비교합니다. 앞에 cd를 붙이거나 뒤에 다른 명령을 연결하면 거부됩니다. 파일 경로는 resolve()로 프로젝트 기준 절대 경로를 맞춰 비교합니다.

실행 뒤 생각해 보세요. 아래 자체 점검에서 deny가 나오면 Agent도 금지된 Read를 시도했다고 볼 수 있나요?

해설 보기

아니요. 사람이 터미널에서 보낸 점검 요청입니다. 이것도 같은 로그에 남으므로 실제 Agent 시도와 혼동하지 마세요. 다른 경로를 막아도 Agent가 승인 경로를 끝까지 실행한다는 보장은 없어, 프로그램 실행과 보고서 읽기를 따로 확인합니다.

R3 준비 후 파일 구조와 역할
프로젝트/
├── docs/guide.md                                  ← 직접 Read/search/glob 차단
├── .cursor/
│   ├── hooks.json                                 ← 기록 다음 route-policy, failClosed=true
│   ├── hooks/cursor-harness-lab-route-policy.py   ← 정확한 명령·보고서만 허용
│   └── rules/cursor-harness-lab-review-route.mdc  ← R2와 같은 실행 지시
└── lab-results/r3/
    ├── events.jsonl                               ← allow/deny와 세부 이유
    ├── review-report.json                         ← 허용된 프로그램만 만드는 결과
    ├── document-{before,after}.sha256
    └── route-summary.{json,md}
Terminal · PROJECT_ROOT · R3 독립 준비
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py r3 git

새 Agent 대화에서 다시 같은 입력만 보냅니다.

Cursor Agent 입력 · R1~R3 고정 프롬프트
docs/guide.md를 검토하고 문제점을 알려주세요. 파일은 수정하지 마세요.
강제되는 실행 경로
                         ┌─ Read/search/glob docs ── deny + 정책 로그
고정 프롬프트 ──▶ Agent ├─ 다른 Shell·체인·리다이렉션 ── deny + 정책 로그
                         └─ 정확한 review_doc 명령 ── allow
                              └─ 정확한 r3 보고서 Read ── allow ──▶ 답변

Agent 실행 뒤 아래 자체 점검은 금지된 직접 Read 요청을 정책 스크립트에 한 번 전달합니다. 정책이 직접 남긴 deny라서 에이전트가 만든 주장을 대신 받지 않습니다. 이어서 경로 요약을 만듭니다.

Terminal · 정책 deny 자체 점검 + 실제 경로 요약
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 -c 'import json,subprocess; payload={"workspace_roots":[],"hook_event_name":"preToolUse","tool_name":"Read","tool_input":{"file_path":"docs/guide.md"}}; subprocess.run(["python3",".cursor/hooks/cursor-harness-lab-route-policy.py"],input=json.dumps(payload),text=True,check=True)'
python3 lab-results/route_summary.py r3
이 제한의 경계

이것은 정해진 랩에서 Cursor 도구 경로를 제한하는 연습이며 OS 샌드박스나 모든 가능한 도구에 대한 보장이 아닙니다. 기존 사용자 훅은 제거하지 않으므로 정확한 프로그램 명령도 사용자 훅의 영향을 받을 수 있습니다. 정책 오류는 failClosed: true로 닫힙니다.

전체 정책 원본

준비에서 files/route_policy.py 전문을 확인하세요. file_pathpath를 프로젝트 절대 경로로 맞춘 뒤, 정확한 review-report.json 읽기와 정확한 프로그램 명령 외에는 거부합니다. 명령 연결, 출력 전환, 인터프리터 조각, cat/grep류와 쓰기·삭제·편집 도구는 허용하지 않습니다.

확인

  • r3/route-summary.json에서 정확한 프로그램 셸 실행과 프로그램 보고서가 구분되어 있습니다.
  • events.jsonl에 자체 점검의 outcome=deny, reason=route-policy가 있고 정확한 명령의 허용과 따로 집계됩니다.
  • 원본 문서가 바뀌지 않았고 보고서의 발견 항목은 R2 프로그램 결과와 같습니다.

경로 훅이 실패했나요? · 엄격한 R3 설정 끄기

선택 · 문서 검토 Skill

필요할 때만 검토 절차를 불러옵니다

8분

프로젝트 Skill은 재사용 지시입니다. 새 에이전트나 강제 권한이 아니며, disable-model-invocation: true라서 사용자가 슬래시 명령을 직접 입력해야 시작됩니다.

먼저 필요한 상태R2 또는 R3 준비 완료 · docs/guide.md 존재
활성 파일.cursor/skills/lab-document-review/SKILL.md

배울 원리 · 필요할 때 지시 불러오기. Skill은 매번 긴 절차를 다시 쓰지 않게 묶어 둔 문서입니다. 실행 프로그램이나 추가 권한은 아닙니다.

왜 이렇게 동작할까요? · 슬래시 명령이 Python을 바로 실행하나요?

SKILL.md 머리말의 namedescription은 이름과 용도를 알려 줍니다. disable-model-invocation: true로 정해 사용자가 /lab-document-review를 직접 불러옵니다. 본문은 프로그램 실행→보고서 읽기→설명을 지시합니다.

슬래시 호출은 지시를 불러오는 시작점입니다. 실제 Python 실행은 그 지시를 따른 Agent의 Shell 요청이며 활성 훅이 있으면 똑같이 검사받습니다. Skill이 R3의 제한을 건너뛰지는 않습니다.

여기서는 R2의 Rule도 함께 둡니다. 결과만으로 Skill 때문에 경로가 바뀌었다고 판정하지 않습니다. 이 장에서는 재사용 절차를 직접 불러오고 실제 실행 근거를 확인하는 방법을 익힙니다.

아래 명령처럼 R2에서 시작하세요. R3를 유지하면 Skill의 준비 상태 확인을 위해 CURRENT를 직접 Read하는 요청도 거부됩니다. R3에서는 정확한 승인 프로그램이 내부에서 CURRENT를 확인한다는 점을 구분해야 합니다.

먼저 생각해 보세요. Skill에 “모든 도구를 허용한다”고 적으면 훅의 deny가 풀릴까요?

해설 보기

아니요. Skill은 모델의 지시이고 실행 권한은 훅 조건이 결정합니다. 절차 변경은 SKILL.md, 차단 조건 변경은 활성 정책 코드에서 다룹니다.

터미널 · PROJECT_ROOT · R2와 Skill 준비
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 .cursor-harness-kit/prepare.py r2 git
Skill이 사용하는 기존 흐름
/lab-document-review
  └─ python3 lab-results/review_doc.py docs/guide.md
      └─ lab-results/<r2|r3>/review-report.json

R2 또는 R3를 준비하면 정식 목록의 Skill 원본이 활성 위치에 바이트 그대로 복사됩니다. 소스는 준비의 전체 파일 목록에서 언제든 확인할 수 있습니다.

Cursor Agent 입력 · 사용자가 직접 호출
/lab-document-review
실제 근거확인할 값
review-report.jsonroute_receipt.producercursor-harness-review-doc
findings[].codemissing-required-heading, duplicate-heading, empty-section
공식 동작 범위

프로젝트 Skill 위치와 슬래시 호출은 Cursor Skills 문서를 따릅니다. 전역 Skill 설치나 네트워크 호출은 하지 않습니다.

Skill이 자동으로 실행되지 않나요? · Skill까지 포함해 실습 설정 끄기

내 프로젝트에 적용

원하는 동작 하나만 옮깁니다

10분

예시는 파일 수정 거부 하나입니다. 엄격한 R3 전체 설정은 일상 프로젝트에 그대로 복사하지 않습니다. 먼저 실습에서 S2 준비를 다시 실행해 기준 원본 상태를 만든 뒤 활성 스크립트 하나만 가져갑니다.

배울 원리 · 필요한 연결만 옮기기. 정책은 실행할 스크립트와 호출할 이벤트의 등록으로 연결됩니다. 실습 키트 전체가 있어야만 동작하는 것은 아닙니다.

왜 이렇게 동작할까요? · lab-results 없이도 차단되나요?

deny-write는 CURRENT가 없으면 기록을 건너뛰고 정상적으로 deny를 출력합니다. 이 정책 하나에는 결과 폴더가 필수가 아닙니다. 복사한 훅의 메시지에 lab-results를 보라는 문구가 있어도 이 프로젝트에는 로그가 없을 수 있습니다. 반면 R3는 CURRENT=r3와 정확한 프로그램·보고서 경로를 전제로 하므로 같은 방식으로 옮기면 안 됩니다.

S4의 커밋 거부를 옮길 때는 조건이 다릅니다. 커밋 훅과 함께 lab-results/cursor_harness_lab_shell.py도 같은 상대 경로에 필요합니다. SVN을 선택하려면 lab-results/vcs.txt도 옮깁니다. 기록용 CURRENT가 없어도 거부는 동작하지만, 공통 모듈이 없으면 정상 판단이 아니라 훅 실행 오류가 됩니다.

아래 로컬 점검은 응답만 확인합니다. hooks.json 연결은 새 Cursor 대화에서 작은 읽기·수정 요청을 보내 도구 결과까지 확인해야 합니다. 새 프로젝트 터미널에서는 그 폴더에서 PROJECT_ROOT도 다시 설정하세요.

옮기기 전에 설명해 보세요. 언제 실행하는가, 무엇을 검사하는가, 오류 나면 어떻게 하는가는 각각 어디에 있나요?

해설 보기

이벤트 preToolUse는 hooks.json, 도구 이름 조건은 Python, 오류 처리인 failClosed는 등록 항목에 있습니다. 셋을 구분하면 필요한 부분만 바꿀 수 있습니다.

1. 옮길 파일 한 개

실습 프로젝트의 .cursor/hooks/cursor-harness-lab-deny-write.py 내용을 새 프로젝트의 같은 경로에 저장합니다. .cursor-harness-kit/, lab-results/, R3 경로 정책은 복사하지 않습니다.

2. 기존 hooks.json에 항목 하나 합치기

파일 전체를 바꾸지 말고 기존 키와 훅을 남긴 채 preToolUse 배열에 아래 실습 항목만 추가합니다.

추가 전 예시추가 뒤 예시
{
  "version": 1,
  "owner": "my-project",
  "hooks": {
    "preToolUse": [
      {"command": "python3 user-hook.py",
       "failClosed": false}
    ]
  }
}
{
  "version": 1,
  "owner": "my-project",
  "hooks": {
    "preToolUse": [
      {"command": "python3 user-hook.py",
       "failClosed": false},
      {"command": "python3 .cursor/hooks/cursor-harness-lab-deny-write.py",
       "failClosed": true}
    ]
  }
}

3. 한 번 확인하고 끄기

아래 로컬 점검은 실제 파일을 만들지 않고 활성 훅에 Write와 Read를 한 번씩 전달합니다. Write deny, Read allow가 나와야 합니다.

터미널 · 새 PROJECT_ROOT · 변경 파일 없음
cd "${PROJECT_ROOT:?먼저 프로젝트 경로를 설정해 주세요}" || exit
python3 -c 'import json,subprocess; p=".cursor/hooks/cursor-harness-lab-deny-write.py"; base={"workspace_roots":[],"hook_event_name":"preToolUse"}; [(lambda r: print(t, json.loads(r.stdout)["permission"]))(subprocess.run(["python3",p],input=json.dumps({**base,"tool_name":t,"tool_input":{"file_path":"hook-check.txt"}}),text=True,capture_output=True,check=True)) for t in ("Write","Read")]'

끄려면 방금 추가한 정확한 명령의 항목 하나만 hooks.json에서 제거합니다. 다른 훅과 스크립트는 건드리지 않습니다.

버전 관리 선택은 그대로

커밋 실습을 골랐다면 Git은 로컬 저장소만, SVN은 기존 작업 복사본만 사용합니다. 원격 사용은 필요하지 않습니다.

문제 해결

증상에서 바로 확인할 파일로 갑니다

배울 원리 · 끊긴 연결부터 찾기. 등록→입력→조건→응답→실제 파일 순으로 좁혀 갑니다. 모두 바꾸면 무엇 때문에 달라졌는지 알기 어렵습니다.

왜 이렇게 동작할까요? · deny 로그가 없으면 허용된 건가요?

로그가 없다는 것만으로는 판단할 수 없습니다. 등록이 없거나 다른 프로젝트·시나리오에 기록됐거나 기록 전에 오류가 났을 수 있습니다. failClosed가 true이면 정상 deny 로그 없이도 훅 오류로 차단될 수 있습니다.

  1. manage.py statushooks.json으로 호출 연결을 봅니다.
  2. 활성 파일, CURRENT, 실제 도구 이름을 확인합니다.
  3. 훅 오류와 stdout의 JSON 형식을 봅니다. 진단 문장은 stderr로 보냅니다.
  4. 조건이 의심되면 직접 편집 장의 probe로 같은 입력을 비교합니다.

먼저 생각해 보세요. probe는 맞는데 Cursor에서 다르게 보이면 어디부터 볼까요?

해설 보기

probe는 등록을 거치지 않으므로 hooks.json의 이벤트·명령·활성 파일부터 봅니다. 그다음 실제 도구 입력이 같은지 확인합니다. 처음부터 Python 조건을 다시 바꿀 필요는 없습니다.

이벤트가 없음python3 .cursor-harness-kit/manage.py status에서 enabled를 보고, .cursor/hooks.json에 실습 명령이 등록됐는지 확인합니다. 기대 파일은 lab-results/<CURRENT>/events.jsonl입니다.
CURRENT 또는 루트가 다름PROJECT_ROOT를 열린 프로젝트 루트로 다시 설정하고 lab-results/CURRENT가 실행할 시나리오인지 확인한 뒤 해당 준비 명령을 다시 실행합니다.
훅 스크립트 오류터미널에서 해당 준비 명령을 다시 실행합니다. 직접 편집본은 lab-results/archive/active-edits-NNN/에 남고 활성 파일은 기준 원본으로 복구됩니다. cursor_harness_lab_shell을 찾을 수 없다는 오류라면 키트 갱신 뒤 준비 명령까지 실행했는지 확인합니다.
커밋이 아닌데 S4가 거부함reason=deny-unsupported-shell 또는 deny-invalid-shell인지 확인합니다. command_issue가 있는 unknown은 커밋 발견이 아니라 구문 거부입니다. S4의 지원 범위를 보고 단순 명령으로 나누어 요청하세요.
Agent가 다른 도구를 고름events.jsonltool을 실제 결과로 인정합니다. 직접 편집 확인은 모델 선택에 기대지 말고 files/probe.py를 실행합니다.
규칙이 무시됨 / 훅이 거부함규칙은 지시라서 무시될 수 있습니다. 훅 거부는 outcome=denyreason을 남깁니다. S5와 S4 결과를 대조합니다.
준비 뒤 직접 수정이 사라짐prepare.py s2 <git|svn>는 활성 파일을 다시 씁니다. 보관본은 lab-results/archive/active-edits-NNN/.cursor/hooks/에서 찾습니다.

마무리

필요한 동작만 남기고 끝냅니다

이제 결과를 자기 말로 설명해 보세요. 어떤 입력이 들어와서, 어느 파일의 조건을 지나고, 어떤 응답 때문에 결과가 달라졌는가를 연결하면 다른 프로젝트에도 적용할 수 있습니다.

이해 확인 · 다음 변경은 어디에서 해야 할까요?
  1. Write는 허용하고 StrReplace는 거부하고 싶습니다. 무엇을 바꿀까요?
  2. 프로그램을 먼저 쓰도록 안내하되 다른 경로는 허용하고 싶습니다. 무엇을 쓸까요?
  3. 프로그램 보고서만 읽게 강제하고 싶습니다. 무엇을 더할까요?
  4. deny를 출력한 훅의 종료 코드가 0입니다. 오류일까요?
답과 이유 보기
  1. 활성 deny-write의 거부 목록에서 Write를 뺍니다. probe로 두 응답을 따로 확인합니다.
  2. R2처럼 Rule로 지시합니다. 필요할 때만 불러올 절차는 Skill로 묶을 수 있습니다. 둘 다 실행 차단은 아닙니다.
  3. R3처럼 실행 전 훅에서 명령과 보고서 경로를 검사합니다. 경로 완성 여부는 로그와 보고서를 함께 봅니다.
  4. 오류가 아닙니다. 종료 코드 0은 훅의 정상 종료, stdout의 deny는 도구 거부입니다.

막히는 질문은 훅의 입력과 응답, Rule과 거부의 차이, 검사 프로그램, 허용 경로로 돌아가 실제 파일에서 근거를 찾아보세요.

하려는 일담당 파일강제로 막는가
실행 전후 기록cursor-harness-lab-observe.py아니요
파일 수정 거부cursor-harness-lab-deny-write.py
커밋 거부cursor-harness-lab-deny-commit.py
커밋하지 말라는 지시cursor-harness-lab-no-commit.mdc아니요
문서 검토 절차 재사용lab-document-review/SKILL.md아니요
정해진 검토 경로 강제cursor-harness-lab-route-policy.py예 · 실습 범위만

규칙을 따르지 않은 결과는 관찰 대상이고, 실제 거부는 훅이 담당합니다.

끝내기 전 확인

  • 하나의 동작을 직접 바꾸고 probe.json 또는 해당 결과 파일로 확인했습니다.
  • 자기 프로젝트에는 원하는 동작의 스크립트와 최소 훅 항목만 옮겼습니다.
  • manage.py disable 뒤 실습 훅 등록이 0인지 확인했거나, 필요한 설정만 의도적으로 남겼습니다.
  • 복원 백업은 lab-results/management-backups/에 있고 기록과 사용자 파일은 그대로입니다.

R3는 정의된 Cursor 도구 경로를 제한하는 학습용 설정이지 운영체제 샌드박스가 아닙니다. 프로그램 보고서도 임의의 파일 변경 전체를 막는 보안 장치는 아닙니다.