Claude Code 비밀값 노출 원인과 로컬 설정 파일 점검법

Claude Code CLI에서 Bash 명령을 다시 묻지 않기로 승인하면 허용 규칙이 프로젝트의 .claude/settings.local.json에 문자열로 저장된다. 규칙에 토큰이나 비밀번호가 평문으로 들어 있으면 값도 함께 남고, 이 파일을 공개 저장소에 커밋해 푸시하면 값도 공개된다. 공식 문서와 Apiiro 보고서는 2026-11-09 KST에 확인했고, Git 동작은 더미 파일로 직접 실행해 봤다.

영구 승인이 모든 명령을 덤프하지는 않지만, 비밀값이 든 정확한 명령을 승인하고 이 파일이 Git에 커밋되면 평문이 저장소에 남는다.

Claude Code 비밀값 노출은 승인 규칙에서 어떻게 시작될까

다시 묻지 않기를 고른 승인은 어디에 남을까.

Anthropic의 Configure permissions 공식 문서는 Bash 영구 승인이 저장소와 명령별로 지속된다고 설명한다. CLI 기준 저장 위치는 .claude/settings.local.json이고, 규칙 형식은 Tool 또는 Tool(specifier)다.

Bash 규칙에 *가 없으면 정확한 명령 전체를 허용한다. *가 있으면 그 자리에 어떤 텍스트가 와도 허용한다.

그래서 아무 명령이나 한 번 승인하면 비밀값이 저장된다는 말은 과장이다. 반대로 헤더나 환경변수 할당에 실제 토큰을 넣은 명령을 정확한 규칙으로 승인하면, 그 문자열이 permissions.allow에 평문으로 들어간다고 본다.

비밀값이 든 명령, 지속 승인, 로컬 설정의 허용 규칙, Git 커밋·푸시의 네 단계를 화살표로 이은 다이어그램. 공개 저장소에 푸시될 때만 공개된다고 적었다.

비밀값이 든 명령은 지속 승인을 거쳐 로컬 설정의 허용 규칙이 된다. 공개 저장소까지 이어지는 것은 Git 커밋과 공개 저장소 푸시가 겹칠 때뿐이다.

공개 저장소에서는 얼마나 발견됐을까

Apiiro의 보고서 Coding Agents Are Leaking Your Secrets to GitHub는 2026-11-01부터 03일까지 공개 GitHub 기본 브랜치를 읽기 전용으로 검색한 결과다. 아래 수치는 모두 Apiiro의 분류 결과이며, 이번에 다시 조사하지 않았다.

공개 .claude/settings.local.json 104,185개 가운데 1,122개(약 1.1%)가 비밀정보를 포함한다고 분류됐다. 1,122는 유효성이 확인된 키의 개수가 아니라 파일 수다.

이 파일들에 든 서로 다른 비밀값은 1,379개다. 커밋 이력, 공유 설정, 대화 기록까지 범위를 넓히면 1,994개 저장소에서 2,539개가 나왔다.

Apiiro는 인증정보를 시험하지 않았고 호스트에 접속하지도 않았다. 그래서 형식과 문맥만으로 분류한 수치로 읽어야 한다.

이 조사에서는 검증 기능을 끈 gitleaks와 TruffleHog가 1,122개 중 각각 564개와 643개를 잡았다. 215개는 두 도구 모두 탐지하지 못했다.

Hacker News에서는 Claude Code 책임자 Boris Cherny가 2026-11-08 KST 댓글로 답했다. 과거 Claude Code가 secret scrubbing을 하지 않았으며 방지책을 검토 중이라는 내용이다.

기본 비밀정보 검사 등 몇 가지 방안은 배포 전에 시제품으로 시험해 보겠다고 했다. 확정된 패치 버전은 확인하지 못했다.

자동 Git 제외가 있는데 왜 올라갈까

Anthropic의 Configure Claude Code 공식 문서는 설정 파일 세 개의 용도를 나눠 설명한다.

파일공식 문서상 용도Git에서 볼 점
~/.claude/settings.json여러 프로젝트에 적용되는 개인 설정홈 경로에 있어 저장소 파일이 아니다
.claude/settings.json팀과 공유하고 커밋하는 프로젝트 설정추적은 정상이지만 비밀값은 넣지 않는다. Apiiro 분류 298개 파일
.claude/settings.local.json개인의 프로젝트별 설정, CLI 영구 Bash 승인 저장Git에 넣지 않을 파일이지만 추적되면 커밋된다. Apiiro 분류 1,122개 파일

문서에 따르면 Claude가 로컬 설정을 처음 쓸 때 이 경로를 제외하는 패턴을 추가한다. 추가되는 곳은 저장소의 .gitignore가 아니다. core.excludesFile에 지정된 파일이나 ~/.config/git/ignore 같은 사용자의 전역 Git 제외 파일이다.

파일을 손으로 만들었고 Claude가 아직 쓰지 않았다면 이 보호를 받지 못한다. 전역 제외가 없는 다른 환경으로 저장소를 옮겨도 마찬가지다.

Git의 gitignore 공식 문서는 제외 규칙이 이미 추적 중인 파일에는 효과가 없다고 설명한다. .gitignore에 적혀 있어도 git add -f로 인덱스에 올린 파일은 계속 추적된다.

