2026.10.17 토
Heldkeys: 에이전트의 자격 증명을 찾을 때 값은 숨기기
오늘의 픽: Heldkeys
Heldkeys 는 코딩 에이전트와 같은 셸에서 실행되어, 에이전트가 읽을 수 있는 자격 증명을 찾아 목록으로 보여 주는 프로덕트다. 환경 변수, 클라우드 CLI 로그인, 레지스트리 토큰, .env 파일을 읽어 심각도와 수명으로 분류하고, 값은 앞 4자와 뒤 3자만 보여 준다.
오늘의 픽으로 선정한 이유는 시크릿을 출력하지 않는다는 약속을 가짜 시크릿 28개로 직접 확인할 수 있어서다.
- 출처
- Show HN
- 만든 방식
- Rust, clap, regex, serde, cargo-dist
무엇이 문제인가
코딩 에이전트개발자 대신 코드를 읽고 고치고 명령을 실행하는 AI 프로그램. Claude Code, Codex 등이 있다.는 개발자의 계정으로 실행된다. 그 셸에는 개발자가 쌓아 둔 로그인이 그대로 있다. ~/.aws/credentials 의 액세스 키, ~/.npmrc 의 배포 토큰, GitHub CLI 의 hosts.yml, 프로젝트의 .env 파일이다. 에이전트가 읽은 이슈에 프롬프트 인젝션이 숨어 있어 셸 명령이 한 번 실행되면, 그 명령은 개발자가 읽을 수 있는 자격 증명을 전부 읽을 수 있다.
시크릿을 찾는 도구로는 GitHub 의 시크릿 스캐닝(secret scanning)이 널리 쓰인다. GitHub 공식 문서(docs.github.com 의 Secret scanning 개념 페이지)에 따르면, 시크릿 스캐닝은 저장소의 모든 브랜치와 전체 Git 기록을 스캔해 API 키, 비밀번호, 토큰 같은 하드코딩된 자격 증명을 찾고, 푸시 보호(push protection)는 시크릿이 든 푸시를 막는다. 저장소나 조직 설정에서 유효성 검사(validity checks)를 활성화하면, 토큰을 발급한 서비스에 폐기 여부를 확인한다. 범위는 저장소에 들어온 내용이다. 개발자의 홈 디렉토리와 셸 환경 변수의 로그인은 커밋되지 않으므로 어느 브랜치에도 없고, 그래서 시크릿 스캐닝의 범위 밖이다. 그런데 에이전트는 이 환경에서 실행된다.
어떻게 해결했나
Hugo Nogueira 가 만든 Heldkeys 는 검사 위치를 저장소에서 에이전트의 셸로 옮긴다. 같은 셸에서 실행되어, 에이전트가 읽을 수 있는 자격 증명을 찾는다. 환경 변수는 이름(*_TOKEN)과 값의 형식(ghp_, AKIA)으로 판별한다. 홈 디렉토리에서는 AWS, gcloud, Azure, kube 의 CLI 로그인, Git 과 GitHub 의 로그인, npm, PyPI, cargo, Docker 의 레지스트리 토큰, 패스프레이즈가 없는 SSH 개인 키, Cursor 와 Claude Code 같은 에이전트 도구의 설정과 로그인을 읽는다. 발견마다 규칙 번호, 심각도, 수명(장기·단기·불명), 접근 권한(쓰기·읽기·불명), 고치는 법이 붙는다.
명령은 세 개다. scan 은 노트북이나 샌드박스의 현재 상태를 검사한다. ci 는 CI지속적 통합(continuous integration). 코드를 올릴 때마다 빌드와 테스트를 자동으로 실행하는 체계다. 작업의 런타임 노출에 더해 워크플로 파일의 에이전트 단계를 정적으로 분석한다. 에이전트 단계에 필요 없는 시크릿이 넘어가는지, 풀 리퀘스트한 브랜치의 변경을 다른 브랜치에 병합해 달라는 요청. 다른 개발자가 검토하고 승인한다.의 코드를 체크아웃한 채 실행되는지, 토큰 권한이 넓은지, 액션이 커밋 해시에 고정됐는지를 검사한다. preflight 는 컨테이너 엔트리포인트에서 PASS 나 FAIL 한 줄만 출력하고 종료 코드로 실행을 막는다. 저장소에는 GitHub Action 과 Claude Code 용 스킬이 함께 들어 있어, 에이전트가 스스로 이 검사를 실행할 수 있다.
무엇이 다른가
첫째, 검사하는 위치가 다르다. GitHub 의 시크릿 스캐닝은 저장소에 푸시된 기록을 중앙에서 계속 검사한다. Heldkeys 는 실행된 기계의 홈 디렉토리와 환경 변수, 마운트된 토큰을 검사한다. 에이전트가 읽을 수 있는 파일을 똑같이 읽어야 하므로 기계마다, 샌드박스마다 따로 실행해야 하고, 정해진 위치만 검사한다. 오늘 ~/.config/myapp/token.toml 에 심은 GitHub 토큰은 검사 26개 어디에도 해당하지 않아 목록에 오르지 않았다. 저장소에 들어온 시크릿을 빠짐없이 찾는 일은 GitHub 의 시크릿 스캐닝이 더 잘한다.
둘째, 판단 기준이 다르다. 시크릿 스캐닝은 패턴에 일치하는 값을 찾아 알리고, 유효성 검사로 발급한 서비스에 확인한다. Heldkeys 는 값의 종류에 따라 수명과 접근 권한을 판정한다. AWS SSO 의 캐시된 세션은 단기라 Medium, 정적 액세스 키는 장기라 High 다. 대신 발급한 서비스에 확인하지 않으므로 접근 권한은 자주 불명으로 남는다. 오늘 환경 변수의 GitHub 토큰은 "long-lived, unknown access" 로 표시됐다.
셋째, 출력 원칙이 다르다. Heldkeys 의 출력은 에이전트의 대화 창과 CI 로그에 그대로 들어간다. 그래서 값을 절대 출력하지 않고, 접두사가 공개된 토큰 형식에만 값의 앞 4자와 뒤 3자로 된 핑거프린트(AKIA…PLE)를 표시한다. 트레이드오프는 식별력이다. 같은 형식의 키가 여럿이면 위치로 구분해야 하고, 비밀번호처럼 형태를 모르는 값은 핑거프린트도 표시되지 않는다.
어떻게 만들었나
- 커밋 6개가 전부 2026-10-16 하루에 쌓였고 제작자는 Hugo Nogueira 혼자다. 0.1.1 에서 0.2.3 까지 버전 다섯 개가 그날 나왔다. AI 로 만들었다는 표기는 커밋과 문서 어디에도 없다.
- Rust 25,121줄, 검사 모듈 26개, 규칙 94개, 테스트 함수 450개다. 의존성은 clap, owo-colors, regex, serde, serde_json, toml, yaml-rust2 일곱 개와 유닉스의 libc 뿐이다.
- 시크릿이 출력에 노출되지 않는지는 테스트로 검증한다. 모든 픽스처를 모든 출력 형식과 stdout, stderr, --output 파일에 통과시키고, 시크릿 값의 어느 위치에서든 연속된 10자를 잘라 낸 조각(10자 조각)이 출력에 하나라도 나오면 실패한다. 값의 일부만 노출돼도 10자 이상이면 잡아내는 방식이다. 픽스처의 가짜 토큰은
{{fake:prefix|rest}}표식으로 커밋돼 gitleaks 가 저장소에서는 아무것도 보고하지 않는다. - 배포는 cargo-dist 로 리눅스용 정적 musl 바이너리와 macOS 바이너리를 만들고 SHA-256 과 빌드 출처 증명을 붙인다. 자신의 CI 에서 heldkeys ci, zizmor, gitleaks 를 실행한다.
실행해 보니
빌드한 뒤 가짜 홈 디렉토리를 만들어 아래 순서로 시험했다.
- 빌드. cargo build --release 가 1분 10초에 끝났고 실행 파일은 4.0 MB 다.
- 가짜 홈. 저장소의 tests/fixtures/full 에 든 파일 20개를 복사하고 demo.tape 에 적힌 perl 한 줄로
{{fake:…}}표식을 완성된 가짜 토큰으로 바꿨다. .env 두 개, 패스프레이즈가 없는 가짜 OpenSSH 키, 워크플로 파일 3개를 더해 26개 파일이 됐다. 환경은 env -i 로 비우고 가짜 토큰 두 개만 넣었다. - 요약 스캔. 자격 증명 32개, 그중 장기·쓰기 권한 11개. 실제 경과 시간은 10 ms 였다(3회: 10, 12, 10 ms). 위치별 여덟 범주로 분류됐고, 환경 변수의 GitHub 토큰과 Cursor 의 mcp.json 에 든 같은 토큰은 "같은 시크릿이 두 곳"으로 묶어 보고했다.
- 게이트. --fail-on long-lived-write --quiet 는 한 줄만 출력하고 종료 코드 1 을 반환했다. preflight 도 같았다.
- ci. 워크플로 3개(데모용, 취약 예제, 하드닝 예제)에서 위험 11건(High 8, Medium 3)이 나왔다. 에이전트 단계에 넘어간 불필요한 시크릿, 풀 리퀘스트 코드의 체크아웃, 쓰기 권한의 셸, 넓은 토큰 권한, 해시에 고정되지 않은 액션이다. 하드닝 예제만 남기고 실행하니 0건에 종료 코드 0 이었다.
- 유출 검사. 같은 방식으로 심은 시크릿 값 28개에서 10자 조각 463개를 만들어 출력 파일 15개를 검색했다. 출력에서 발견된 조각은 0개였다. 데이터베이스 비밀번호와 Docker 비밀번호에는 핑거프린트도 표시되지 않았다.
- 읽기 전용과 네트워크. 파일 26개의 SHA-256 이 스캔 전후로 같았다. strace 로 확인하니 openat 42번 가운데 쓰기 플래그는 0번이었고, --output 은 지정한 파일만 만들었다. HELDKEYS_NO_NET 을 빼고 네트워크 호출을 추적하니 TCP 연결은 169.254.169.254:80 으로의 한 번뿐이었다.
- 더 심기. 이름이 낯선 환경 변수에 넣은 ghp_ 토큰과 AKIA 키는 값의 형식으로 탐지됐고, 평범한 비밀번호 값은 핑거프린트 없이 "Possible credential" 로 표시됐다. ~/.claude/.credentials.json 의 가짜 OAuth 토큰은 High 였다.
$ heldkeys
heldkeys: 32 credentials reachable from ~/code/app (11 long-lived with write access)
Fix these first
HIGH Anthropic API key env: ANTHROPIC_API_KEY sk-a…AKE
HIGH GitHub OAuth token ~/.config/gh/hosts.yml (github.com) gho_…013
HIGH Plaintext git password ~/.git-credentials (git.example.com)
HIGH Long-lived AWS access key ~/.aws/credentials [default] AKIA…PLE
+27 more credentials, see --details
Where they are high medium low
Shell environment 2 - -
Cloud CLIs (AWS, gcloud, Azure, kube) 7 5 -
Git and GitHub 4 3 -
Package registries (npm, PyPI, cargo, Docker) 4 1 -
SSH 1 - -
MCP and agent configs 1 - -
Sandbox and host 1 - -
Project .env files 2 - 1
32 credentials reachable, 11 long-lived with write access.
If a prompt injection can run one shell command here, it can use all of them.
확인하지 못한 것
- 실제 홈 디렉토리. 규칙대로 가짜 홈에서만 실행했으므로 실제 기계에서의 오탐과 미탐은 알 수 없다.
- scan --docker 와 --kube, ssh-agent 검사, macOS 경로. Docker 데몬과 ssh-agent 가 실행 중인 기계와 macOS 가 필요하다.
- 테스트 450개, GitHub Action, SARIF 출력, 베이스라인(--write-baseline)과 .heldkeys.toml 의 예외 처리. 다음에 베이스라인을 만든 뒤 시크릿을 하나 더 심어, 새 발견으로 보고되는지 확인하자.
판정
작동한다. 자격 증명 32개를 10 ms 만에 분류했고, 시크릿 값은 노출하지 않았다. 다만 정해진 위치만 검사하므로 낯선 경로의 토큰은 찾지 못한다. 코딩 에이전트를 자신의 계정으로 실행하기 전에 그 셸에서 무엇이 읽히는지 알고 싶고, 같은 검사를 CI 와 컨테이너 엔트리포인트의 게이트로도 사용하고 싶은 경우에 써 볼 만하다. 저장소에 이미 커밋된 시크릿을 찾는 일이라면 이 프로덕트는 적합하지 않다. README 의 비교표에도 그 일은 TruffleHog 와 gitleaks 의 몫이라고 적혀 있다.
얻어갈 것
- 시크릿을 다루는 도구는 출력 전부를 자동 검사에 넣자. 오늘 Heldkeys 의 자체 테스트와 같은 방식으로 직접 검사하니, 10자 조각 463개 가운데 출력에서 발견된 조각은 0개였다.
- 테스트 픽스처의 가짜 토큰은 완성형으로 커밋하지 말자. 0.2.3 은
{{fake:…}}표식으로 바꿔 gitleaks 가 저장소에서 아무것도 보고하지 않게 했다. - 읽기 전용이라는 말은 해시와 strace 로 확인하면 된다. 오늘 파일 26개의 해시가 전후 동일했고 openat 42번 가운데 쓰기 플래그는 0번이었다.
- 에이전트에게 도구를 쥐여 줄 때는 금지 규칙을 스킬에 같이 작성하자. 동봉된 스킬 문서에는 발견을 확인하려고 cat 이나 env 를 실행하지 말라는 규칙이 적혀 있다.
시작하기
git clone https://github.com/hugomn/heldkeys && cd heldkeyscargo build --releasetarget/release/heldkeystarget/release/heldkeys --detailstarget/release/heldkeys ci
- 필요한 것
- Rust 1.88 이상 (Cargo.toml 의 rust-version) · 소스 빌드 없이 쓰려면 릴리스의 정적 musl 바이너리. SHA-256 과 빌드 출처 증명이 함께 있다
- 비용
- MIT, 무료. 가격 페이지는 없다.
실행하는 셸의 환경 변수와 홈 디렉토리를 읽으므로, 오늘은 가짜 홈을 --home 과 --root 로 지정하고 env -i 로 비운 환경에서 실행했다. HELDKEYS_NO_NET=1 이면 클라우드 메타데이터 주소로의 TCP 연결도 하지 않는다.
실행해서 확인한 절차다.
진행
- 3일활발
- 7일조용함
- 15일예정
- 30일예정
- 90일예정
- 180일예정
- 365일예정
최근 확인 7일, 2026.10.23 금
- 런칭 이후 커밋
- 5 → 15
- 스타
- 4 → 6
- 마지막 커밋
- 2026.10.17
이 기간에 커밋이 없다.