Claude Code에 이제 그림을 그릴 수 있게 되다: 에이전트 내부에서 검토하는 실험
요약
Anthropic의 Claude Code에 '모드(Mods)' 기능이 추가되어 에이전트 내부에서 동작을 검토하고 수정할 수 있게 되었습니다. 모드는 JavaScript/TypeScript 이벤트 핸들러로, 도구 호출이나 프롬프트 발생 같은 이벤트를 관찰하거나 변경하여 기본 동작을 막는 것이 가능합니다. 이는 개발자가 에이전트의 작동 방식을 깊이 이해하고 커스터마이징하는 데 중요한 변화를 가져옵니다.
핵심 포인트
- 모드는 Claude Code 내부에서 실행되는 이벤트 핸들러입니다.
- 도구 호출, 프롬프트 등 다양한 이벤트를 관찰하거나 변경할 수 있습니다.
- 그리기(Draw), 도구 호출 진입(Step into) 등 강력한 커스터마이징 기능이 추가되었습니다.
2주 전, Claude Code가 변경 로그 속의 수정 사항들 사이에서 놓치기 쉬운 무언가를 출시했습니다. 버전 2.1.287에는 릴리스 노트에 'Claude Mods: 플러그인이 이제 더 깊은 동작을 수정할 수 있습니다'라고 적힌 기능이 추가되었습니다. 이 설명으로는 그 기능을 충분히 과소평가한 것입니다. 모드(mod)란 Claude Code 내부에서 실행되는 코드로, 도구 호출이 발생하기 전에 모든 것을 확인하고 터미널에 자체 인터페이스를 그릴 수 있는 코드입니다.
저는 에이전트가 작성하는 코드를 읽기 위한 IDE인 Reado를 만들었습니다. 도구가 에이전트 자체의 작동 방식을 변경할 수 있게 된다면, 제가 가장 먼저 알고 싶은 것은 그것이 검토(review)에 어떤 의미를 가지는지입니다. 그래서 저는 문서를 읽고 Anthropic이 출시한 모드들을 살펴본 후, 작은 실험용 모드를 작성했습니다. 미리 분명히 하자면: 이것은 Reado의 일부가 아니며, 저는 이것을 배포하지 않을 것입니다. 이 글은 제가 이를 구축하면서 배운 것과, 모드가 검토 워크플로우에서 어디에 속하고 어디에 속하지 않아야 하는지에 대한 저의 생각을 담고 있습니다.
모드란 무엇인가
개요는 이를 명확하게 설명합니다: 모드는 'JavaScript 또는 TypeScript 이벤트 핸들러로 구성됩니다. Claude Code가 도구 호출, 제출된 프롬프트, 또는 인터페이스의 일부가 그려지는 것과 같은 이벤트가 발생할 때 하나를 호출합니다.' 각 핸들러는 이벤트를 관찰하거나, 변경하거나, 혹은 자체적으로 응답하여 기본 동작이 절대 실행되지 않도록 할 수 있습니다.
Claude Code는 이미 훅(hooks), 스킬(skills) 및 MCP 서버를 가지고 있었기 때문에, 무엇이 새로운지 묻는 것은 당연합니다. 차이점은 코드가 어디에서 실행되는가입니다. 설정 훅은 Claude Code가 시작하고 기다리는 셸 명령어입니다. MCP 서버는 모델에게 도구를 제공하는 별도의 프로세스입니다. 반면 모드는 Claude Code 자체의 프로세스 내에 있는 함수이기 때문에, 다른 것들이 할 수 없는 일들을 할 수 있습니다:
- 그리기(Draw). 대화 기록 옆에 패널이 생기거나, 프롬프트 위에 띠가 생기고 그 안에 텍스트, 버튼 및 입력 필드가 표시됩니다.
- Claude Code가 그린 것을 다시 그리기(Redraw what Claude Code draws). 도구 호출의 행, 스피너, 질문 대화 상자 등. 단, 모더레이터(mod)가 스타일을 변경할 수 없는 권한 프롬프트는 제외합니다.
- 도구 호출 내부로 들어가기(Step into a tool call). 이를 붙잡거나, 다시 작성하거나, 거부하거나, 도구를 실행하지 않고 답변할 수 있습니다.
- Claude의 턴 없이 즉시 실행되는 명령 추가. Claude가 작업하는 중에도 가능합니다.
모더레이터(mod)는 매니페스트(manifest), 코드를 가리키는 hooks.json, 그리고 코드 자체라는 세 개의 파일로 구성된 플러그인입니다. 빌드 단계가 필요하지 않으며, Claude Code는 .ts와 .tsx를 직접 로드합니다. 핸들러는 Express나 Koa를 작성해 본 사람이라면 익숙할 미들웨어(middleware) 형태를 가집니다:
on('tool.call', async ($, e, next) => {
// e는 호출 정보: e.tool, e.file_path, e.old_string...
const result = await next(e) // 도구를 실행하도록 둡니다.
...
$가 핸들러를 빠져나올 수 있는 유일한 방법입니다. 파일 읽기, 프로세스 실행, 모델 호출, 그리기 등 모든 것이 이 과정을 거칩니다. 이는 의도적인 설계 선택이며, 제가 나중에 설명할 보안과 관련이 있습니다.
첫 번째 모더레이터들은 검토에 관한 것입니다
제 시선을 사로잡은 것은 Anthropic이 이를 이용해 처음 구축한 기능들입니다. 살펴볼 만한 네 가지 모더레이터가 있는데, 두 개는 Claude Code 내부에 포함되어 있고 나머지 두 개는 사용자가 직접 설치하는 샘플입니다. 이들은 각기 다른 기능을 수행하지만, 각각 에이전트가 무엇을 하는지 검토하는 서로 다른 순간을 다룹니다:
| 시점 | 모더레이터 | 기능 |
|---|---|---|
| 위험한 명령 실행 직전 | blast-radius (샘플) | 위험한 셸 명령어(rm -rf, git reset --hard, 강제 푸시, 마이그레이션 등)를 붙잡고, 그것이 무엇을 변경할지 보여준 후 진행(Proceed) 또는 취소(Cancel)를 기다립니다. |
| ... |
이러한 순서로 배치하면 패턴이 명확해집니다. 에이전트의 작업 검토는 에이전트 내부로 이동하고 있습니다. 마지막에 한 번 풀 리퀘스트(pull request)를 하는 것이 아니라, 과정 중에 작은 점검들이 이루어지는 방식입니다.
저는 이것이 맞다고 생각하며, 또한 이 역시 한계가 있을 것이라고 생각합니다. 이에 대해서는 아래에서 더 자세히 설명하겠습니다.
실험 (The experiment)
Reado의 루프(loop)는 다음과 같이 작동합니다. 사용자가 코드를 읽고 정확한 라인에 주석을 남기며, 그중 일부는 태스크(task)로 표시되고 에이전트가 이 태스크들을 해결합니다. 이 주석들은 리포지토리 내 .reado/ 폴더에 존재하며, reado 커맨드라인 도구가 이를 목록화해 줍니다. 에이전트는 Reado의 MCP 서버나 CLI를 통해 이러한 주석들을 찾아냅니다.
문제는 반대 방향이 부족했다는 것입니다. 만약 Claude가 다른 작업 중에 사용자가 표시한 라인을 수정한다면, 사용자에게 아무것도 알려주지 않습니다. 사용자의 주석은 '센트(cents)를 정수형으로 유지하라'고 되어 있는데, Claude가 다른 이유로 해당 라인을 재작성하고, 사용자는 다음번에 파일을 읽을 때 그것을 알게 되거나, 아예 모르게 됩니다.
그래서 이 실험은 한 가지 기능을 합니다. 사용자가 열려 있는 Reado 태스크를 가진 파일에서 Claude가 편집할 경우, 프롬프트 위에 주석이 표시된 배너(band)가 나타납니다. 만약 그 수정이 주석이 고정된 정확한 텍스트를 대체했다면, 그 사실을 알려줍니다.

이것은 Reado의 터미널 내부에서 실행된 세션의 실제 스크린샷입니다. 여기는 모드의 핵심 부분이며, 그림을 그리는 코드를 제외한 모든 내용입니다:
async function tasksFor($: EngineInterface, path: string) {
const cwd = dirOf(path)
const top = await $.process.run(['git', 'rev-parse', '--show-toplevel'], { cwd })
...
이 배너는 AbovePrompt 사이트의 ui.render 훅입니다. 이 훅은 작은 트리를 반환하며, 여기에는 Box, Text, 그리고 Button 요소들이 포함됩니다. 이 요소들은 사용자가 터미널에 있는지 데스크톱 앱(Desktop app)에 있는지 여부에 따라 의존하기 때문에 $.ui.resolve(e)를 통해 가져옵니다. '알겠습니다 (Got it)' 버튼을 누르면 목록이 지워집니다.
설명할 가치가 있는 세 가지 결정체
파일이 아닌 CLI를 통해 주석을 읽습니다. 모드(mod)는 $.fs.read로 파일을 직접 읽을 수 있고, Reado의 주석은 일반 Markdown입니다. 하지만 CLI는 이미 어떤 주석이 열려 있는지, 어떤 것이 작업(task)인지, 그리고 각 주석이 어느 텍스트에 연결되어 있는지를 알고 있습니다. 이를 호출하는 것은 편집당 프로세스 비용이 발생합니다. 이를 재구현하려면 정확성(correctness)을 희생해야 하는데, 그것은 가장 비싼 종류의 손실입니다.
'정확하다(Exact)'는 것이 대체된 텍스트가 연결된 스니펫을 포함한다는 의미이지, 줄 번호가 일치한다는 의미가 아닙니다. 위에서 어떤 것이든 변경되면 줄 번호는 틀어집니다. Reado는 주석이 연결된 텍스트를 저장하고, Edit 호출은 old_string에 대체되는 정확한 텍스트를 전달합니다. 만약 한 문자열이 다른 문자열을 포함한다면, Claude가 당신이 주석을 단 것을 변경했다는 의미입니다. 이것은 단순한 문자열 확인이며, 줄 번호와 달리 위에서 발생하는 모든 변화에도 살아남습니다. Write는 파일 전체를 대체하므로, 그 안에 있는 모든 작업(task)이 건드려진 것으로 간주됩니다.
차단하지 않습니다. 이것이 진짜 결정이었습니다. tool.call 훅은 { deny: reason }을 반환할 수 있으며, 열린 작업을 가진 줄에 대한 어떤 편집도 거부하는 것은 한 줄의 변경으로 충분합니다. 저는 그것을 구현하지 않았는데, Reado의 루프에서 플래그가 지정된 줄에 대한 편집은 그 작업의 _수정(fix)_인 경우가 매우 많기 때문입니다. 당신이 요청한 것이죠. 여기에 가드(guard)를 두면 에이전트가 원하는 작업을 수행하는 도중에 멈추고, 당신에게 설명하기보다는 훅에게 자신을 설명하도록 밀어낼 것입니다. 차단은 작업 루프의 역할을 중복하게 만듭니다. 알림(notice)은 무언가 일어났다는 것을 알려주고, 그것이 중요한지 여부는 당신에게 결정권을 남겨둡니다.
테스트하기
모드는 claude plugin test라는 테스트 러너를 제공하며, 이 도구는 플러그인 아래에 아무것도 없는 상태에서 실제 엔진을 대상으로 사용자의 *.test.ts 파일을 실행합니다. 이는 편리한 것으로 판명되었습니다: 테스트가 모드가 수행하는 호출에 대한 답변을 합니다. 제 테스트는 process.run에 가짜 작업 목록으로 답변하고, Edit를 발생시킨 다음, 터미널과 데스크톱 표면 모두에 밴드(band)를 마운트하여 무엇이 그려졌는지 확인합니다:
on('process.run', async (_$, e) =>
e.argv[0] === 'git'
? { value: { exitCode: 0, stdout: '/repo\n', stderr: '' } }
...
테스트가 로드되기도 전에 버그를 잡아냈습니다. 저는 루프 변수 이름을 h로 지정했는데, 모드(mod) 내의 JSX는 전역 h 호출로 컴파일됩니다. 렌더(render) 훅이 "h is not a function" 오류와 함께 충돌했습니다. JSX 팩토리(factory)를 가리는 것은 두 번 실수할 일이 아닙니다.
작동을 멈추는 곳
모드는 샌드박스되지 않습니다. 문서에는 명확하게 나와 있습니다: 모드는 "사용자의 권한으로 실행되는 코드"입니다. 모드는 사용자의 파일을 읽고 쓸 수 있고, 모든 프롬프트(prompt)를 볼 수 있으며, 도구 호출(tool call)을 재작성하고, 사용자에게 요청되기 전에 도구 호출을 승인할 수 있습니다. 만약 샌드박싱(sandboxing)을 켜 놓았다면, "모드가 시작하는 프로세스는 그 외부에서 실행됩니다." GitSpawn 이후로 저는 모드가 .git/config나 에이전트 설정과 같은 목록에 속해야 한다고 생각합니다: 본질적으로 불활성(inert)하다고 취급하는 파일이지만, 실제로는 코드인 파일들입니다.
완화책은 좋으며, 이는 모든 것을 $를 통해 라우팅하는 것에서 비롯됩니다. 모드를 설치하기 전에, claude plugin validate는 해당 소스를 읽고 연결되는 모든 이벤트와 수행하는 모든 호출을 나열합니다. 제 모드의 경우:
hooks: prompt.submit, tool.call, ui.render{component=AbovePrompt}
calls: $.process.run (via tasksFor), $.state.get, $.state.set, $.ui.resolve
두 줄만으로도 이 모드가 프로세스를 실행하고 그림을 그리며, 네트워크나 파일 시스템에 직접적으로 손대지 않는다는 것을 알 수 있습니다. 무언가를 설치하기 전에 그 줄들을 읽어보세요.
그림 그리기는 터미널 및 데스크톱에서만 가능합니다. VS Code 확장 프로그램의 채팅 패널에서는 훅(hooks)이 실행되지만, 그려지는 것은 아무것도 나타나지 않으며, claude -p와 Agent SDK도 마찬가지입니다. 그림을 그리는 모드는 자신이 어디서 실행되고 있는지 확인하고 일반적인 줄로 대체되어야 합니다.
Claude Code 자체의 수정 사항만 볼 수 있습니다. Bash 호출에서 sed를 통해 이루어진 수정은 Edit 도구를 거치지 않기 때문에, 제 모드는 이를 감지하지 못합니다. Bash 명령어도 매칭할 수는 있지만, 쉘(shell) 명령어가 어떤 파일을 건드리는지 추측하는 것은 제가 검토 도구에 원하지 않는 종류의 휴리스틱(heuristic)입니다.
그리고 더 큰 한계: 프롬프트 위의 밴드(band)는 검토가 아니라 알림입니다. 이 밴드는 에이전트가 당신이 신경 쓰는 라인에 '접촉했다'는 사실만을 알려줍니다. 해당 파일의 라인, 주변 함수, 또는 그것을 호출하는 다른 세 곳의 코드를 보여주지는 않습니다. Diff 패널(Diff panes), 리플레이 극장(replay theaters), 그리고 사이드 에이전트(side agents) 모두 이 형태를 공유합니다. 즉, 코드 자체가 아니라 '변경 사항'만을 제공한다는 것입니다. 이것은 유용하지만, 제가 별도의 읽기 공간을 계속 구축하는 이유이기도 합니다. 이러한 모드(mod)가 그 둘을 연결할 수 있습니다. 어느 한쪽도 대체하지는 않습니다.
Reado에서 의미 있는 것은 무엇일까요
그래서, 이것을 만들었다면: Reado에 모드를 넣을까요? 이번 모드는 아니며, 당분간은 아닙니다. 제 추론은 이렇습니다.
Reado는 모든 에이전트와 작동합니다. 반면, 모드(mod)는 특정 에이전트와 작동합니다. Reado의 코멘트는 CLI와 MCP 서버를 통해 에이전트에 도달하기 때문에, 동일한 루프가 Claude Code, Codex 또는 명령을 실행하거나 MCP 서버와 통신할 수 있는 모든 에이전트에서 작동할 수 있습니다. 모드는 오직 Claude Code 내부에서만 실행됩니다. 워크플로우에 필수적인 것은 모든 에이전트가 사용할 수 있는 부분에 남아 있어야 하며, 그렇지 않으면 이 도구는 조용히 Claude Code의 액세서리가 되어버립니다.
이 특정 알림은 필요하지만 필수는 아닙니다. 에이전트가 작업하는 동안 터미널을 지켜보고 있을 때는 도움이 됩니다. 하지만 Reado의 워크플로우에서는 나중에 파일에서 변경 사항을 읽게 되며, 코멘트는 그 라인에 바로 있습니다. 이 밴드는 당신이 어차피 보게 될 무언가에 대해 알려주는 것입니다.
모드가 자리를 차지할 수 있는 곳은 에이전트만 볼 수 있는 순간입니다. 모드는 편집이 일어나는 '동안' 존재합니다. 어떤 턴(turn)이 그것을 만들었는지, 사용자가 무엇을 요청했는지, 그리고 Claude가 자신이 무엇을 하고 있었다고 말했는지를 알고 있습니다. 편집기에서 파일을 열 때쯤이면 이 모든 것이 사라지고, diff는 그저 diff일 뿐입니다. Reado는 이미 에이전트가 reado thought를 사용하여 자신의 추론 과정을 서술하게 하지만, 그것은 에이전트가 결정할 때만 가능합니다. 모드는 에이전트에게 아무것도 요청하지 않고도 플래그가 지정된 모든 편집의 '이유(why)'를 포착할 수 있습니다. 이것이 이번 실험에서 제가 실제로 구축하는 것을 고려할 단 하나의 아이디어이며, 단순히 모드가 새롭기 때문이 아니라 일상적인 사용에서 지속 가능성이 입증될 경우에만 그렇습니다.
제가 얻은 일반적인 교훈은 이렇습니다: 에이전트 내부에서만 알 수 있는 것을 에이전트 내부에 배치하세요. 전체 파일을 볼 수 있는 곳에서 읽기를 수행해야 합니다.
사용해 보기
Claude Code 2.1.287 이상 버전을 사용한다면, 모드가 무엇을 할 수 있는지 확인하는 가장 빠른 방법은 샘플인 blast-radius를 사용하는 것입니다. playground repository를 클론하고, 폴더에 대해 claude plugin validate를 실행하여 어떤 작업을 하는지 확인한 다음, claude --plugin-dir로 한 세션 동안 로드하세요. 또는 /plugin enable cc-plugin-you-should-know@builtin으로 내장 사이드 에이전트를 켜세요.
만약 Reado가 루프의 어느 부분(에이전트가 해결하는 정확한 라인에 대한 주석, 사용하려는 어떤 에이전트를 사용하든 상관없이)에서 작동하는지 궁금하다면, 무료 오픈 소스입니다. 그리고 만약 자신만의 리뷰 모드를 작성한다면, 보고 싶습니다. 댓글이나 Discord에 알려주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기