이 동작을 Git 2.53.0 격리 저장소에서 확인했다. 더미 Bearer 토큰 규칙, PGPASSWORD=DUMMY_DB_PASSWORD 규칙, $DEMO_TOKEN 참조 규칙을 직접 JSON으로 작성했다.

전역 제외를 비운 새 저장소에서 git -c core.excludesFile=/dev/null status --short --untracked-files=all을 실행하자 ?? .claude/settings.local.json이 그대로 나왔다. 파일 이름만으로는 자동 제외되지 않는다.

이어서 .gitignore에 .claude/settings.local.json 한 줄을 적고 아래 명령을 차례로 실행했다.

git -c core.excludesFile=/dev/null check-ignore -v .claude/settings.local.json
git -c core.excludesFile=/dev/null add -f .claude/settings.local.json
git ls-files --error-unmatch .claude/settings.local.json
git -c core.excludesFile=/dev/null check-ignore -v .claude/settings.local.json
git -c core.excludesFile=/dev/null check-ignore --no-index -v .claude/settings.local.json

첫 check-ignore는 .gitignore 규칙을 출력하며 종료코드 0을 냈다. add -f 다음에 실행한 ls-files는 파일 경로를 출력했다.

이 상태에서 일반 check-ignore는 아무것도 출력하지 않고 종료코드 1을 냈다. 인덱스를 무시하는 --no-index는 다시 규칙을 출력하며 0을 냈다. 일반 검사의 빈 출력만으로 추적 여부를 판단하면 안 된다.

값 출력 없이 내 저장소를 점검하는 절차

cat이나 git diff로 설정을 바로 열면 화면 공유나 로그에 키가 다시 찍힐 수 있다. 그래서 내용 대신 인덱스 상태와 패턴 건수부터 확인한다.

git ls-files -- .claude/settings.json .claude/settings.local.json

# ignore 패턴 적용 여부만 확인. 0=일치, 1=미일치, 그 외=오류 확인 필요
git check-ignore --no-index -q -- .claude/settings.local.json
printf 'ignore_check_exit=%s\n' "$?"

평문 후보는 값을 화면에 내지 않고 건수만 센다. Bearer 문자열, 환경변수 평문 할당, URL의 사용자 정보, --password 뒤의 값이 대상이고 $DB_URL 단독 참조는 세지 않지만 URL 안의 변수는 셀 수 있다.

grep -Eo 'Bearer [A-Za-z0-9._-]{8,}|[A-Z_]+=[^$" ]{8,}|://[^/@" ]+:[^/@" ]+@|--password (\\")?[^$"\\ ]+' .claude/settings.local.json | wc -l

앞의 더미 JSON에서 이 명령은 2를 출력했다. 변수 참조만 있는 더미 파일에서는 0이었다.

  1. 프로젝트 루트에서 git ls-files를 실행해 .claude/settings.local.json이 추적 목록에 나오는지 본다.
  2. 위 명령으로 로컬 설정에 평문 후보가 있는지 확인한다.
  3. 저장소 .gitignore에 .claude/settings.local.json을 적는다. 그래야 다른 환경에도 제외 규칙이 전달된다. 이미 추적 중이면 git rm --cached .claude/settings.local.json으로 추적을 해제한다.
  4. 승인 프롬프트가 뜨면 규칙 문자열을 읽고, 실제 값 대신 $DB_URL 같은 변수 참조가 남게 한다. 평문이 이미 커밋되거나 푸시됐다면 줄만 지우지 말고 해당 키를 회전하거나 폐기한다.

이번 검증의 한계

직접 확인한 것은 손으로 만든 JSON과 Git 명령의 동작까지다. 실제 Claude 승인 화면이 어떤 규칙을 저장하는지는 재현하지 않았다.

이 건수 명령은 로컬 설정 파일 하나만 보는 휴리스틱이다. mysql -p비밀번호 형태, 인코딩된 값, env 객체 속 일반 비밀번호 값은 잡지 못한다. 중첩 폴더, worktree, 과거 커밋은 검사하지 않으므로 0건이 안전을 뜻하지는 않는다.

시크릿 파일을 분리하는 기본 원칙은 Claude Code 보안 — API키를 안전하게 관리하는 법에서 다뤘다. 설정에 적힌 규칙과 실제 적용 범위의 차이는 Claude Code 훅이 안 막는 이유 — 로컬 적용 범위와 deny 점검에서 이어진다.

결론

Claude Code CLI의 영구 Bash 승인은 규칙 문자열을 .claude/settings.local.json에 남긴다. 비밀값이 든 명령을 그대로 승인하고 이 파일이 Git에 커밋되면 평문이 저장소에 남는다. 판단 기준은 전역 제외가 있느냐가 아니라 내 저장소 인덱스에 이 파일이 있는지, 규칙에 평문 대신 변수 참조가 쓰였는지다. 지금 git ls-files -- .claude/settings.json .claude/settings.local.json부터 실행해 보자.

댓글

이 블로그의 인기 게시물

AI 코딩 스킬, 많을수록 좋을까 — 자산이 부채로 바뀌는 손익분기

M4 Max 128GB 로컬 LLM 4종 속도 비교 — 실측 기록과 한계

Opus 5는 더 잘 쓰고, 더 AI답게 쓴다 — 세 모델 첫날 실측