-
설정 파일 하나가 거대한 JSON 덩어리일 때 — OpenClaw가 오타·스키마 위반·비밀 노출을 막은 방법IT 2026. 7. 13. 22:00
개인용 AI 어시스턴트를 직접 운영해 본 사람은 비슷한 경험을 한다. 챗봇이 쓸 모델, 연결할 메신저 채널, 자동화 규칙, 권한 정책이 전부 설정 파일 하나에 쌓인다. OpenClaw는 오픈소스 개인 AI 어시스턴트인데, 그 설정이 모두
~/.openclaw/openclaw.json한 파일에 들어간다. 모델 선택, WhatsApp·Telegram 채널, cron 작업, 훅(hook, 외부 이벤트로 에이전트를 깨우는 HTTP 엔드포인트), API 키까지 — 수십 개 섹션이 한 덩어리다. 문제는 이 파일을 손으로 직접 고칠 때 생긴다. 쉼표 하나 빠뜨리면 게이트웨이(Gateway, OpenClaw의 중심 서버 프로세스)가 아예 부팅을 거부하고, API 키를 따옴표 안에 평문으로 박아 두면 그 비밀이 화면·로그·백업에 그대로 노출된다. 이 글은 OpenClaw가 브라우저 기반 Control UI의 "Config" 메뉴로 이 위험을 어떻게 풀었는지 — 폼과 raw JSON을 병행하고, 스키마로 검증하고, SecretRef로 비밀을 분리하는 방식 — 을 따라간다.배경 — 설정이 한 파일에 모이는 이유와 그 대가
OpenClaw의 설정 파일은 JSON5 형식이다. JSON5는 일반 JSON에 주석과 끝쉼표(trailing comma)를 허용하도록 확장한 형식 — 사람이 손으로 편집하기 좋으라고 고른 것이다. 기본 설정은 다음처럼 짧게 시작한다.
// ~/.openclaw/openclaw.json { agents: { defaults: { workspace: "~/.openclaw/workspace" } }, channels: { whatsapp: { allowFrom: ["+15555550123"] } }, }코드 설명. 이건 문서의 "minimal config" 예시로, 작업 폴더 하나와 WhatsApp 허용 발신자 한 명만 정의한다.
agents는 에이전트(독립적으로 동작하는 AI 인스턴스) 설정,channels는 메신저 연결 설정이다. 설정의 진실의 원천(source of truth)이 파일 하나로 일원화돼 백업·버전 관리·이전이 단순해지는 게 이 접근의 장점이다. 단, 채널을 늘리고 cron·훅을 켜는 순간 같은 파일이 수십 개 중첩 객체로 부풀어 손으로 다루기 버거워진다.OpenClaw가 이 파일을 다루는 경로는 네 가지다. 각 경로가 어디에 강하고 어디서 위험한지를 먼저 보자.
다이어그램 설명. 위 다이어그램은 같은 설정 파일을 고치는 네 가지 입력 경로가 모두 한 파일로 수렴한다는 것을 보여준다. 마법사는 처음 셋업에 좋지만 이미 짜인 설정의 특정 값 하나를 바꾸기엔 무겁고, CLI 한 줄 명령(
openclaw config set)은 스크립트엔 좋지만 어떤 필드가 있는지 미리 알아야 한다. 직접 편집은 가장 자유롭지만 가장 위험하다 — 검증 없이 저장하면 그대로 깨진 설정이 된다. Control UI 경로가 이 글의 주제다. 이 패턴을 고른 이유는 "초보자는 마법사, 자동화는 CLI, 세밀한 조정은 UI"처럼 사용자 성격에 맞는 진입로를 열어 두기 위함이다.그래서 무슨 가치인가. 설정을 한 파일에 모으면 관리는 단순해지지만, 그 파일이 커질수록 직접 편집의 사고 위험이 비선형으로 커진다. Control UI는 바로 이 "커진 한 덩어리"를 사람이 안전하게 만질 수 있게 하는 도구다.
핵심 문제 — 직접 편집이 만드는 세 가지 사고
거대한 JSON 한 덩어리를 손으로 고칠 때 실제로 터지는 사고는 크게 세 가지다. 각각을 구체적인 실패 시나리오로 보자.
첫째, 문법·스키마 위반으로 게이트웨이가 부팅을 거부한다. OpenClaw는 설정에 대해 엄격 검증(strict validation)을 한다 — 스키마에 완전히 일치하지 않으면 받지 않는다는 정책이다. 모르는 키, 잘못된 타입, 허용되지 않는 값이 하나라도 있으면 게이트웨이가 아예 시작을 거부한다. 쉼표 오타나 키 이름 하나가 어시스턴트 전체를 내려 버린다.
둘째, 비밀이 평문으로 노출된다. 모델 API 키, 채널 봇 토큰 같은 비밀을 설정 값에 따옴표로 직접 박으면, 그 문자열이 파일에 그대로 남는다. 설정을 화면에 띄우거나, 로그로 덤프하거나, 백업·git에 올리는 순간 비밀이 함께 새어 나간다. 개인 서버라도 백업 클라우드나 실수로 공유한 화면 캡처를 타고 유출될 수 있다.
셋째, 동시 편집이 서로의 변경을 덮어쓴다. 브라우저 탭 두 개, 또는 UI와 직접 편집이 같은 파일을 거의 동시에 저장하면, 나중에 저장한 쪽이 먼저 저장한 쪽의 변경을 통째로 날린다. 게다가 화면에 표시하려고 길이를 잘라낸(truncated) 설정 스냅샷을 그대로 다시 저장하면, 허용 목록 같은 배열이 잘린 채로 덮여 권한이 조용히 사라질 수 있다.
이 세 사고는 성격이 다르다. 하나로 묶어 비교해 보면 왜 각각 다른 방어가 필요한지 분명해진다.
다이어그램 설명. 위 다이어그램은 세 가지 사고와 각각에 대응하는 방어 장치를 짝지어 보여준다. 좌에서 우로 읽으면, 직접 편집이라는 한 행위가 세 갈래 사고로 갈라지고, 각 사고는 서로 다른 메커니즘으로 해결한다 — 문법·스키마 위반은 폼과 스키마 검증으로, 비밀 노출은 SecretRef와 화면 마스킹으로, 동시 저장 충돌은 base-hash 가드로. 이렇게 짝지어 보여주는 이유는 "Config 메뉴가 그냥 예쁜 편집기"가 아니라 세 가지 독립된 위험을 각각 겨냥한 설계임을 드러내기 위해서다. 함정은 이 세 방어를 한 덩어리로 뭉뚱그려 이해하는 것 — 셋은 동작 시점도 다르다. 스키마 검증은 폼 렌더링 때, SecretRef는 비밀 필드 표시·저장 때, base-hash는 저장 직전에 작동한다.
그래서 무슨 가치인가. 사고를 세 종류로 분리해 정의했기 때문에, 각각 가장 알맞은 시점에 가장 가벼운 방어를 끼울 수 있다. 한 가지 거대한 "안전 모드"로 뭉치면 어느 하나도 제대로 막지 못한다.
해결 방법 — 폼·Raw 병행, 스키마 검증, SecretRef
OpenClaw의 Control UI는 Vite + Lit로 만든 작은 단일 페이지 앱(SPA, 페이지 전환 없이 한 페이지에서 동작하는 웹 앱)으로, 게이트웨이가 직접 서빙한다. 기본 주소는
http://127.0.0.1:18789/이고, 그 안의 "Config" 탭이 설정 편집을 담당한다. 핵심 설계는 세 축이다.축 1 — 폼과 Raw JSON을 함께 제공
Config 탭은 두 가지 모드를 가진다. 폼(Form) 모드는 설정에서 입력 폼을 자동 생성한다. 각 필드의
title·description같은 문서 메타데이터를 함께 끌어와 라벨과 설명으로 보여준다. Raw JSON 모드는 텍스트 에디터로 파일 원문을 직접 고치도록 지원한다.두 모드는 단순한 토글이 아니다. 안전하게 전환할 수 없는 상황을 코드가 직접 차단한다.
다이어그램 설명. 위 다이어그램은 모드 선택이 사용자 취향이 아니라 스냅샷의 성질로 결정된다는 것을 보여준다. 분기 질문은 "이 스냅샷을 raw 텍스트로 바꿨다가 되돌려도 원래 모양이 보존되는가"이다. 여기서 왕복(round-trip)이란 파일 원문 → 화면 표시 → 다시 파일 원문으로 돌아왔을 때 포맷·주석·
$include(설정을 여러 파일로 쪼개는 분할 포함 구문) 배치가 그대로 살아남는지를 말한다. 왕복이 가능하면 두 모드를 다 열어 주고, 불가능하면 오른쪽 분기로 가서 폼 모드만 남기고 Raw 모드를 잠근다. 이 패턴을 고른 이유는, 안전하게 왕복할 수 없는 스냅샷에서 Raw 편집을 허용하면 저장 시 주석이나 분할 구조가 평탄화(flatten)돼 사용자의 원래 파일 구조가 파괴되기 때문이다. 함정은 "Raw 모드가 안 보인다"를 버그로 오해하는 것 — 그건 의도된 보호 동작이다.축 2 — 스키마 검증과 적용·재시작
폼은 게이트웨이가 노출하는
config.schema(전체 JSON Schema)와config.schema.lookup(경로별 노드)을 바탕으로 그려진다. 필드의title/description문서 메타데이터는 중첩 객체, 와일드카드(*), 배열 항목([]), 그리고anyOf/oneOf/allOf같은 스키마 합성 분기까지 따라 내려간다. 저장과 적용은 별도 RPC(원격 프로시저 호출, 게이트웨이 API를 부르는 호출)로 나뉜다.다이어그램 설명. 위 다이어그램은 편집부터 적용까지의 저장 파이프라인을 보여준다. 위에서 아래로, 먼저 현재 스냅샷과 그 해시(hash, 파일 내용을 짧은 지문으로 요약한 값)를 받고, 저장 요청에 그 해시를 baseHash로 함께 실어 보낸다. 그다음 중간의 분기 — 제출된 설정 안에 있는 활성 비밀 참조(SecretRef)가 실제로 해석(resolve)되는지 미리 점검(preflight)한다. 해석되지 않는 활성 참조가 하나라도 있으면 파일에 쓰기 전에 거부하고, 모두 해석되면 기록한 뒤 적용·재시작으로 넘어가 마지막 활성 세션을 깨운다. 이 패턴을 고른 이유는 "쓰고 나서 깨진 걸 발견"하는 대신 "쓰기 전에 막는" 것이 복구 비용을 크게 줄이기 때문이다. 함정은 저장(
config.set)과 적용(config.apply)을 같은 동작으로 착각하는 것 — 저장은 파일에 기록만 하고, 적용은 검증 후 실행 중인 게이트웨이가 그 설정을 반영하도록 재시작까지 한다.축 3 — SecretRef로 비밀을 분리
비밀 평문 노출을 막는 핵심이 SecretRef다. SecretRef는 비밀 값 자체를 설정에 박는 대신, "그 비밀을 어디서 가져오라"는 참조(reference)만 적는 객체다. 값이 아니라 위치를 적는다는 게 핵심이다.
{ models: { providers: { openai: { apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" } }, }, }, channels: { googlechat: { serviceAccountRef: { source: "exec", provider: "vault", id: "channels/googlechat/serviceAccount" }, }, }, }코드 설명. 이 예시는 OpenAI API 키를
source: "env"로(환경 변수OPENAI_API_KEY에서 읽기), Google Chat 서비스 계정을source: "exec"로(외부 비밀 관리 도구를 실행해 가져오기) 참조한다. 핵심은 설정 파일 어디에도 실제 키 문자열이 없다는 것이다.source는 비밀의 출처 종류(env환경 변수 /file파일 /exec외부 명령),provider는 그 출처 안에서 어떤 공급자를 쓸지(없으면"default"),id는 그 공급자 안에서의 식별자다 — SecretRef는 이 세 필드를 모두 갖춘 객체다. 이 패턴을 고른 이유는 비밀의 수명과 설정의 수명을 분리하기 위함이다 — 설정 파일은 git에 올리거나 공유해도 키가 새지 않고, 키 자체는 환경 변수나 vault(비밀 전용 저장소)에서 따로 관리된다. 참고로 단순히 값을 끼워 넣고 싶을 때는${VAR_NAME}형태의 환경 변수 치환도 쓸 수 있는데, 이건 로드 시점에 문자열로 대체되는 더 가벼운 방식이고 SecretRef는 구조화된 참조 객체라는 점이 다르다.Control UI는 이 SecretRef를 화면에서 특별히 다룬다. 구조화된 SecretRef 객체 값은 폼의 텍스트 입력란에 읽기 전용(read-only)으로 렌더된다. 코드를 보면 비밀 참조 객체를 감지하면 입력란을 읽기 전용으로 잠그고 "Structured value (SecretRef) - use Raw mode to edit" 같은 안내를 띄운다. 이렇게 하는 이유는, 객체로 된 참조를 폼 텍스트란에서 무심코 고치면 객체가 문자열로 망가지는(object-to-string corruption) 사고가 나기 때문이다. 또한 폼 저장 시에는 복원 불가능한 낡은 마스킹 자리표시자(redacted placeholder, 비밀을 가린
***같은 표시)는 버리고, 저장된 비밀에 여전히 대응하는 마스킹 값은 보존한다 — 화면에 가려 보이던 별표를 그대로 저장해 진짜 키를 덮어쓰는 사고를 막는다.그래서 무슨 가치인가. 폼·Raw 병행은 초보자와 숙련자를 모두 받고, 스키마 검증은 깨진 설정을 부팅 전에 잡고, SecretRef는 비밀을 설정 본문 밖으로 빼낸다. 세 축이 각자 다른 사고를 막으면서 같은 "Config 한 탭" 안에서 함께 동작한다.
결과 — 거대한 한 덩어리를 안전하게 만지게 되다
이 설계가 사용자·에이전트·시스템에 주는 실제 가치를 정리하면 다음과 같다.
- 부팅 거부 사고가 편집 단계에서 차단된다. 폼이 스키마에서 직접 생성되므로 모르는 키나 잘못된 타입을 만들기 어렵고, 저장 전 미해석 비밀 참조까지 미리 점검(preflight)해 거부한다. 잘못된 설정으로 게이트웨이가 멈추기 전에 막는다.
- 비밀이 설정 본문에서 사라진다. SecretRef로 키를
env/file/exec출처로 빼내면, 설정 파일을 화면에 띄우거나 백업·공유해도 진짜 키가 노출되지 않는다. 화면에서는 구조화된 참조를 읽기 전용으로 잠가 실수로 망가뜨리는 것도 막는다. - 동시 편집 충돌이 조용한 데이터 손실로 이어지지 않는다. 저장에 base-hash 가드가 붙어, 다른 곳에서 먼저 바뀐 설정을 모르고 덮어쓰려 하면 차단된다. 잘린 스냅샷이 허용 목록 배열을 통째로 날리는 사고도 막힌다.
마지막으로, 만약 직접 편집으로 설정이 깨졌더라도 OpenClaw는 성공 부팅마다 "마지막으로 검증된 정상 설정(last-known-good)" 사본을 보관한다. 검증 실패 시 자동 복원은 하지 않지만,
openclaw doctor --fix로 손상된 설정을 고치거나 정상 사본을 복원할 수 있다. 즉 Control UI의 사전 방어가 실패하더라도 한 겹의 사후 안전망이 더 있는 셈이다.핵심을 한 줄로 요약하면 이렇다. 설정이 거대한 JSON 한 덩어리라는 사실은 바뀌지 않지만, OpenClaw는 그 한 덩어리를 "사람이 손으로 직접 깨뜨릴 수 있는 텍스트"에서 "스키마가 지키고, 비밀이 분리되고, 충돌이 차단되는 편집 대상"으로 바꿨다. 개인이 AI 어시스턴트를 오래 운영할수록 — 채널과 자동화가 쌓일수록 — 이 차이가 사고 한 번과 멀쩡한 운영을 가른다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
AI 에이전트에게 셸을 쥐여 주는 순간 — OpenClaw가 exec 실행과 승인 게이트로 위험을 길들인 방법 (0) 2026.07.15 fetch로는 로그인·동적 페이지를 못 읽는다 — OpenClaw가 격리 브라우저로 푼 방법 (0) 2026.07.14 페이지 한 장 읽자고 브라우저를 통째로 띄울 건가 — OpenClaw가 web_fetch로 본문만 가볍게 긁는 방법 (0) 2026.07.14 LLM은 어제 일을 모른다 — OpenClaw가 에이전트에 웹 검색을 붙인 방법 (0) 2026.07.14 에이전트에 외부 도구를 붙이는 콘센트 — OpenClaw가 MCP 서버를 등록·관리하는 방법 (0) 2026.07.13 대화가 쌓일수록 기억이 흐려진다 — OpenClaw가 잠을 자며 기억을 정리하는 방법 (0) 2026.07.13 에이전트가 내 폰과 다른 서버까지 손을 뻗을 때 — OpenClaw가 노드 권한을 통제한 방법 (0) 2026.07.12 에이전트가 스스로 만든 능력을 그냥 켜도 될까 — OpenClaw가 Skill Workshop으로 푼 방법 (0) 2026.07.12 능력을 다 켜 두면 에이전트가 위험해진다 — OpenClaw가 Skills 메뉴로 능력을 골라 켜는 방법 (1) 2026.07.12 에이전트 다섯개가 한 서버에 산다 — OpenClaw가 에이전트마다 격리된 워크스페이스를 한 화면에서 점검하는 방법 (0) 2026.07.11