AI 코딩 에이전트가 시크릿 값을 노출하지 않고 환경변수를 사용하게 하는 CLI
한국어 · English
에이전트에게 API 키를 넘기면 그 값이 대화 로그에 남는다. curl -v 출력이나 에러 스택 한 줄로도 노출된다. agents-env는 값을 자식 프로세스로만 전달하고, 에이전트의 대화 기록에는 키 이름만 남긴다. 스키마나 암호화 vault, 클라우드 계정 없이 기존 .env 위에 바로 얹어 쓴다.
agents-env run TAVILY_API_KEY@work -- curl -H "Authorization: Bearer {{TAVILY_API_KEY}}" https://api/...
{{TAVILY_API_KEY}}는 명령이 실행되는 순간에만 실제 값으로 치환된다. 대화 로그에 남는 것은 {{TAVILY_API_KEY}} 문자열뿐이고, curl이 키를 출력하더라도 [masked:TAVILY_API_KEY]로 가려진 뒤에 나온다.
환경변수·시크릿 관리 도구는 많지만, 값을 AI 대화 로그에 남기지 않으면서 에이전트가 .env를 읽고 쓰게 하는 것은 드물다. 비슷한 도구 14개를 검토했을 때 아래 네 가지를 모두 갖춘 것은 없었다.
- 출력 마스킹: 주입한 시크릿이 자식의 stdout/stderr에 나타나면 실시간으로
[masked:KEY]로 치환한다.doppler run·infisical run은 주입만 하고 출력은 막지 않는다. - 값 비경유 복사:
copy는 전역 store의 시크릿을 로컬.env로 옮기되, 값이 에이전트 컨텍스트를 거치지 않는다. - 에이전트 자동 감지: Claude Code와 Codex 환경을 자동 인식해 기본적으로 값을 숨긴다. 수동 플래그가 필요 없다.
- 비대칭 쓰기 가드: 사람이 관리하는 전역 마스터
.env는 이 도구로 수정할 수 없다.
get은 키를 찾는 명령, run은 키를 쓰는 명령이다. mytool --key "$(agents-env get KEY)"처럼 명령 치환으로 값을 꺼내 쓸 수는 없다. $(...)는 값을 셸 명령줄, 즉 에이전트 컨텍스트로 가져오기 때문에, 에이전트 모드의 get은 값 대신 키 이름과 길이만 반환한다. 값은 run이 자식 프로세스로 직접 전달한다.
마스킹은 출력 스트림(자식 → 호출자)만 처리하고 프로그램이 받는 입력은 바꾸지 않는다. 따라서 프로그램은 실제 값으로 정상 동작하며, 그 값이 출력으로 되돌아올 때만 가려진다.
| 명령 | 설명 |
|---|---|
get <pattern> |
키 조회(부분 일치). 사람에게는 KEY=value, 에이전트에게는 KEY [set, N chars] # tag만 출력. |
ls [pattern] |
키 이름과 태그. 어떤 모드에서도 값은 출력하지 않음. |
run <KEY[@tag]...> -- <cmd> |
자식 env에 주입, 출력 마스킹, argv의 {{KEY}} 치환. --all은 스코프 전체 주입. |
set <KEY> <VALUE> --to <file> |
로컬 파일에 비밀이 아닌 값 기록. 크리덴셜 형태면 경고. |
copy <KEY[@tag]...> --to <file> |
전역 store의 시크릿을 로컬 파일로 복사. 값은 출력하지 않음. --as NEWKEY로 키 이름 변경. |
edit |
전역 store를 $EDITOR로 연다. 사람 전용이며 에이전트 모드·비TTY에서는 거부. |
doctor |
파일 권한, gitignore 커버리지, 오래된 백업, 태그 없는 중복 키, Claude Code deny 규칙 점검. |
전체 옵션은 agents-env --help.
스코프와 파일. 기본 스코프는 전역 store다. -l/--local은 ./.env를, -f <name>은 ./<name>을 읽어 .env.local, .env.production 등 여러 파일을 다룬다.
agents-env -f .env.production get DATABASE
중복 키. 한 키에 계정이 여럿이면 인라인 # comment가 태그가 된다. run, copy처럼 결과가 갈리는 작업은 유일 매치를 요구하며, 모호하면 후보 태그(값 제외)를 보여주고 멈춘다.
agents-env copy NOTION_API_KEY@demodev --to .env.local
set/copy는 현재 디렉토리의 .env* 파일만 쓴다. 전역 store는 구조적으로 도달할 수 없다.
- 쓰기 대상을 전역 스코프로 지정하는 플래그가 없다.
- 파일명은 bare
.env/.env.*만 허용한다. 경로 구분자를 막아../, 절대경로,.bak타겟이 차단된다. - 타겟이 심볼릭링크거나, 하드링크를 가졌거나, 전역 store와 동일 파일이면 거부한다.
- 전역 store 디렉토리 내부에서의 쓰기를 거부한다.
- git 레포 안에서는 시크릿을 쓰는
copy타겟이 추적되지 않으면서 gitignore되어 있어야 한다. 아니면 하드 에러로 막는다(override 없음,.gitignore수정 필요).
쓰기 전에는 <file>.YYMMDD.bak 백업을 만든다. 같은 날 두 번째 쓰기부터는 그날의 첫 백업을 유지한다(작업 시작 전 상태가 복구 지점이므로). 이후 O_NOFOLLOW 임시 파일에 쓰고 rename으로 교체한다. 백업도 .env로 시작하므로 .env* gitignore 한 줄로 함께 덮인다.
마스킹은 심층 방어 수단이지 샌드박스가 아니다. 자식이 출력한 시크릿 원문은 잡지만, 자식이 재인코딩한 값(base64, URL 인코딩, 분할)은 잡지 못한다. cat .env나 Claude Code의 @.env 인라인 참조는 이 도구를 우회하며, 그쪽은 하네스 deny 규칙으로 막아야 한다. doctor가 ~/.claude/settings.json의 Read(**/.env) 계열 deny 여부를 점검하므로, 두 레이어가 서로를 보완하도록 함께 설정하는 것을 권한다.
- 자동 감지는 Claude Code와 Codex만 지원한다. Cursor, Aider, Windsurf, 자체 제작 하네스에서는 에이전트 여부를 인식하지 못한다. 이 경우
get이 값을 출력하고--no-mask도 허용된다(run의 출력 마스킹 자체는 모든 환경에서 동작한다). 해당 도구에서는 그 환경 셸 설정에AGENTS_ENV_AGENT_MODE=1을 넣거나 config에markers=...를 추가해 직접 활성화한다. - 감지는 신호이지 차단막이 아니다. 모드는 env 마커로 판단하므로, 에이전트가 마커를 제거하거나(
env -u CLAUDECODE ...)--no-mask를 써서 사람 모드를 강제할 수 있다. 대상 위협은 실수로 시크릿을 로깅하면 안 되는 정직한 에이전트다. 악의적 에이전트는~/.dotfiles/.env를 직접 읽을 수 있으며, 그것은 이 도구가 아니라 deny 규칙이 막을 영역이다. {{KEY}}는 같은 사용자의ps에 노출된다. 값이 자식 argv에 들어가 같은 사용자의 다른 프로세스가 읽을 수 있다. 공유 머신에서 민감한 값은{{KEY}}대신 env 주입을 사용한다.- 부모 디렉토리 스왑 TOCTOU. 가드가 cwd를 canonicalize하고 임시 파일에
O_NOFOLLOW를 쓰지만, 같은 사용자 권한의 공격자가 쓰기 도중 부모 디렉토리를 rename하면 우회할 수 있다. 완전한 차단은 디렉토리 fd(openat/renameat) 방식이 필요하며 다음 버전에서 다룬다. 로컬 동일 사용자 쓰기 권한 없이는 도달할 수 없고, 그 권한이 있다면 시크릿은 이미 노출된 상태다. - 줄 끝은 LF로 정규화된다. 라운드트립은 주석·순서·간격을 보존하지만 CRLF 파일은 LF로 다시 쓰이고 끝에 개행이 추가된다.
설치와 설정은 함께 배포되는 에이전트 스킬에 포함되어 있어, Claude Code나 Codex에 맡길 수 있다. 수동 설치는 다음과 같다.
cargo install agents-env
# crates.io 등록 전이면:
cargo install --git https://github.com/ai-native-engineer/agents-env
MIT