bholmesdev/hubble.md · 언어 TypeScript(모노레포) · 라이선스 MIT · 데스크톱 버전 v0.1.23 · 별 약 1,090 · 포크 63 · 생성 2026-02-17 · 저자 Ben Holmes(@bholmesdev, Astro 코어팀))
Hubble은 "내 폴더에 쌓이는 마크다운 파일을, 사람도 AI 에이전트도 함께 편집하는 노트 앱"이다. Notion 같은 블록 에디터(/ 명령, 마크다운 단축키, 프론트매터 속성)를 쓰지만 저장 형식은 평범한 .md 텍스트다. 그래서 에이전트를 그 폴더에 붙이면 곧바로 협업이 되고, 에이전트가 노트를 고치면 Hubble이 즉시 화면을 갱신한다. 여기에 "HTML 앱"이라는 확장 개념으로, 노트 폴더를 표·책장·지도 같은 맞춤 뷰로 바꿀 수 있다.
Notion은 책(노트)을 회사 창고(클라우드 DB)에 넣어 두고, 창고 규칙(플러그인 SDK)을 통해서만 정리를 돕는다. Hubble은 책을 내 방 책장(로컬 폴더)에 그대로 두고, 사람이든 AI 도우미든 같은 책장에서 직접 책을 꺼내 고치게 한다.
도우미(에이전트)가 새 정리 도구가 필요하면? Hubble은 앱스토어를 뒤지지 않는다. 도우미에게 "이 책들 목록표 하나 만들어 줘"라고 말하면, 도우미가 그 자리에서 HTML 파일 한 장을 써넣고, 그게 곧 그 폴더 전용 미니앱이 된다.
---로 감싸 적는 YAML 메타데이터 영역. 제목·태그·날짜·완료여부 같은 "속성"을 담는다. Hubble은 이 프론트매터를 File Properties(파일 속성)의 단일 진실원으로 삼아, true/false는 체크박스로, 2026-07-30은 날짜로 자동 해석한다.Hubble이 떠오른 배경엔 "코딩 에이전트가 파일을 직접 다루는" 흐름이 있다. Claude Code·Codex·Cursor는 결국 폴더 안 파일을 읽고 쓰는 도구다. 그렇다면 노트 앱이 파일을 독점 DB에 가둘 게 아니라 그냥 폴더에 풀어 두면, 에이전트가 곧바로 1급 협업자가 된다는 게 Hubble의 통찰이다. README의 핵심 문구: "Point your agent at your notes folder to start collaborating. Hubble will live-reload as your agent edits."
두 번째 무기는 확장 방식이다. 보통 앱은 플러그인 SDK(자바스크립트 API)를 배워야 기능을 늘린다. Hubble은 "에이전트에게 HTML 앱을 짜 달라"로 대체한다. 파일시스템 자체가 확장 표면이 되는 셈이다.
| 항목 | Hubble | Notion | Obsidian |
|---|---|---|---|
| 저장 형식 | 로컬 .md + 프론트매터 | 클라우드 독점 DB | 로컬 .md |
| 편집 경험 | 블록 에디터(WYSIWYG) | 블록 에디터 | 주로 마크다운 원문 |
| 에이전트 협업 | 폴더가 곧 API · 라이브리로드 | API 있으나 파일 아님 | 파일 편집 가능(리로드 수동) |
| 확장 방식 | 에이전트가 HTML 앱 작성 | 제한적 API/통합 | JS 플러그인 SDK |
| 가격/라이선스 | 무료 · MIT 오픈소스 | 상용(부분 무료) | 무료(비오픈소스) |
맞다, 텍스트 에디터로도 .md는 연다. 어려운 건 (1) Notion급 편집 경험을 주면서도 원본을 평문으로 유지, (2) 외부(에이전트) 편집을 안전하게 실시간 반영, (3) 에이전트가 짠 임의의 HTML을 시스템을 망치지 않게 실행하는 것이다. Hubble은 마크다운↔리치텍스트 왕복 변환 엔진, chokidar 파일 감시 라이브리로드, 그리고 불투명 출처(opaque origin) 샌드박스 iframe + 토큰 인증 브로커로 이 셋을 각각 정공법으로 풀었다.
번역가와 원문의 관계. 당신은 예쁜 편집 화면(리치텍스트)에서 글을 쓰지만, 디스크에는 늘 "원문 마크다운"이 저장된다. 화면과 원문 사이를 오가는 동시통역사(remark 파서 + ProseMirror 직렬화기)가 있어, 어느 쪽에서 고쳐도 뜻이 어긋나지 않게 왕복 번역을 보장한다. 그래서 에이전트가 원문을 고쳐도, 사람이 화면에서 고쳐도 충돌 없이 같은 파일이 유지된다.
Hubble은 pnpm 워크스페이스 모노레포다(전부 TypeScript ~5.8, 린트·포맷은 ESLint+Prettier가 아니라 Biome 2.4, React Compiler까지 켜져 있음). 크게 apps/*(실행되는 3개 앱)와 packages/*(공유 코어)로 나뉜다.
electron-vite 5 + electron-builder 26 + electron-updater 6.8(자동 업데이트, 현재 macOS 한정).@tiptap/core·react·pm·starter-kit + image/list 확장) — ProseMirror 기반 블록 에디터.class-variance-authority·clsx·tailwind-merge, sonner(토스트), @iconify-json/mingcute.@tailwindcss/vite, HTML 앱엔 @tailwindcss/browser 주입).chokidar 4(파일 감시), Zod 4(검증), ignore 7(.gitignore 파싱), alpinejs 3.15(HTML 앱에 주입).astro-icon·gsap·canvas-confetti, Node 22 필요.react-router 7 + Convex 1.34(백엔드). 로컬 FS가 없으니 워크스페이스는 항상 클라우드 동기화형.unified·remark-parse·remark-gfm·mdast·yaml로 파싱, ProseMirror로 변환. Hubble의 심장.editor 위의 React UI. Tiptap 테이블, highlight.js/lowlight(코드 하이라이트), cmdk(커맨드 팔레트), @dnd-kit(드래그).global.js, html-app-theme.css)만. HTML 앱에 주입되는 에이전트 브리지.hubble 바이너리.Hubble의 도메인 모델은 CONTEXT.md의 용어집이 곧 설계도다. 핵심 개념: Workspace(.hubble/config.json이 있는 논리적 노트 묶음. 클라우드가 아니라 설정 파일의 존재로 정의됨), HTML App(폴더 안의 .html 파일을 Hubble이 실행하는 미니 UI), Embed(마크다운 안에 인라인으로 박은 HTML 앱), Workspace Snapshot(원자적으로 조립된 로딩 완료 상태 — 이게 완성돼야 화면을 그린다).
흥미로운 사실: 이 팀은 원래 Tauri에서 Electron으로 옮겼다(ADR-0002). 이유는 렌더러를 Chrome DevTools Protocol로 노출해 에이전트가 UI를 테스트할 수 있게 하려던 것. Main 프로세스만 파일시스템을 만지고, Preload는 contextBridge로 타입 안전한 window.desktopApi 하나만 노출하며(contextIsolation:true, nodeIntegration:false), Renderer는 결론만 받는다. 특히 렌더러는 사용자가 허용한 경로(grantedFiles/grantedRoots)만 만질 수 있는 권한 그랜트 모델을 쓴다 — 앰비언트 파일 접근이 없다.
Hubble의 "평문을 유지하면서 예쁜 편집"을 가능케 하는 엔진(packages/editor). .md 텍스트는 먼저 프론트매터(YAML)를 본문과 분리(ADR-0003)한 뒤, remark AST를 거쳐 Tiptap/ProseMirror 문서로 바뀐다. 편집 후엔 다시 마크다운으로 직렬화해 디스크에 쓴다. 이 왕복 신뢰성(round-trip fidelity)은 MarkdownRoundtrip.test.ts 등으로 집요하게 테스트된다 — "화면에서 고치고 저장했더니 원문이 미묘하게 망가지는" 사고를 막는 계약이다.
가장 배울 게 많은 부분(ADR-0004/0005). 에이전트가 써넣은 임의의 HTML을 어떻게 안전하게 실행할까? Hubble은 불투명 출처(opaque origin) 샌드박스 iframe에 그 HTML을 넣는다: sandbox="allow-scripts allow-forms"(same-origin 없음), name={token}으로 고유 토큰 부여. iframe은 파일시스템에 직접 못 닿고, 오직 주입된 window.hubble.files.* API를 postMessage로 호출한다. 호스트 쪽 브로커(IframeView.tsx)가 토큰을 확인하고 Zod 스키마 검증 + 워크스페이스 경로 확인(assertPathInWorkspace)을 거친 뒤에만 실제 파일 작업을 대행한다.
"에이전트가 짠 코드니까 조심"이 문구가 아니라 구조다. ① 불투명 출처 iframe으로 브라우저 레벨 격리 → ② postMessage로만 통신(직접 호출 불가) → ③ 토큰으로 "이 iframe이 맞는지" 확인 → ④ Zod로 입력 검증 → ⑤ 경로가 워크스페이스 안인지 확인. 다섯 겹의 방어선이 겹쳐 있다. 신뢰할 수 없는 플러그인/코드를 실행해야 하는 모든 앱의 교과서.
<iframe sandbox>에 allow-same-origin을 빼면, 그 iframe은 부모 페이지와 다른 "출처 없는(null) 출처"로 취급된다. 부모의 쿠키·localStorage·DOM에 접근할 수 없고, 오직 postMessage로만 대화할 수 있다. 신뢰할 수 없는 코드를 유리 상자에 가두는 웹 표준 기법.에이전트는 Tailwind 클래스와 Alpine x-data만 쓴 평범한 .html을 써넣으면 된다. 비결은 Main 프로세스가 hubble-asset:// 프로토콜로 그 파일을 서빙하기 전에 테마 CSS·Hubble 런타임·Tailwind 브라우저 빌드·Alpine.js를 자동 주입하기 때문이다. 주입되는 packages/runtime/global.js가 window.hubble을 만든다:
// packages/runtime/global.js — HTML 앱이 파일을 다루는 유일한 통로
window.hubble = {
files: {
list: (glob = "**/*") => requestHubble("files.list", { glob }),
read: (path) => requestHubble("files.read", { path }),
create: (input) => requestHubble("files.create", { input }),
update: (path, patch) => requestHubble("files.update", { path, patch }),
remove: (path) => requestHubble("files.remove", { path }),
open: (path) => requestHubble("files.open", { path }),
// safeList/safeRead/... = 던지지 않고 { ok, value } 반환하는 변형
},
};
호스트 쪽 브로커는 각 메서드를 Zod로 검증하고 경로를 워크스페이스 안으로 가둔다. update는 패치형이라 생략한 본문·속성 키는 보존되고, 속성 삭제는 명시적으로만 된다:
// apps/desktop/src/editor/IframeView.tsx (요지)
if (request.method === "files.update") {
const path = await resolveFilePath(parseInput(markdownPathSchema, params.path), true);
const patch = parseInput(filePatchSchema, params.patch); // Zod 검증
const absolutePath = await resolveWorkspaceFile(workspacePath, path, { exists: true });
const markdown = await applyMarkdownPatch(absolutePath, patch); // 실제 파일 수정
await refreshFiles(workspacePath);
return { ok: true, value: parseMarkdownFile(path, markdown) };
}
구조에서 읽히는 의도는 "코어와 껍데기의 분리"다. 마크다운 엔진(editor)은 React를 모르는 순수 로직으로 떼어 두고, React UI(ui)가 그 위에 얹힌다. 그래서 데스크톱(Electron)·브라우저(Convex)·CLI가 같은 코어를 공유한다. 그리고 레포 자체가 에이전트로 운영된다 — .agents/.claude/.codex 설정과 docs/adr(설계 결정 기록)이 그 증거다.
Hubble 저장소 자동화는 Warp Factory 에이전트로 굴러간다: 이슈가 올라오면 에이전트가 분류(triage)·라벨링하고, ready-to-implement 이슈엔 드래프트 PR을 열며, PR은 사람이 보기 전에 AI 코드 리뷰를 거친다. skills-lock.json은 사용 스킬을 콘텐츠 해시로 고정해(mattpocock/skills, vercel-labs/agent-skills 등) 재현성을 확보한다. "AI가 만든 노트 앱을 AI가 유지보수한다"는 실제 사례.
contextIsolation:true, nodeIntegration:false, 타입 안전한 단일 contextBridge API, 그리고 권한 그랜트 모델(렌더러는 사용자가 허용한 경로만 접근). "권한은 기본 없음, 필요한 것만 명시"의 실전판. electron/main.ts와 preload.ts가 교과서.
불투명 출처 샌드박스 iframe + postMessage 비동기 RPC 브로커 + 토큰 인증 + Zod 검증 + 경로 제한. 임의의 사용자/에이전트 작성 HTML을 안전하게 실행하는 보안 아키텍처 사례 연구. ADR-0004/0005를 함께 읽자.
remark(unified) 파싱 + 커스텀 ProseMirror 직렬화기, 그리고 왕복 테스트를 정확성 계약으로 삼는 법. 평문을 진실의 출처로 유지하는 WYSIWYG 에디터를 만드는 진지한 수업.
YAML 1.2 의미론, 타입 추론(true/false→체크박스, YYYY-MM-DD→날짜), 패치 시 알 수 없는 키 보존(ADR-0003). "구조화 데이터를 평문에 얹는" 실전 패턴.
pnpm 워크스페이스(workspace:* 의존), 프레임워크 독립 코어(editor)와 React UI(ui) 분리, ESLint+Prettier 대신 Biome, React Compiler + 감사(audit) 게이트. 여러 앱이 코어를 공유하는 대형 구조의 정석.
디스크를 진실의 출처로 두고 Convex에 콘텐츠 해시 기반 충돌 감지로 조정하는 packages/sync/src/sync.ts. pushed/pulled/deleted/conflicts를 반환하는 재조정(reconciliation) 로직.
| 항목 | 요구 / 권장 |
|---|---|
| 일반 사용자 | GitHub Releases에서 프리빌드 앱 다운로드. macOS는 서명·공증됨, Windows/Linux는 미서명(첫 실행 시 OS 경고 가능) |
| 자동 업데이트 | 현재 macOS 한정 |
| 빌드 도구 | Node.js, pnpm, (macOS 빌드 시) Xcode CLT xcode-select --install |
| 빌드 명령 | pnpm install → pnpm bundle:desktop:mac(또는 :win/:linux) → 결과 apps/desktop/release/ |
| 개발 | pnpm dev:desktop (또는 :debug — 렌더러를 CDP 127.0.0.1:9222로 노출) |
| 검사 | pnpm build(Biome+빌드+타입체크) · pnpm check(Biome) · pnpm typecheck |
| apps/web | Node 22.x |
| 에이전트 연동(선택) | Claude Code/Codex/Cursor 등 코딩 에이전트를 노트 폴더에 지정 + npx skills add bholmesdev/hubble-skills |
데스크톱은 v0.1.23(2026-02-17 생성, 활발히 개발 중)이다. 클라우드 동기화(sync)와 브라우저판(www)은 HEAVILY WIP로 표시돼 있어 안정성을 기대하기 이르다. 자동 업데이트는 macOS만 지원하고, Windows/Linux 빌드는 미서명이라 첫 실행 시 OS가 막을 수 있다. 지금의 안정 경로는 로컬 데스크톱 앱 + 폴더 편집이다.
Hubble에서 새 워크스페이스(폴더)를 만들고 노트를 몇 개 쓴 뒤, 같은 폴더를 Claude Code/Cursor로 열어 에이전트에게 "이 노트에 요약 문단 추가해 줘"라고 시켜 보자. Hubble 화면이 실시간으로 갱신되는지 관찰하라. 프론트매터(--- 블록)를 손으로 넣어 보고 속성으로 인식되는지도 확인.
apps/desktop/fixtures/playground/todo-demo.html를 열어, Alpine.js x-data와 window.hubble.files.* 호출이 어떻게 노트를 목록·생성·토글하는지 읽자. hubble.files.list()와 create()를 직접 한두 줄 바꿔 동작 변화를 관찰하라.
packages/editor에서 pnpm test로 MarkdownRoundtrip.test.ts를 실행하고, 표·체크박스·중첩 리스트가 들어간 마크다운을 새 테스트 케이스로 추가해 보자. 왕복 후 원문이 보존되는지(혹은 깨지는지) 직접 확인하며 변환기의 한계를 체감하라.
hubble-skills를 설치한 뒤 에이전트에게 "이 노트들의 프론트매터 date를 읽어 타임라인 뷰를 그리는 HTML 앱을 만들어 줘"라고 요청하자. 생성된 파일이 어떤 hubble.files API를 쓰는지, 샌드박스 안에서 왜 안전한지 코드로 추적하라.
IframeView.tsx 브로커와 runtime/global.js에 files.rename 같은 새 메서드를 end-to-end로 추가해 보자. Zod 스키마·경로 확인·에러 처리(safe 변형)를 모두 갖추고, HTML 앱에서 호출해 동작시키면 신뢰 경계 설계를 온전히 이해하게 된다.
| 주차 | 주제 | 무엇을 / 왜 |
|---|---|---|
| 1주차 | 마크다운 & 프론트매터 | CommonMark/GFM 문법, YAML 프론트매터. "글이 곧 구조화 데이터"라는 관점 잡기. |
| 2주차 | unified / remark AST | 마크다운을 트리(mdast)로 파싱·변형·직렬화. Hubble editor 코어의 기반. |
| 3주차 | ProseMirror / Tiptap | 리치텍스트 문서 모델, 스키마, 트랜잭션. 마크다운↔에디터 변환 구현. |
| 4주차 | Electron 아키텍처·보안 | Main/Renderer/Preload 3분할, contextIsolation·sandbox·IPC, 권한 그랜트. |
| 5주차 | 웹 샌드박싱 | iframe sandbox, 불투명 출처, postMessage RPC, CSP. 신뢰할 수 없는 코드 격리. |
| 6주차 | 파일 감시 & 로컬 우선 | chokidar 감시, 외부 변경 반영, 콘텐츠 해시 충돌 감지(Convex 동기화). |
| 7주차 | 모노레포 & 툴링 | pnpm workspace, Biome, React Compiler, electron-builder 크로스플랫폼 배포. |
| 8주차 | 에이전트 친화 설계 | "파일시스템을 API로", 스킬(skills) 패턴, 레포 자동화. 나만의 에이전트-네이티브 도구 설계. |
---로 감싼 YAML 메타데이터. Hubble의 File Properties 단일 진실원(타입 자동 추론).npx skills add bholmesdev/hubble-skills)CONTEXT.md · AGENTS.md · docs/adr/* — 이 딥다이브의 1차 소스