
AI 에이전트나 스킬을 만들다보면 비밀값을 어떻게 관리하면 좋을지 하는 생각을 자주 하게 된다. API를 호출하려면 API Key가 필요하고, 특정 기능을 자동화하는 과정에서 아이디와 비밀번호가 필요할 수도 있다. 그런데 그 값을 프롬프트에 박아버리면 대화 기록에 남고, 명령어 인자로 넘겨도 shell history나 프로세스 목록에 남게 된다. 설정 파일에 넣어두면 실행은 편하지만, 에이전트가 파일을 읽고 기능을 실행하는 과정에서 특정 값들이 노출될 수도 있다.
이 모든 문제는 결국 "에이전트가 비밀값을 쓰면서도, 그 값이 대화와 파일과 로그에 전혀 남지 않게 만들어야 한다"로 귀결된다. 한 번 넣어두면 만료되기 전까지는 문제 없이 계속 쓸 수 있어야 하고, 필요할 때마다 사용자가 다시 복사해서 붙여 넣지 않아도 되어야 한다. 즉, 비밀값은 어딘가에 저장되어 있어야 한다. 다만 그 위치가 프롬프트나 설정 파일이면 곤란하다. 에이전트가 읽을 수는 있지만, 에이전트가 기억하거나 출력하지는 않는 위치가 필요하다.
macOS Keychain
내가 선택한 방식은 macOS의 Keychain이다. Keychain은 이미 macOS 안에서 비밀번호, 인증서, 앱 credential을 저장하는 용도로 쓰이고 있다. 브라우저나 앱이 비밀번호를 저장할 때 사용하는 그 익숙한 저장소를, 에이전트와 스킬의 비밀값 저장소로도 활용하는 것이다.
이 방식의 장점은 명확하다. 먼저 비밀값을 프롬프트에 다시 적지 않아도 된다. 한 번 Keychain에 넣어두면, 이후에는 스킬이 정해진 이름으로 값을 꺼내 쓸 수 있다. 사용자는 매번 API Key를 복사해서 붙여 넣지 않아도 되고, 에이전트는 대화 안에서 비밀값을 직접 전달받지 않아도 된다.
두 번째 장점은 비밀값의 위치가 분명해진다는 점이다. 설정 파일에 둘 수도 있고, 환경변수에 둘 수도 있고, 어딘가의 임시 파일에 둘 수도 있다. 하지만 저장 위치가 많아질수록 나중에 “이 값이 어디서 들어왔지?”를 추적하기 어려워진다. Keychain을 기준으로 삼으면 규칙이 단순해진다. 비밀값은 Keychain에 있고, 스킬은 필요할 때만 거기서 읽는다.
세 번째 장점은 출력하지 않는 규칙을 강제하기 쉬워진다는 점이다. 에이전트가 알아야 하는 것은 비밀값 그 자체가 아니라, Keychain에서 어떤 항목을 읽어야 하는지다. 예를 들어 service와 account 이름만 알고 있으면 된다. 실제 값은 API 요청을 만들거나 로그인 폼을 채우는 순간에만 메모리로 올라오고, 작업이 끝나면 요약이나 로그에 남기지 않는다.
blog command
이 구조는 스킬을 만들 때 특히 편하다. 내가 기존에 쓰던 blog 관리 커맨드는 매번 비밀값을 넘겨주어야 했다. 글을 만들거나 수정할 때마다 인증값을 함께 전달해야 했고, 커맨드는 그 값을 이용해 API 요청을 보냈다. 기능은 동작했지만, 사용할수록 불편한 지점이 분명해졌다. 같은 값을 계속 복사해서 붙여 넣어야 했고, 그 값이 대화 안에 남는 것도 신경 쓰였다.
그래서 blog command의 인증 흐름을 Keychain 기준으로 바꿨다. 먼저 어떤 이름으로 비밀값을 저장하고 찾을지 정해야 했다. Keychain에서는 값을 식별할 때 service와 account를 함께 사용한다. service는 이 비밀값이 어떤 서비스나 기능에 쓰이는지, account는 그 안에서 어떤 계정이나 주체의 값인지 나타낸다.
Keychain service name: `opencode.blog.ayden94.token`.
Keychain account name: `ayden94-blog`.에이전트나 스킬은 실제 비밀값을 기억하지 않는다. 대신 이 두 식별명만 알고 있다. 값을 저장할 때도 같은 이름을 쓰고, 값을 읽을 때도 같은 이름을 쓴다. 나중에 인증값을 교체해야 할 때도 같은 항목을 갱신하면 된다
커맨드 원문에는 이 규칙이 이렇게 들어간다.
- If no bearer token is provided, retrieve the stored token from macOS Keychain. If no stored token exists, ask the user to provide a token.
- Never print the raw token in commands, logs, summaries, or errors.
Keychain service name: `opencode.blog.ayden94.token`.
Keychain account name: `ayden94-blog`.
When the user provides a token, immediately upsert it into Keychain, replacing any previous value:
`security add-generic-password -a ayden94-blog -s opencode.blog.ayden94.token -w "$TOKEN" -U`
For safer manual/interactive storage, prefer putting `-w` last without an inline value so `security` prompts for the secret instead of placing it in shell history or process arguments:
`security add-generic-password -a ayden94-blog -s opencode.blog.ayden94.token -U -w`
When the user does not provide a token, read it from Keychain:
`TOKEN="$(security find-generic-password -a ayden94-blog -s opencode.blog.ayden94.token -w 2>/dev/null)"`
Do not run `security find-generic-password ... -w` as a standalone user-visible command because it prints the raw token to stdout.핵심은 세 가지다.
- 사용자가 비밀값을 제공하면 Keychain에 저장한다. 이때 같은 service와 account가 이미 있으면 기존 값을 갱신한다.
- 사용자가 비밀값을 제공하지 않으면 Keychain에서 기존 값을 읽는다. 저장된 값이 없으면 다른 곳을 뒤지지 않고 사용자에게 다시 요청한다.
- 어떤 경우에도 비밀값을 출력하지 않는다. API 요청에 필요한 header를 만들 때만 쓰고, 명령어 출력이나 작업 요약에는 남기지 않는다.
또 하나의 규칙은 목적지 확인이다. 기본 URL인 https://ayden94.com에는 저장된 값을 자동으로 쓸 수 있다. 하지만 base-url이 다른 host로 바뀌면 같은 값을 보내면 안 된다. 이때는 새 비밀값을 제공하게 하거나, 최소한 명시적인 확인을 받아야 한다.
이렇게 해두면 비밀값의 출처가 단순해진다. 값은 Keychain에 있고, 커맨드는 같은 식별명으로 읽는다. .env, 저장소 파일, shell history, 추측한 환경변수로 fallback하지 않기 때문에 나중에 문제가 생겨도 어디를 봐야 하는지 분명하다.
Keychain의 한계
이 방식의 장점은 명확하다. 에이전트가 비밀값을 “알고 있는 것처럼” 일할 수 있지만, 실제로는 프롬프트나 설정 파일에 값을 남기지 않는다. 사용자는 한 번 저장해두고 반복해서 쓸 수 있고, 스킬은 값 자체가 아니라 Keychain 항목 이름만 기억하면 된다.
하지만 Keychain이 모든 문제를 해결하는 것은 아니다. 이 방식은 로컬 Mac에서 개인 자동화를 굴릴 때 좋은 기본값에 가깝다. 팀 단위 secret 관리나 production 배포 secret은 별도의 vault, password manager, secret manager를 쓰는 편이 맞다.
iCloud Keychain도 너무 믿고 가정하지 않는 편이 좋다. Safari 비밀번호나 앱 credential은 여러 기기에서 동기화될 수 있지만, CLI로 저장한 generic password 항목이 모든 Mac에서 항상 같은 방식으로 동기화된다고 기대하면 곤란하다. 여러 Mac에서 같은 스킬을 쓴다면 각 Mac에서 한 번씩 등록하게 만드는 쪽이 더 예측 가능하다.
그래도 로컬 에이전트와 스킬에는 이 정도 경계만으로도 꽤 많은 찜찜함이 사라진다. 좋은 자동화는 에이전트에게 모든 것을 알려주는 방식이 아니다. 몰라도 되는 값은 모르게 두고, 필요한 순간에만 안전한 위치에서 빌려 쓰게 만드는 방식이다.
더 읽어보기
2026.05.21
OpenCode + OMO 에이전트 실전 가이드
에이전트를 많이 만든다고 작업이 자동으로 잘 굴러가지는 않는다. 오히려 역할이 겹치고, 권한이 과하게 열리고, 어느 순간부터는 사람이 에이전트들을 관리하느라 더 바빠지는 일이 생긴다. OpenCode와 OMO(Oh My OpenAgent)를 함께 쓸 때 필요한 것은 더 많은 프롬프트가…
2026.05.21
6편 — 하네스와 실전 레시피
테스트 하네스가 테스트 대상을 외부로부터 감싸고 실행 환경을 제어하듯, 에이전트 오케스트레이션 시스템 역시 견고한 하네스가 필요하다. 하네스 없이 개별 에이전트만 늘려가는 방식은 위험하다. 어떤 에이전트가 어떤 권한으로 무엇을 실행했는지 추적이 되지 않을뿐더러, 에이전트의 작은 실수가…
2026.05.21
5편 — 에이전트 조율하기
에이전트 하나에게 탐색, 구현, 리뷰를 모두 시키면 일단 코드는 나온다. 하지만 그 코드가 정말 요구사항에 맞는지, 아키텍처를 해치지는 않는지 확인하는 주체가 사라진다. 구현자와 리뷰어가 같은 에이전트라면, 그 리뷰어는 필연적으로 자기 코드가 맞다고 생각하는 확증 편향에 빠지기 때문이다…
2026.05.21
4편 — OMO에 연결하기
OMO는 OpenCode의 대체재가 아니다. OpenCode가 제공하는 에이전트, 커맨드, 스킬, 권한 시스템이라는 물리적 토대 위에 Sisyphus 중심의 오케스트레이션 레이어를 얹는 플러그인이다. 따라서 OMO를 활성화하더라도 기존에 구축한 .opencode/ 하위의 모든 설정 체계…
2026.06.01
React Server Components를 위한 컴포넌트 아키텍처
이 포스트는 Vercel의 Next.js 팀 소속 개발자 Aurora Scharff가 자신의 블로그에 올린 Component Architecture for React Server Components 게시글을 번역한 것이다. 번역하는 과정에서 다소 의역이 있을 수 있으며, 일부 번역에는…
2026.05.26
차트는 멈췄는데 윈도우가 움직인다
상황 어느날 서비스를 살펴보시던 팀장님께서 이런 말씀을 slack에 남기셨다. 진호님, 예측 차트에서 zoom을 계속하면 어느 순간 라인 차트가 아니라 단일 스캐터 차트처럼 보이는 데, 이거 수정하면 좋을 거 같아요. 어느정도 zoom을 하면 그 이후로는 zoom이 안 되도록 할 수 없…
2026.05.21
피자가게로 이해하는 디자인 패턴
에이든 피자는 처음부터 복잡한 시스템을 만들 생각이 없었다. 처음에는 메뉴 몇 개만 만들면 됐다. 그런데 손님은 커스텀 주문을 넣기 시작했고, 주방은 상태를 나눠야 했고, 결제와 배달앱과 알림이 하나씩 붙었다. 코드도 가게를 닮는다. 장사가 잘될수록 이상하게 더 쉽게 망가진다. 디자인…
2026.05.21
7. Decorator — 토핑 추가할 때마다 클래스를 새로 만들 수 없다
에이든 피자에서 주문서를 객체로 만들자 취소와 재주문은 한결 편해졌다. 그런데 주문이 편해지자 손님들도 한결 편해졌다. 편해진 손님은 더 많은 요구를 한다. "치즈 추가요", "올리브도 추가요", "소스 많이요", "조금 더 바삭하게 구워주세요" 같은 요청이 주문대 위로 쌓이기 시작했다…
댓글
댓글을 불러오는 중...