AI 코딩 에이전트가 비밀 정보를 유출할 수 있습니다: Claude Code용 로컬 사전 커밋 DLP 구축
요약
AI 코딩 에이전트가 로컬 대화 기록에 민감한 자격 증명(API 키, 비밀번호 등)을 남겨 유출 위험이 있습니다. 본 글은 이러한 사각지대를 탐지하고 삭제하는 AgentSweep 도구와 사전 커밋 훅 구축 방법을 제시합니다. 이는 코딩 어시스턴트의 로컬 히스토리 관리 취약점을 다룹니다.
핵심 포인트
- AI 에이전트는 대화 기록에 민감 정보를 남깁니다.
- AgentSweep은 Claude Code, Codex 등 다양한 에이전트의 JSONL 파일을 스캔합니다.
- 사전 커밋 훅을 사용하여 코드가 커밋되기 전 비밀 정보 유출을 방지해야 합니다.
- 단순히 키를 로테이션하는 것만으로는 기록에 남은 바이트를 제거할 수 없습니다.
당신이 데이터베이스 연결을 디버깅하기 위해 .env 파일을 Claude Code에 붙여넣었다고 가정해 봅시다. 당신은 다음 메시지에서 자격 증명을 제거하고, 저장소를 정리하며, 데이터베이스 비밀번호를 변경합니다. 하지만 당신의 로컬 대화 기록에는 원래의 내용이 여전히 남아 있습니다.
Claude Code는 ~/.claude/projects/ 아래에 JSONL 트랜스크립트를 보관합니다. Codex는 세션 JSONL을 ~/.codex/sessions/ 아래에 보관합니다. 사용자 권한으로 실행되는 프로세스는 두 에이전트 모두에게 허락을 구하지 않고도 이 파일들을 읽을 수 있습니다.
저는 이러한 기록을 스캔하고, 자격 증명 형태의 문자열을 보고하며, 기록 형식을 깨뜨리지 않으면서 이를 삭제하는 AgentSweep를 만들었습니다. 또한 개발자들이 코드를 커밋하기 전에 이 별도의 비밀 저장소를 확인할 수 있도록 사전 커밋 훅(pre-commit hook)도 추가했습니다.
이 제목은 자격 증명 노출을 설명하는 것이지, 당신의 코딩 어시스턴트가 키를 공격자에게 전송했다는 증거는 아닙니다. 개발자가 비밀 정보를 붙여넣거나 에이전트가 비밀 정보를 포함한 도구 출력을 캡처하게 할 때 노출이 발생합니다. 공격자는 디스크에 남겨진 복사본을 악용합니다.
저장소 외의 자격 증명 저장소
저장소 스캐너는 스테이징하거나 커밋하는 파일만 확인합니다. 하지만 당신 에이전트의 기록은 다른 곳에 존재합니다:
Developer home
|
+ .claude/
...
에이전트 버전마다 레이아웃이 다릅니다. AgentSweep는 conversations/ 디렉토리가 존재한다고 가정하는 대신, Claude Code의 프로젝트 트리에서 *.jsonl 파일을 탐색합니다. 또한 기본 홈을 스캔할 경우 개발자가 위치를 옮긴 프로필을 놓칠 수 있기 때문에 CLAUDE_CONFIG_DIR과 CODEX_HOME도 준수합니다.
개발자들은 이러한 기록에 API 키보다 더 많은 것을 축적합니다: 비밀번호가 포함된 PostgreSQL URL, AWS 액세스 키 ID, GitHub 토큰, 개인 키 블록, 그리고 지갑 복구 구문 등. 어시스턴트의 응답은 프롬프트에서 자격 증명을 반복할 수 있고, 도구 출력은 또 다른 사본을 도입할 수 있습니다.
Cursor, Windsurf, 그리고 Aider도 로컬 히스토리를 유지하지만, 저장 형식이 다릅니다. Aider는 Markdown 히스토리를 사용하고; 다른 통합 기능들은 JSON 또는 SQLite를 사용합니다. 모든 코딩 어시스턴트를 JSONL 생성기로 취급하면 이러한 저장소들을 놓치게 됩니다. AgentSweep은 소스 어댑터(source adapters)를 사용하여 검색, 문자열 추출 및 형식별 마스킹을 수행합니다.
키를 로테이션(Rotating a key)하는 것은 해당 자격 증명을 제공자 측에서 무효화할 뿐입니다. 이는 전사 기록(transcript), 백업 또는 파일 시스템 스냅샷에 있는 바이트를 제거하지 않습니다. Git 히스토리를 정리한다고 해도 이러한 디렉토리들은 건드리지 않습니다.
설치 스크립트는 파일 시스템 접근 권한으로 실행됩니다
패키지를 손상시킨 공격자는 설치 과정 중에 코드를 실행할 수 있습니다. npm의 postinstall 스크립트는 설치하는 사용자의 접근 권한을 상속받습니다. 공격자들은 동일한 목적으로 Python 패키지 빌드 또는 설치 실행 경로를 사용할 수 있으며, PyPI는 npm의 라이프사이클 훅(lifecycle-hook) 이름을 사용하지 않습니다.
연구원들은 이러한 유형의 도난 사례를 문서화했습니다. Aikido가 분석한 손상된 @bitwarden/[email protected]에서 악성 preinstall 페이로드는 .env, 클라우드 자격 증명, ~/.claude.json, 그리고 ~/.claude/mcp.json 등의 파일을 목표로 삼았습니다. 해당 보고서는 AI 도구 설정 자격 증명의 도난을 입증합니다. 하지만 이 특정 페이로드가 대화 JSONL을 수집했다는 것은 입증하지 않습니다.
전사 기록 수집기(transcript harvester) 역시 동일한 파일 시스템 접근 권한이 필요합니다. 아래 다이어그램은 해당 공격 경로를 모델링합니다:
flowchart TD
D[개발자가 npm 종속성을 설치함] --> P[손상된 패키지가 postinstall을 실행함]
P --> U[페이로드가 개발자 사용자로서 실행됨]
...
JSONL 파싱에서 악용(exploit)할 필요가 없습니다. 공격자는 일반 파일을 읽기만 하면 됩니다. 소유자 전용 권한 모드(owner-only permission mode)는 다른 로컬 사용자에 대해서는 도움이 되지만, 계정 하에서 실행되는 악성 코드를 막지는 못합니다.
AgentSweep은 저장된 히스토리 노출을 줄입니다. 이는 유출을 취소하거나, 제공자의 프롬프트 사본을 제거하거나, 악성 코드가 라이브 .env 파일을 읽는 것을 막을 수는 없습니다.
다섯 단계, 명확한 쓰기 경계
저는 발견(discovery)과 탐지(detection)를 변형(mutation) 과정에서 분리했습니다. scan은 발견 보고서로 끝납니다. fix는 마스킹(redaction) 단계를 추가하지만, 개발자는 여전히 노출된 자격 증명을 직접 취소해야 합니다.
flowchart LR
D[1. Discover: 소스 히스토리 루트 탐색] --> S[2. Scan: Aho-Corasick 및 정규식 규칙 + BIP-39]
S --> F[3. Findings: 마스크 처리된 보고서와 위치]
...
AgentSweep은 스캔 및 마스킹 과정에서 네트워크 요청을 전혀 하지 않습니다. 로컬 히스토리를 읽고, 로컬 매칭을 수행하며, 로컬 결과를 작성합니다. 설치는 패키지를 다운로드하고, 취소 URL을 여는 것은 브라우저를 사용합니다. 별도의 업데이트 명령어만이 PyPI에 접촉합니다. 이 모든 행동은 스캔 또는 마스킹 파이프라인에 속하지 않습니다.
저는 그 경계를 선택했습니다. 왜냐하면 자격 증명 정리 도구는 검사하는 자료를 업로드해서는 안 되기 때문입니다.
발견(Discovery): 스키마 추측 없이 문자열 추출
JSONL의 경우, 소스 어댑터가 레코드를 파싱하고 위치 정보와 함께 문자열 값을 산출합니다. 스캐너는 발견 사항을 파일, 물리적 라인, 중첩된 키 경로에 연결할 수 있습니다. 마스커는 그 후 파싱된 레코드에서 해당 값에 도달할 수 있습니다.
소스 계층은 SQLite 및 Markdown에도 중요합니다. 데이터베이스 어댑터는 행 인식 업데이트와 무결성 검사가 필요합니다. 텍스트 어댑터는 라인 보존 교체가 필요합니다. 탐지 규칙을 공유하는 것이 직렬화 전략을 공유할 필요는 없습니다.
스캔(Scan): 모든 문자열에 대해 207개 패턴 실행 방지
AgentSweep은 207개의 정규식 규칙과 더불어 기억술(mnemonic) 탐지를 문서화합니다. 각 패턴을 각 프롬프트에 적용하는 것은 일반적인 소스 코드와 산문에 많은 반복 작업을 수행하게 합니다.
저는 Aho-Corasick 사전 필터링을 사용하여 공유된 패스로 규칙 키워드를 식별합니다. 매처는 이러한 키워드들을 실패 링크(failure links)가 있는 트라이(trie)로 표현합니다. 키워드를 찾은 후, 스캐너는 정규식 평가를 위해 관련된 규칙들을 선택합니다.
키워드 일치(keyword hit)가 문자열에 자격 증명(credential)이 포함되어 있음을 증명하지는 않습니다. 정규식은 여전히 형태와 경계를 확인합니다. 반대로, 안전한 키워드 게이트(safe keyword gate) 없이 규칙을 가진 경우, 키워드 일치 없이도 적격 상태로 남아 있어야 합니다. 그렇지 않으면 최적화 과정에서 오탐지(false negatives)가 발생할 수 있습니다.
지갑 구문(wallet phrases)의 경우, 단어 목록에 포함되는 것만으로는 충분하지 않습니다. BIP-39 검출기(detector)는 12개, 15개, 18개, 21개 또는 24개의 단어로 구성된 후보 길이를 확인하고 체크섬을 검증합니다. 이는 우연히 암호학적 구문어(mnemonic words)를 포함하는 많은 영어 산문을 거부합니다. 유효한 체크섬은 암호학적 구문어 형태의 값을 식별할 뿐이며, 이와 관련된 지갑에 누군가가 자금을 지원했다는 것을 증명하지는 않습니다.
정규식 엔진: RE2 추가 시 호환성 유지하기
기본 설치는 Python의 re를 사용합니다. 개발자는 선택적 네이티브 백엔드를 설치할 수 있습니다:
uv tool install 'agentsweep[fast]'
AgentSweep은 호환 가능한 패턴에 대해 google-re2를 사용합니다. RE2는 백트래킹(backtracking)을 방지하고 해당 표현식에 대한 선형 시간 매칭(linear-time matching)을 제공하여, 개발자가 대량의 로그를 세션에 붙여넣을 때 도움이 됩니다.
RE2가 모든 Python 정규식 구문을 지원하는 것은 아닙니다. 룩어라운드(Lookarounds), Python 전용 앵커(anchors), 그리고 유니코드 의미론(Unicode semantics)은 주의가 필요합니다. AgentSweep은 보호된 비-ASCII 입력(guarded non-ASCII inputs)을 포함하여 지원되지 않는 규칙과 의미론적 예외 케이스에 대한 Python 경로를 유지합니다. 또한 네이티브 디스패치로는 도움이 되지 않을 짧은 문자열 및 밀집 매칭(dense matches)에 대해서도 해당 경로를 유지합니다.
저는 전체 혼합 파이프라인에 대해 선형 시간 스캐닝을 주장한 것이 아닙니다. Python 폴백 규칙은 여전히 Python의 엔진을 사용합니다.
커밋된 호환성 감사는 202개 규칙 스냅샷에서 144개의 RE2 규칙과 58개의 stdlib 규칙을 기록하고 있습니다. 이 스냅샷은 README에 명시된 207개 규칙 수보다 이전의 것입니다. 이는 현재 목록이라기보다는 라우팅 설계에 대한 증거로 간주하십시오. 규칙 개수는 버전 정보와 함께 필요합니다.
읽기 전용 보고서부터 시작하기
AgentSweep은 Python 3.11 이상을 요구합니다. 격리된 도구 환경에서 CLI를 설치한 다음, 소스를 선택하십시오:
스캔 종료 코드는 스크립트에 작은 계약을 제공합니다:
| 종료 코드 | 의미 |
|---|---|
0 | 발견된 내용 없음 |
| ... |
구체적인 보고서 예시를 위해, 테스트 기록에 AWS 문서 샘플 키인 AKIAIOSFODNN7EXAMPLE가 포함되어 있다고 가정해 봅시다. 다음 블록은 실행 기록이나 터미널 형식에 대한 바이트별 약속이 아니라 발견된 정보만을 보여줍니다:
source claude-code
file <project>/session.jsonl
line 1
...
전체 자격 증명을 이슈나 다른 채팅에 복사하지 않고 보고서를 검토하세요. 탐지기는 제공업체가 해당 값을 여전히 수락하는지 알 필요 없이 자격 증명 구문을 인식할 수 있습니다. AgentSweep은 오프라인으로 작동하므로 키의 유효성을 테스트하지 않습니다.
알려진 오탐(false positive)의 경우, 좁은 .agentsweepignore 항목을 사용하세요. 전체 제공업체 규칙을 차단하면 해당 제공업체의 향후 유출이 후크에서 보이지 않게 됩니다.
JSON 값 마스킹 및 파싱 가능한 기록 유지하기
직렬화된 JSON에 대한 원시 치환은 이스케이프 처리를 손상시키거나 의도한 값을 벗어난 구문을 소비할 수 있습니다. 저는 파싱된 레코드에서 문자열 값을 마스킹하고, 그 후 레코드를 다시 직렬화합니다.
이러한 예시적인 단일 레코드 기록을 고려해 보세요. AWS 키는 작동하는 자격 증명이 아니라 공개 문서 예시에서 가져온 것입니다.
Before:
{"type":"user","message":{"content":"AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE"}}
After:
{"type":"user","message":{"content":"AWS_ACCESS_KEY_ID=[REDACTED:aws-access-key]"}}
값 수준 차이(Value-level diff):
- AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
+ AWS_ACCESS_KEY_ID=[REDACTED:aws-access-key]
마스킹기는 레코드의 구조를 유지합니다. 재직렬화는 공백을 변경할 수 있으므로, 저는 비밀 정보 외의 바이트별 동일성을 약속하지 않습니다.
프로덕션 기록의 경우, 에이전트를 종료하고 수정하기 전에 최근 쓰기(recent-write) 창이 만료되도록 두세요:
대화형 모드에서는 발견된 내용을 검토하고 프롬프트가 나타날 때 REDACT을 입력하세요. 현재 알파 버전은 프로덕션 루트 옵트인(production-root opt-in)이 필요합니다. 백업 기능을 활성화 상태로 유지하세요.
8가지 핵심 쓰기 보호 장치
저는 이 작업 주변에 8가지 핵심 보호 장치를 사용합니다. 또한 프로젝트는 알파 프로덕션 게이트와 감사 로그(audit log)를 문서화하고 있습니다.
| 보호 장치 | 엔지니어링 이유 |
|---|---|
| 대체 전 구문 분석 (Parse before replacement) | JSON 구분자 편집 없이 문자열 값을 대체합니다 |
| ... | |
fsync()와 대체 순서는 일반적인 원자적 대체(atomic-replace) 작업 중 대상의 부분적 재작성을 방지합니다. 이는 모든 파일 시스템에 대한 저장 하드웨어 또는 전원 손실 내구성에 대한 포괄적인 보장을 정당화하지는 않습니다. |
형식 검사(format check)는 os.replace()가 커밋하기 전에 재작성된 콘텐츠를 검증합니다. 이를 원본을 대체한 후의 유효성 검사라고 설명하면 독자들에게 잘못된 실패 모델을 제공하게 됩니다.
POSIX에서는 모드 0600이 백업 접근을 소유자에게 제한합니다. Windows는 다른 의미론적(semantics) ACLs를 사용하므로, Python의 모드 인수를 동등한 Windows 보안 경계로 해석하지 마십시오.
개발자는 --force를 사용하여 recent-write 및 프로세스 게이트를 우회할 수 있습니다. 이 플래그는 격리(containment), 심링크 거부(symlink rejection) 또는 유효하지 않은 콘텐츠 실패를 우회하지 않습니다. 동시성 검사(concurrency checks)를 재정의하는 것보다 에이전트를 종료하는 것을 선호하십시오.
히스토리 스캐닝을 프리-커밋 체크포인트로 만들기
프로젝트의 훅(hook)을 .pre-commit-config.yaml에 추가하세요:
repos:
- repo: https://github.com/Ishannaik/agent-sweep
rev: v0.1.9
...
프로젝트의 README는 예제에서 이 릴리스 태그를 사용합니다. 더 새로운 고정(pin)을 선택하기 전에 릴리스 변경 사항을 검토하십시오.
훅 설치:
pre-commit install
훅 정의는 다음을 호출합니다:
agentsweep scan --all --detected
이는 pass_filenames: false와 always_run: true를 설정합니다. 따라서 개발자는 스테이징한 파일과 관계없이 커밋당 하나의 히스토리 스캔을 받게 됩니다. 감지된 히스토리 루트가 없으면, 스캔은 깨끗하게 종료됩니다.
이 훅(hook)은 디텍터가 자격 증명 형태의 값을 발견할 때 커밋을 차단합니다. 이 과정에서 아무것도 수정(redact)하지 않습니다. 개발자는 보고서를 검토하고, 실제 노출된 자격 증명을 취소하며, 별도의 수정 작업을 실행해야 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기