프롬프트에 적지 않고 코딩 에이전트에게 시크릿을 건네는 방법
에이전트가 명령을 실행하려면 Stripe 키나 데이터베이스 비밀번호가 필요합니다. 그걸 채팅에 붙여넣는 순간 디스크의 트랜스크립트에, 모델 제공업체에, 때로는 커밋에까지 남습니다. 우리가 직접 세 번 틀린 뒤에 도달한 규칙은 이것입니다. 시크릿이 에이전트에 닿는 통로는 환경 변수 하나뿐이다.
상황은 늘 똑같습니다. 에이전트가 작업을 반쯤 하다가 테스트용 Stripe API를 호출해야 하거나, 스테이징 데이터베이스에 마이그레이션을 돌려야 하거나, 비공개 레지스트리에 푸시해야 합니다. 에이전트가 키를 달라고 합니다. 키는 비밀번호 관리자 안에 있고, 에이전트는 기다리고 있고, 세상에서 가장 빠른 방법은 그걸 채팅에 붙여넣는 것입니다.
그 붙여넣기가 키가 당신의 통제를 벗어나는 순간입니다. 모델이 악의적이어서가 아니라, 프롬프트가 저장되고 전달되는 물건이기 때문입니다. 디스크의 트랜스크립트 파일로 들어가고, 모델 제공업체로 가고, 대화를 물려받는 모든 재시도와 모든 서브에이전트가 그것을 복사합니다.
우리는 코딩 에이전트를 돌리는 데스크톱 앱 AgentsRoom을 만듭니다. 그래서 이 문제를 우리 손으로 풀어야 했습니다. 2026년 9월에 세 번 틀렸습니다. 이 글은 그 끝에 도달한 규칙과, 그 규칙을 만들어 낸 세 가지 실수, 그리고 우리 앱을 쓰든 안 쓰든 그 규칙을 적용하는 방법입니다.
짧은 답
- 시크릿이 에이전트에 닿는 통로는 환경 변수이고, 그 외에는 없습니다. 메시지도, 티켓도, 스킬도, 명령줄 옵션도 아닙니다.
- 에이전트는 값이 아니라
$NAME을 씁니다. 명령이 실행될 때 셸이 그것을 펼칩니다. - 값은 저장소 바깥에 한 번만 저장하세요. OS 키체인으로 암호화하고, 다른 모든 곳에서는 이름으로 참조합니다.
- 대화를 한 번 거친 시크릿은 타 버린 것입니다. 교체하고, 그다음에 정리하세요.
- 마스킹은 안전망이지 계획이 아닙니다. 키처럼 생긴 것은 잡아내지만, 단어처럼 읽히는 데이터베이스 비밀번호는 잡지 못합니다.
그 붙여넣기는 보기보다 나쁩니다
세 가지 일이 동시에 벌어지는데, 눈에 보이는 건 첫 번째뿐입니다.
모델 제공업체로 갑니다. 누구나 아는 부분입니다. 당신의 키는 이제 남의 인프라 위 요청 본문 안에, 그것도 당신이 수정할 수 없는 대화 안에 있습니다.
디스크에 평문으로 남습니다. Claude Code, Codex, Antigravity CLI를 비롯한 CLI들은 대화를 나중에 이어 갈 수 있도록 트랜스크립트로 남겨 둡니다. 그 파일은 당신의 홈 폴더에, 암호화되지 않은 채로, CLI가 기록을 보관하는 내내 놓여 있습니다. 한 번만 자신의 트랜스크립트에서 sk- 를 검색해 보고 무엇이 나오는지 확인해 보세요.
옆으로 번집니다. 대화는 에이전트의 기억이므로, 대화를 복사하는 것은 무엇이든 키를 함께 복사합니다. 이어서 연 세션, 포크한 에이전트, 부모의 컨텍스트를 들고 띄운 서브에이전트, 프로젝트 파일에 적힌 요약. 우리는 키가 채팅 메시지에서 티켓 설명으로, 거기서 두 번째 에이전트의 첫 메시지로 옮겨 가는 것을 봤습니다. 아무도 잘못한 사람이 없는데도 말이죠.
그리고 평소의 안전망은 버티지 못합니다. 출력 마스킹은 패턴으로 동작합니다. sk-…, glpat-…, JWT 모양. thursday-harbour-71 이 당신의 스테이징 데이터베이스 비밀번호라는 것은 알 길이 없습니다. 우리 앱에서도 일치하는 값은 터미널 출력에서 가리지만, 그래도 그것은 공중그네 밑에 친 그물이지 공중그네 자체는 아니라고 봅니다.
문은 하나라는 규칙
규칙은 한 줄입니다. 자격 증명이 에이전트에 닿는 통로는 에이전트가 돌아가는 프로세스의 환경 변수이고, 다른 어떤 통로도 아니다.
나머지는 전부 글자 그대로의 텍스트입니다. 당신이 치는 메시지, 티켓 설명, 스킬 파일, 시스템 프롬프트, 명령줄 옵션. 이 모두는 사람이나 모델이 읽는 것이므로, 거기에 적힌 시크릿은 공개된 시크릿입니다.
명령줄은 따로 한 문장을 받을 자격이 있습니다. 채팅 메시지보다 안전해 보이지만 실제로는 그렇지 않기 때문입니다. --api-key=… 안의 값은 ps 를 통해 머신의 모든 프로세스에 보이고, 셸 기록에 남고, 실행된 명령을 기록하는 모든 것에 로그로 남고, 프로세스 줄을 그대로 다시 보여 주는 진단 도구에도 나타납니다. 프로세스 하나의 환경 변수는 그 프로세스와 root만 읽을 수 있습니다. 노출 면은 더 작고, 편의성은 똑같습니다.
한편 에이전트는 값 자체가 필요했던 적이 없습니다. 필요한 건 이렇게 쓰는 것뿐입니다.
curl -H "Authorization: Bearer $STRIPE_SECRET_KEY" https://api.stripe.com/v1/charges
셸이 실행 시점에 $STRIPE_SECRET_KEY 를 펼칩니다. 에이전트가 쓴 것은 17자와 달러 기호 하나입니다. 그 트랜스크립트에 남는 것도 17자와 달러 기호 하나입니다.
우리 앱에서는 어떻게 동작하나
메커니즘 자체보다 형태가 중요하지만, 구현을 하나 볼 수 있으면 그 형태를 따라 하기가 쉬워집니다.
보관소 하나, 파일 하나. AgentsRoom은 시크릿을 홈 폴더의 파일 하나, 권한 0600에 보관하고, 운영체제 키체인으로 암호화하며, 쓸 때마다 그것을 다시 확인합니다. 서버로 동기화되는 것은 없고, 다른 컴퓨터로 넘어가는 것도 없습니다. 시크릿은 그것을 가진 머신의 것입니다. 키체인을 쓸 수 없으면, 평문으로 물러나는 대신 저장이 실패합니다.
값은 한 번만 저장합니다. 명령 및 연결 > 시크릿, 어느 프로젝트에서든 열 수 있습니다. 이름(영문자, 숫자, 점, 하이픈, 밑줄, 64자까지라서 그대로 환경 변수 이름으로 쓸 수 있습니다)과, 비밀번호 입력란에 치는 값. 새로 뜨는 줄에는 이름과 그 참조 {{secret:STRIPE_SECRET_KEY}}, 그리고 참조를 복사하는 버튼이 보입니다. 값을 보여 주는 버튼도, 값을 복사하는 버튼도 없습니다. 값을 인터페이스로 돌려주는 코드 경로가 아예 없기 때문입니다.
참조는 환경이 만들어지는 자리에서 씁니다. 에이전트 편집 > 고급 > 환경 변수, 또는 프로젝트 설정, 또는 제공업체 설정, 또는 개발용 명령줄입니다.
STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}}
실행 시점에 메인 프로세스가 이제 막 만들려는 터미널의 환경에 값을 채워 넣습니다. 렌더러도 보지 못하고, 에이전트도 보지 못하고, 휴대폰도 보지 못합니다. 화면 순서는 시크릿 관리자 페이지에 정리해 두었습니다.
에이전트가 알아내는 것은 이름이지 값이 아닙니다. 우리 MCP 도구에 연결된 에이전트는 secrets_list 를 호출해 그 머신에 있는 이름 목록을 받을 수 있습니다. 쓸모 있으려면 그 정도면 충분합니다. 이렇게 말할 수 있으니까요. "STRIPE_SECRET_KEY 가 저장되어 있네요. 이 에이전트의 환경에 STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}} 를 추가해 주시면 제가 $STRIPE_SECRET_KEY 를 쓰겠습니다." 값은 읽을 수 없습니다. 그 값을 쓰는 명령을 실행하는 중에도 마찬가지입니다.
우리가 내보낸 세 가지 실수
위 규칙은 적어 놓고 보면 뻔해 보입니다. 이 규칙은 버그 세 개를 겪고 남은 자국입니다. 모두 2026년 9월, 모두 사용자가 신고한 것입니다.
티켓에 적어 둔 참조가 치환된 채로 나왔다
9월 19일. Robert는 티켓 설명에 매핑을 적어 두었습니다. 티켓을 집어 가는 사람이 무엇을 설정해야 하는지 알 수 있도록, 지극히 합리적인 STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}} 였습니다. API로 티켓을 읽으면 참조는 그대로 돌아왔습니다. 그런데 그 티켓에서 띄운 에이전트는 첫 메시지에서 값을 받았습니다.
원인은 배관의 사소한 부분이었지만 결과는 진짜였습니다. 티켓 브리프는 에이전트의 첫 프롬프트가 되는데, 그 프롬프트는 실행 명령의 환경 변수 안에 실려 다녔습니다. 우리 리졸버는 환경의 모든 값을 치환했으므로, 프롬프트도 함께 치환했습니다. 키는 디스크로 갔고 모델 제공업체로도 갔습니다. 키 교체 말고는 길이 없었습니다.
수정은 에이전트의 실행 명령을 글자 그대로 다루게 만드는 것이었습니다. 환경에 에이전트 id가 실려 있으면, 첫 프롬프트를 담은 변수는 건드리지 않습니다. 같은 변수를 쓰는 개발용 명령에는 에이전트 id가 없으므로 전과 똑같이 치환됩니다. 부작용 하나는 일부러 받아들였습니다. 에이전트의 CLI 옵션에 적은 참조도 더는 치환되지 않는데, 명령줄은 어차피 ps 와 로그에 남으니 그게 맞습니다.
교훈. 문은 일부러 좁아야 합니다. "쓸모 있을 만한 곳에서는 전부 치환하자"는, 시크릿이 결코 있어서는 안 될 곳에 가 닿는 바로 그 경로입니다.
메시지에 붙여넣은 참조는 끝내 치환되지 않는다
9월 22일. Cs040618은 정반대의 문제를 신고했습니다. "참조를 대화에 붙여넣는데 에이전트가 이 키가 뭔지 이해하지 못합니다."
버그가 아닙니다. 메시지는 설계상 글자 그대로의 텍스트이고, 그것이 바로 사흘 전의 수정입니다. 하지만 그 동작을 하는 순간에 앱은 아무 말도 하지 않았고, 메시지로 {{secret:…}} 를 받은 에이전트도 무엇이라 답해야 할지 몰랐습니다.
수정은 셋, 모두 문이 어디 있는지 알려 주는 일이었습니다. 입력창에 참조를 붙여넣을 때 떠서 환경 변수를 가리키는 힌트, 시크릿 목록 위에 두어 참조가 치환되는 세 개의 입력란 이름을 밝히고 "메시지 안에서는 절대 아니다"라고 말하는 도움말, 그리고 참조가 메시지로 도착했을 때 무엇이라 답할지 적어 둔 에이전트 자체 지침의 한 줄입니다.
‘설정 > 시크릿’은 존재하지 않았다
9월 28일. Mohammad의 에이전트가 보고를 올렸습니다. 사용자가 설정 > 시크릿에서 OTPIQ_API_KEY 를 추가하고 삼십 분 동안 여러 번 저장했는데도, secrets_list 는 여전히 똑같은 옛날 시크릿 세 개만 돌려준다는 내용이었습니다.
저장 경로 자체는 멀쩡했습니다. 시크릿이 보관소에 닿은 적이 한 번도 없었던 것인데, 설정에는 시크릿 페이지가 없기 때문입니다. 데스크톱 앱의 도움말 문구 여섯 개, 이 사이트의 한 페이지, 그리고 에이전트 자체 지침이 모두 존재하지 않는 화면으로 사람들을 보내고 있었습니다. 설정에 실제로 있는 것은 환경 변수 블록이고, 그래서 그는 거기에 토큰을 중괄호로 감싸 적었습니다. 그것은 참조가 아닙니다. 절대 치환되지 않고, 그대로 에이전트에 전달되고, 게다가 프로젝트와 함께 동기화되므로 토큰은 머신을 떠났습니다.
우리는 문구를 고쳐 진짜 경로를 적었고, 에이전트용 도구가 시크릿을 어디에 추가하는지 말하게 했고, 환경 블록에 검사를 하나 넣었습니다. 평문으로 보이는 자격 증명, 참조가 아닌 중괄호 속 이름, 이 머신에 없는 시크릿을 가리키는 참조를 각각 짚어 줍니다.
여기서의 교훈은 시크릿과는 아예 상관이 없습니다. 도움말 문구 속 잘못된 경로는 보안 버그입니다. 사용자를 가장 가까이 있는, 그럴듯해 보이는 입력란으로 옮겨 놓기 때문입니다.
AgentsRoom을 쓰지 않는다면
규칙은 어디로든 옮겨 갑니다. 당신 환경에 맞는 것을 하나 고르세요. 전부 같은 곳, 에이전트가 돌아가는 프로세스의 환경 변수에서 끝납니다.
- 비밀번호 관리자의 CLI. 1Password라면
op run -- claude, Bitwarden이나 Doppler에도 같은 것이 있습니다. 도구가 템플릿에서 참조를 읽어 자식 프로세스의 환경에 진짜 값을 주입하고, 디스크에는 아무것도 쓰지 않습니다. - gitignore 한
.envrc와direnv. 단순하고, 로컬이고, 디렉터리 단위입니다. 첫 값을 넣기 전에.envrc와.env*를.gitignore에 추가하세요. 넣은 뒤가 아니라요. - 에이전트가 절대 읽지 않는
.env파일. 저장소 바깥에 두고, 에이전트를 띄우는 셸에서 읽어 들이세요. 파일이 저장소 안에 있으면 에이전트는 결국 그것을 열고, 메시지에 인용하고, 당신은 출발점으로 돌아옵니다. - 클라우드 자격 증명은 수명이 짧은 것으로. CI에서는 오래가는 AWS 키 대신 GitHub OIDC를, 서비스 계정 키 파일 대신 워크로드 아이덴티티를 쓰세요.
무엇을 고르든, 에이전트가 더는 묻지 않도록 이 규칙을 에이전트 지침에 넣으세요. CLAUDE.md 나 AGENTS.md 의 한 문단이면 됩니다. 자격 증명 값을 절대 묻지 말 것, 절대 파일에 적지 말 것, $NAME 을 쓰고 어떤 변수를 설정해야 하는지 사용자에게 알려 줄 것. 딱 그 내용만 담은 작은 스킬을 공개해 두었고, 다섯 줄짜리입니다.
키가 이미 대화를 거쳐 버렸다면
이 순서대로, 그리고 첫 단계는 선택이 아닙니다.
- 교체하세요. 보낸 프롬프트는 되돌릴 수 없습니다. 그 값은 유출된 것으로 보고 새 값을 발급하세요.
- 트랜스크립트를 찾으세요. CLI들은 대화 파일을 홈 폴더에 보관합니다. 거기서 옛 값을 검색해 일치하는 것을 지우거나 잘라 내세요.
- 텍스트를 복사하는 곳들을 확인하세요. 클립보드 관리자 기록, 에이전트에게 브리핑한 티켓이나 이슈, 에이전트가 저장소에 적어 둔 요약, 그리고 키를 쥔 채 에이전트가 커밋했다면
git log -p까지. - 문은 그다음에 세우세요. 새 값을 환경 변수 뒤에 두고, 이름으로 참조하고, 다음 에이전트가 묻지 않도록 지침을 갱신합니다.
4단계가 마지막인 이유는, 유출된 키 주위에 깔끔한 구조를 세워 봐야 더 말끔한 유출일 뿐이기 때문입니다.
사람들이 실제로 묻는 질문
API 키를 AI 에이전트 프롬프트에 절대 넣으면 안 되는 이유는 무엇인가요?
프롬프트는 저장되고 전달되기 때문입니다. Claude Code, Codex를 비롯한 CLI들은 대화를 디스크의 트랜스크립트 파일에 기록하고, 메시지 자체는 모델 제공업체로 갑니다. 거기서부터 키는 클립보드 기록에도, 메시지를 그대로 받아 적는 모든 로그에도, 대화를 복사하는 재시도와 서브에이전트에도 남습니다. 패턴 기반 마스킹이 잡아내는 것은 sk- 나 glpat- 같은 모양이지, 평범한 단어처럼 보이는 데이터베이스 비밀번호가 아닙니다.
코딩 에이전트는 읽을 수 없는 시크릿을 어떻게 사용하나요?
환경 변수를 통해 사용합니다. 에이전트가 돌아가는 프로세스의 환경 변수에 값을 넣어 두면, 에이전트는 명령에 값 대신 $STRIPE_SECRET_KEY 라고 씁니다. 명령이 실행될 때 셸이 그것을 펼칩니다. 에이전트는 문자 자체를 한 번도 보지 않으므로, 그 문자는 대화에도 트랜스크립트에도 들어가지 않습니다.
대신 명령줄 인자로 시크릿을 넘겨도 되나요?
넘길 수는 있지만 그러지 마세요. 명령줄은 ps 를 통해 머신의 모든 프로세스에 보이고, 셸 기록에도, 프로세스 로그에도, 실행된 명령을 읽는 온갖 진단 도구에도 들어갑니다. 프로세스 하나의 환경 변수는 그 프로세스와 root만 읽을 수 있으니, 노출 면이 훨씬 작습니다.
AgentsRoom은 에이전트가 쓰는 시크릿을 어디에 저장하나요?
홈 폴더에 있는 파일 하나, 권한 0600, 운영체제 키체인으로 암호화되며, 그 머신에만 있습니다. 서버로 동기화되는 것은 없고, 값을 다시 보여 주는 버튼도 없습니다. 앱은 이름을 보여 주고, 에이전트도 이름을 보며, 값은 터미널이나 명령이 시작되는 순간 메인 프로세스 안에서만 복호화됩니다.
에이전트에게 보내는 메시지에 시크릿 참조를 붙여넣으면 어떻게 되나요?
아무것도 치환되지 않습니다. 의도한 동작입니다. 메시지, 티켓 설명, 스킬, 시스템 프롬프트는 글자 그대로의 텍스트라서, 거기에 적힌 참조는 텍스트인 채로 에이전트에 도착합니다. 치환되는 것은 환경 변수의 값뿐입니다. 우리는 이걸 호되게 배웠습니다. 예전에는 티켓 설명에 적어 둔 참조가, 그 티켓에서 띄운 에이전트의 첫 메시지에서는 치환된 상태로 나왔습니다.
키가 이미 대화를 거쳐 버렸습니다. 어떻게 해야 하나요?
무엇보다 먼저 키를 교체하세요. 보낸 것은 되돌릴 수 없기 때문입니다. 그 키는 제공업체 쪽에도 있고, 디스크의 트랜스크립트 파일에도 있습니다. 그다음 그 키가 들어 있는 트랜스크립트를 지우거나 잘라 내고, 클립보드 관리자를 확인하고, 에이전트가 파일에 적어 두지는 않았는지 저장소를 grep 하세요. 새 값을 환경 변수 뒤에 두는 것은 그 뒤의 일입니다.
짧게 정리하면
에이전트는 당신의 키를 읽을 필요가 없습니다. 필요한 것은 환경에 이미 그 키가 들어 있는 프로세스입니다.
값은 저장소 바깥에 한 번만 저장하고, OS가 암호화하게 하세요. 환경이 만들어지는 자리에서 이름으로 참조하세요. 에이전트에게는 $NAME 을 쓰게 하세요. 나머지 통로는 전부 글자 그대로 두세요. 치환되면 편했을 통로까지 포함해서요. 편한 통로가 바로 저장되고 전달되는 통로이기 때문입니다.
그리고 틀렸을 때는, 반드시 틀리게 되어 있습니다만, 키 교체가 먼저고 정리는 그다음입니다.
AgentsRoom 다운로드
모든 AI 에이전트를, 모든 프로젝트에서, 하나의 창으로 실행하세요.
컴패니언 앱: 이동 중에도 에이전트를 모니터링
Claude, Codex, Antigravity CLI 또는 다른 AI 공급자를 사용하세요.
버그와 요청을 공개 백로그로 바로 보내세요.
계속 읽기
Codex, OpenCode, Antigravity, Claude Code의 음성 모드: 어떤 CLI에 있을까?
2026년 10월 13일에 코딩 에이전트 CLI 다섯 개와 앱 두 개를 확인했습니다. Codex CLI와 oh-my-pi는 음성으로 대화하고, Claude Code와 Antigravity CLI는 프롬프트에 받아쓰기만 하며, OpenCode에는 내장된 것이 없습니다. 각각을 시작하는 방법, 오디오가 어디로 가는지, 그리고 음성이 토큰으로 얼마나 드는지 정리했습니다.
기사 읽기Claude Code의 Routines: 클라우드에서 도는 것, 내 컴퓨터에 남는 것, 그리고 고르는 법
Routines는 저장해 둔 프롬프트를 여러분 없이 실행하는 Claude Code의 방식입니다. 예약 시각에, API 호출로, 또는 GitHub 이벤트로 시작되며, 저장소를 새로 클론한 cloud session으로 돌아갑니다. 지금은 리서치 프리뷰 단계입니다. Claude Code에는 작업을 로컬에서 예약하는 방법도 두 가지(데스크톱 앱의 예약 작업과 /loop) 있는데, 셋은 똑같이 동작하지 않습니다. 최소 간격, 로컬 파일 접근, 권한 확인, 노트북이 잠자기에 들어갔을 때 일어나는 일이 다릅니다. 이 가이드는 문서에 적힌 제약과 함께 셋을 정리하고, 저희의 야간 에이전트 일곱 개가 왜 로컬 머신에서 돌아가는지 설명합니다.
기사 읽기Claude 계정 하나를 컴퓨터 두 대에서, 또는 두 사람이: 되는 것과 안 되는 것
Claude 계정 하나는 가진 모든 기기에서 로그인할 수 있고, 모든 기기가 Claude와 Claude Code를 합쳐 같은 5시간 한도와 주간 한도를 씁니다. 두 사람이 계정 하나를 쓰는 것은 Anthropic 약관 위반이고, 가족 요금제도 없습니다. 2026년 10월 25일에 확인한 내용과, 어느 컴퓨터가 얼마나 썼는지 보는 방법을 정리했습니다.
기사 읽기