AI 에이전트는 선의로 사고를 친다: 실행 직전에 막는 '안전 후크' 만들기
요약
AI 에이전트의 편리함 이면에는 '선의'에 의한 보안 위험이 존재합니다. 본 글은 AI가 툴을 사용하려는 직전에 개입하여, 환경 파괴나 외부 데이터 유출 같은 부적절한 조작을 사전에 차단하는 '안전 후크(Safety Hook)' 메커니즘을 소개합니다. 이는 단순 규칙 설정 이상의 강제적인 보안 계층입니다.
핵심 포인트
- AI 에이전트의 위험은 악의가 아닌 선의에서 비롯된다.
- 규칙 기반 지시보다 실행 직전에 막는 '후크' 구조가 필수적이다.
- 안전 후크는 PC 환경 파괴, 설정 변경, 외부 데이터 전송을 차단한다.
- PreToolUse 후크를 통해 툴 사용 전에 강제적인 보안 검사를 수행할 수 있다.
서론: AI 에이전트는 '좋은 의도'로 행동한다
AI 에이전트(Cline, Claude Code, Cursor 등)에게 작업을 맡기면 파일을 수정하고, 명령어를 실행하며, 웹을 검색해 줍니다. 매우 편리하지만, 현장에서 무서운 것은 악의가 아니라 선의입니다.
- "종속 패키지가 부족해서 PC 전체에 넣어 두었습니다"
- "설정이 없어서 홈 폴더 안을 찾아보았습니다"
- "작동 확인을 위해 API로 전송해 보았습니다"
이 모든 것은 지시를 달성하려는 결과입니다. 하지만 회사 내부 PC에서 이런 일이 발생하면, 환경을 파괴하거나・프로젝트 외부를 들여다보거나・사외에 데이터를 보내는 보안 사고가 됩니다.
본 글에서는 필자가 집의 로컬 AI 환경(Cline + Ollama)에 구현한 '안전 후크(Safety Hook)'를 소개합니다. AI가 툴을 사용하려는 직전에 매번 개입하여, 약속에 어긋나는 조작이 실행되기 전에 막는 메커니즘입니다.
'규칙으로 부탁하는 것'만으로는 부족한 이유
우선 떠오르는 것은 AI에게 지시(규칙 파일)를 작성할 때 "〇〇은 하지 마세요"라고 적는 것입니다. 이것도 중요하지만, 요청에 불과합니다.
- 모델이 긴 작업 도중에 규칙을 잊어버린다
- "이번에는 예외"라고 판단해 버린다
- 작은 모델(로컬 LLM)일수록 지시를 따르는 힘이 약하다
그래서, 2단계 구조로 만들었습니다.
| 계층 | 역할 | 비유하자면 |
|---|---|---|
| 규칙 (약속) | AI에게 '해서는 안 될 것'과 막혔을 때의 행동 방식을 전달한다 | 사내 규정 |
| 안전 후크 (강제) | 툴이 실행되기 직전에 내용을 검사하고, 위반 시 실행하지 못하게 한다 | 출입 게이트 |
규칙은 '지켜달라'는 것이고, 후크는 '지키지 않았을 때 막는 것'입니다. 규칙을 빠져나간 조작도 반드시 후크를 거치게 하는 것이 핵심입니다.
지켜야 할 약속 3가지 + 1가지
PC 환경을 함부로 망가뜨리지 않기: 레지스트리, 환경 변수, 서비스, 작업 스케줄러, PC 전체에 소프트웨어 도입 등 -
설정을 임의로 변경하지 않기: git이나 npm의 글로벌 설정, VSCode 확장, 프로젝트 내부의 .vscode/나 .git/ 등 -
프로젝트 외부를 함부로 사용하지 않기: 열려 있는 폴더 밖은 읽는 것만으로도 막는다 -
외부에 전송하지 않기: curl, git push, 스크립트에서의 POST 등. 웹을 검색할 때는 읽기 전용의 전용 툴만 사용하게 한다.
세 번째 항목에서 '읽는 것만으로 막는다'가 핵심입니다. 홈 폴더에는 SSH 키, 클라우드 인증 정보, 브라우저 데이터 등이 있습니다. AI가 이를 읽으면 그 내용은 모델의 입력이 되고, 로그에도 남게 됩니다.
메커니즘: Cline의 PreToolUse 후크
Cline에는 툴을 실행하기 직전에 외부 프로그램을 호출하는 'PreToolUse 후크'가 있습니다.
Windows 버전의 Cline은 %USERPROFILE%\Documents\Cline\Hooks\에 있는 PreToolUse.ps1 파일을 찾습니다. 판별의 본체는 Node.js로 작성하고 싶었기 때문에, .ps1 파일은 전달만 받도록 했습니다.
# 판별은 같은 폴더의 guard.mjs가 수행한다.
# guard.mjs가 작동하지 않거나 아무것도 반환하지 않으면, 안전한 쪽으로 처리하여 멈춘다 (fail closed).
$utf8 = New-Object System.Text.UTF8Encoding($false)
...
판별 내용: 툴의 종류별로 살펴보기
판별 본체(guard.mjs)에서는 툴을 종류별로 나누어 검사합니다.
| 툴의 종류 | 보는 것 | 예시 |
|---|---|---|
| 파일 읽기/쓰기 | 경로가 워크스페이스 안에 있는지. 쓰기라면, 지켜야 할 설정 폴더가 아닌지 | `.. |
나 %APPDATA%경유 지정도 외부로 간주한다 | | 명령어 실행 | 위험한 명령어 패턴 | 레지스트리,setx, schtasks, winget install, npm -g, git config --global, curl, git push, ssh`
| 내장 웹 검색/브라우저 | 원칙적으로 모두 중단한다 | 웹은 읽기 전용의 전용 툴만 사용하게 한다 |
규칙은 '패턴'과 '막은 이유'의 쌍으로 가지고 있습니다. 막은 이유는 AI에게 그대로 반환되므로, AI가 '사용자에게 요청하는' 방향으로 전환됩니다.
const SYSTEM_RULES = [
[setx/i, "환경 변수의 영구 변경"]
[schtasks|(Register|Unregister|Set|Enable|Disable)-ScheduledTask/i, "작업 스케줄러"]
...
실제로 막히면 이렇게 보입니다
예를 들어, AI가 '경로를 통과시켜 놓겠습니다'라며 환경 변수를 덮어쓰려고 한 상황입니다. 후크에는 Cline으로부터 다음과 같은 JSON이 도착합니다 (이해하기 쉽도록 간소화했습니다).
{
"preToolUse": { "toolName": "execute_command" },
"tool_call": { "input": { "command": "setx PATH "%PATH%;C:\tools\bin"" } },
...
판단은 setx 규칙에 해당하므로, 후크는 다음과 같이 반환합니다.
{
"cancel": true,
"errorMessage": "【안전 후크로 중지】환경 변수의 영구 변경(setx PATH ...). 필요하다면 사용자에게 이유를 설명하고, 사용자 자신이 실행하도록 요청할 것."
...
명령어는 실행되지 않고 작업이 멈추며, AI는 '환경 변수 변경이 필요합니다. 이유는 〇〇이므로, 직접 설정해 주십시오'와 같이 사용자에게 설명하는 흐름으로 전환됩니다. 막힌 내용은 blocked.log에 한 줄씩 기록됩니다.
파일 조작도 같습니다. .. ..\..\Users<이름>\.ssh\id_ed25519처럼, 워크스페이스 외부를 가리키는 읽기는 상대 경로로 작성되어 있어도 절대 경로로 변환한 후 판단하므로, 외부로 나가는 순간 멈춥니다.
너무 많이 막지 않기 위한 노력
너무 많이 막으면 AI가 아무것도 할 수 없게 되어 결국 후크를 해제하고 싶어집니다. 예를 들어 Python 패키지는,
- PC 공통의 Python에 대한
pip install
→막음 - 프로젝트 내 가상 환경(.venv\Scripts\pip)에서의pip install
→허용
처럼, '프로젝트 내에서 완결되는 것'은 허용하고 있습니다.
웹은 '읽기 전용'의 전용 도구로만 하기
curl을 막으면 AI는 웹을 검색할 수 없게 됩니다. 그래서 가져오는 것만 가능한 작은 MCP 서버를 직접 만들고, 웹은 이 경로를 통해서만 사용하도록 했습니다.
| 제한 | 방지하려는 것 |
|---|---|
| HTTP는 GET만 허용. 요청 본문은 보내지 않음 | 데이터 전송 |
| ... |
테스트: 정말 막히는지
후크는 '막혀야 할 것이 막히는지'를 확인해야 의미가 있습니다.
- 판단 테스트:
49건 모두 합격 (막아야 할 것과 허용해야 할 것 모두) - Cline과 동일한 시작 방법으로 실행 테스트:
11건 모두 합격 -
guard.mjs를 삭제한 상태에서, 제대로 막히는지 확인 (fail closed 확인) - 1회 판단에 걸리는 시간: 약 0.3초
테스트에는 '막아야 할 것'뿐만 아니라 '허용해야 할 것'도 비슷한 양으로 넣었습니다. 너무 많이 막는 것은 후크가 해제되는 가장 큰 원인이 되기 때문입니다.
| 입력 예시 | 기대 결과 |
|---|---|
setx PATH ... | 막음 (환경 변수의 영구 변경) |
git push origin main | 막음 (외부로의 전송) |
type ..\..\secret.txt (워크스페이스 외부) | 막음 (프로젝트 외부) |
.venv\Scripts\pip install requests | 허용 (프로젝트 내 가상 환경) |
git status / npm test | 허용 |
src\app.py 수정 | 허용 |
.vscode\settings.json 수정 | 막음 (프로젝트 내에서도 설정은 지켜야 함) |
현장에 투입할 때의 운영
시스템을 만드는 것보다, 운영으로 인해 해제되지 않게 하는 것이 더 어렵습니다. 제가 주의하고 있는 점들을 꼽자면 다음과 같습니다.
1. 감시자를 AI가 변경하지 못하게 하기
후크 본체는 %USERPROFILE%\Documents\Cline\Hooks\라는 워크스페이스 외부에 두고 있습니다. 후크 자체가 '워크스페이스 외부는 읽기/쓰기가 금지'라고 판정하기 때문에, AI는 자신을 감시하는 후크를 읽거나 수정할 수 없습니다. 팀으로 배포할 때도 프로젝트 리포지토리 안에 두기보다는 각자의 PC의 정해진 장소에 두는 것이 좋습니다.
blocked.log를 매일 읽기
- 첫 1주일은 중단 기록을 읽으면서 '정말로 위험했던 것'과 '막지 않아도 될 것(오탐지)'을 파악할 수 있습니다. 오탐지를 발견하더라도 바로 규칙을 완화하기보다는, AI에게 그 작업을 시킬 필요가 정말 있는지부터 생각해야 합니다. 사람이 한 번 할 수 있는 일이라면, 멈춘 상태를 유지하는 것이 더 안전합니다.
3. 멈추면 '사람이 한다'를 당연하게 만들기
후크 메시지에는 '사용자 자신이 실행할 것'이라고 적혀 있습니다. AI가 멈췄을 때, 사람이 이유를 읽고 필요하다면 사람이 자신의 손으로 실행합니다. '후크를 일시적으로 해제하고 AI에게 시키는 것'이 습관이 되면, 후크는 금방 의미를 잃습니다.
4. Claude Code에서도 같은 방식으로 만들 수 있습니다
Claude Code에도 도구 실행 전에 호출되는 PreToolUse 후크가 있습니다. 입력 형태는 다르지만, '도구 종류별로 검토하기', 'fail closed', '감시자를 워크스페이스 외부에 두기'라는 생각은 그대로 적용할 수 있습니다.
한계: 이것은 '최후의 보루'가 아닙니다
솔직히 말씀드립니다. 이 후크는 실수 사고를 줄이는 장치일 뿐, 악의적인 공격을 막는 샌드박스는 아닙니다.
- 패턴으로 판정하기 때문에, 작성 방식을 고안하면 우회할 수 있습니다.
- '스크립트를 작성한 후 실행하는' 식의 간접적인 작업은 일부만 감지할 수 있습니다.
- AI가 읽은 프로젝트 내부 파일은 물론 모델에 전달됩니다.
진심으로 보호하려면, OS의 메커니즘과 결합하는 것이 기본입니다.
- AI 전용 사용자 계정, VM, 컨테이너 안에서 구동하기
- 명령어 자동 실행을 차단하고 매번 승인하도록 하기(글쓴이도 그렇게 하고 있습니다)
- 회사 기밀 코드는 애초에 클라우드 AI에게 보내지 않기(로컬 LLM 사용하기)
요약
- AI 에이전트의 사고는 악의보다 선의에서 발생한다 - 규칙으로 '부탁'하고, 후크로 '강제'하는 2단계 구조를 만든다
- 후크는 fail closed. 판정할 수 없으면 멈춘다 - 너무 많이 막지 않도록, 프로젝트 내에서 완결되는 작업은 통과시킨다. 테스트에는 '통과해야 할 것'도 포함한다
- 후크 본체는 워크스페이스 외부에 두고, AI가 감시자를 수정하지 못하게 한다
- 그럼에도 불구하고 후크는 만능이 아니다. OS 레벨의 분리와 결합하는 것이 필요하다
이 글의 후크는 로컬 LLM(Ollama)으로 구동하는 AI 코딩 환경의 일부입니다. 환경 전체 구성 방법과 RTX 4080에서의 실측값은 Zenn에 있는 '코드를 외부에 두지 않는 AI 코딩 환경'에 정리했습니다 (1, 2장은 무료로 읽을 수 있습니다).
후크 일체(Cline 버전/Claude Code 버전, 테스트 포함)는 정비된 후 배포할 예정입니다.
토론

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기