Claude Code x Obsidian 가이드: Vault 구조, permissions, 템플릿, 링크 감사
Claude Code와 Obsidian을 안전하게 연결해 daily logs, handoff, snippets, content ops, link audit를 운영합니다.
Obsidian Vault는 오래 쓸수록 가치가 커지지만, 관리 비용도 같이 늘어납니다. daily notes가 쌓이고, 프로젝트 노트는 최신 상태를 잃고, 코드 snippets는 버전과 실패 맥락이 빠지고, 콘텐츠 아이디어는 내부 링크나 CTA와 분리됩니다. Claude Code는 이 문제를 줄일 수 있습니다. Vault는 결국 Markdown 파일이 들어 있는 폴더 구조이기 때문입니다.
안전한 방식은 AI에게 전체 Vault를 마음대로 정리시키는 것이 아닙니다. 더 좋은 workflow는 범위를 좁히는 것입니다. 작은 폴더 구조, 제한된 permissions, 재사용 가능한 템플릿, 변경 후 link audit를 둡니다. 초보자에게 쉽게 말하면 Vault는 노트 보관 폴더이고, harness는 에이전트가 어디를 읽고 쓰고 검증할 수 있는지 정하는 작업 발판입니다.
이 글은 네 가지 use case를 다룹니다. daily logs, project handoff, code snippets, content operations입니다. 동시에 pitfall과 risk도 구체적으로 다룹니다. 진짜 문제는 문장이 조금 어색한 것이 아니라, 조용히 깨진 링크, 잘못된 YAML Properties, 불필요하게 읽힌 private note입니다.
통합 모델
Obsidian 공식 도움말은 internal links를 [[Note name]] 같은 Wikilink 또는 Markdown link로 만들 수 있다고 설명합니다. Daily notes는 날짜 기반 노트를 여는 core plugin입니다. Templates는 {{date}} 같은 값을 넣을 수 있고, Properties는 노트 상단의 YAML metadata입니다. 공식 문서: internal links, Daily notes, Templates, Properties, Web Clipper templates.
Claude Code 쪽에서는 settings와 permissions가 안전 경계입니다. CLAUDE.md는 Vault 규칙을 알려 주고, .claude/settings.json은 파일 접근과 명령 실행을 제한합니다.
flowchart LR
A["Inbox"] --> B["Claude Code review"]
B --> C["Daily logs"]
B --> D["Projects"]
B --> E["Content ops"]
C --> F["Obsidian links"]
D --> F
E --> F
F --> G["Link audit"]
기록의 원본은 Obsidian Markdown입니다. Claude Code는 정리, 요약, 초안, 검증을 맡는 보조자입니다.
작은 Vault 구조로 시작하기
처음부터 폴더를 많이 만들면 분류 문제가 더 커집니다.
mkdir -p inbox daily projects content-ops snippets templates scripts archive private
| Folder | 역할 | Claude Code에 맡길 수 있는 일 |
|---|---|---|
inbox/ | 정리 전 메모와 Web Clipper 자료 | 생성, 분류 |
daily/ | daily logs와 작업 기록 | 생성, 추가, 전날 요약 |
projects/ | project handoff와 결정사항 | handoff 업데이트 |
content-ops/ | 글 아이디어, CTA, 발행 체크 | 초안과 링크 정리 |
snippets/ | 명령어와 코드 예시 | 맥락, 태그, 실패 예시 추가 |
templates/ | Obsidian 템플릿 | 읽기는 허용, 수정은 승인 후 |
archive/ | 완료된 노트 | 수정 금지 |
private/ | 개인 정보나 비밀 | 읽기 금지 |
이 구조만으로도 구체적인 workflow가 생깁니다. Daily logs는 미완료 작업을 다음 날로 넘깁니다. Project handoff는 목표, 현재 상태, 다음 행동, risk를 한 장에 모읍니다. Snippets는 실행 가능한 명령과 버전을 보존합니다. Content operations는 공식 링크, 내부 링크, /products/, /training/를 점검합니다.
CLAUDE.md 추가
Vault 루트에 아래 파일을 둡니다.
# Obsidian Vault Rules
## Purpose
- This vault stores daily logs, project handoffs, code snippets, and content operations notes.
- Keep notes useful in Obsidian without requiring Claude Code to read private files.
## Directory policy
- `daily/`: create or update daily notes in `YYYY-MM-DD.md`.
- `projects/`: maintain one handoff note per active project.
- `content-ops/`: maintain article plans, CTA checks, and publishing notes.
- `snippets/`: store runnable commands and code examples with context.
- `templates/`: read freely, but ask before editing.
- `archive/` and `private/`: do not edit. Do not summarize private files.
## Note rules
- Use Wikilinks like `[[Project name]]` for internal links.
- Use YAML properties at the top of notes.
- Keep one paragraph under five lines.
- Add a `source:` property when a note comes from a URL.
- Add `status: draft`, `status: active`, or `status: done`.
## Safety rules
- Never rename existing notes without asking first.
- Never rewrite `.obsidian/` settings.
- Before bulk edits, list the target files and wait for approval.
- After changes, run `node scripts/audit-wikilinks.cjs .`.
흔한 pitfall은 “Vault를 보기 좋게 정리해줘” 같은 요청입니다. Claude Code가 폴더, 제목, 태그를 추측하게 됩니다. 결과가 깔끔해 보여도 검색, Graph view, backlinks를 망칠 수 있습니다.
permissions를 좁게 설정
Vault에 .claude/settings.json을 만듭니다.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Read(./CLAUDE.md)",
"Read(./daily/**)",
"Read(./projects/**)",
"Read(./content-ops/**)",
"Read(./snippets/**)",
"Read(./templates/**)",
"Read(./scripts/**)",
"Edit(./daily/**)",
"Edit(./projects/**)",
"Edit(./content-ops/**)",
"Edit(./snippets/**)",
"Write(./inbox/**)",
"Write(./daily/**)",
"Write(./projects/**)",
"Write(./content-ops/**)",
"Write(./snippets/**)",
"Bash(node scripts/audit-wikilinks.cjs .)"
],
"ask": [
"Edit(./templates/**)",
"Bash(git *)",
"WebFetch"
],
"deny": [
"Read(./private/**)",
"Read(./**/.env)",
"Read(./**/.env.*)",
"Edit(./.obsidian/**)",
"Edit(./archive/**)",
"Write(./archive/**)",
"Bash(rm *)",
"Bash(del *)"
]
}
}
이 설정은 일상적인 노트 작업은 허용하지만, 템플릿 변경, Git, WebFetch는 확인하게 합니다. 실제 Vault에서는 넓은 bypass 모드를 피하고, 테스트용 Vault에서만 사용하세요.
세 가지 템플릿
Daily template은 얇아야 합니다.
---
date: "{{date:YYYY-MM-DD}}"
status: active
tags:
- daily
---
# {{date:YYYY-MM-DD}}
## Today
- [ ]
## Learned
-
## Questions
-
## Links
- [[Weekly review]]
Project handoff는 다음 행동을 가장 중요하게 둡니다.
---
status: active
owner: Masa
tags:
- project
- handoff
updated: "{{date:YYYY-MM-DD}}"
---
# Project handoff: {{title}}
## Goal
## Current state
## Next action
- [ ]
## Decisions
-
## Risks and pitfall
-
## Links
- [[Daily note]]
- [[Related snippet]]
Content operations 템플릿에는 수익화 경로를 처음부터 넣습니다.
---
status: draft
channel: blog
source:
target_cta:
- /products/
- /training/
tags:
- content-ops
---
# Content ops note: {{title}}
## Search intent
## Reader problem
## Outline
## Internal links
- [[Claude Code approval sandbox guide]]
- [[Claude Code documentation generation]]
## Monetization CTA
- Product CTA:
- Training CTA:
## Publish checklist
- [ ] Official links checked
- [ ] Code examples tested
- [ ] Broken internal links audited
함께 볼 글은 approval sandbox, documentation generation, content funnel audit입니다. 개인용 템플릿은 products, 팀 도입과 교육은 training으로 연결합니다.
Node로 internal links 감사
다음을 scripts/audit-wikilinks.cjs로 저장합니다.
#!/usr/bin/env node
const fs = require("node:fs");
const path = require("node:path");
const vaultRoot = path.resolve(process.argv[2] || ".");
const ignoredDirs = new Set([".git", ".obsidian", ".trash", "node_modules"]);
const allFiles = [];
const markdownFiles = [];
function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (ignoredDirs.has(entry.name)) continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.isFile()) {
allFiles.push(full);
if (entry.name.toLowerCase().endsWith(".md")) markdownFiles.push(full);
}
}
}
function toPosix(filePath) {
return filePath.split(path.sep).join("/");
}
function withoutMd(value) {
return value.replace(/\.md$/i, "");
}
function stripTarget(raw) {
return raw.split("|")[0].split("#")[0].split("^")[0].trim();
}
function safeDecode(value) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function isExternal(target) {
return /^(https?:|mailto:|obsidian:|#|\/)/i.test(target);
}
function candidateKeys(target, fromFile) {
const clean = safeDecode(stripTarget(target));
if (!clean) return [];
const keys = new Set();
const normalized = toPosix(path.normalize(clean));
keys.add(normalized);
keys.add(withoutMd(normalized));
if (clean.includes("/") || clean.includes("\\")) {
const fromDir = path.dirname(toPosix(path.relative(vaultRoot, fromFile)));
const relative = toPosix(path.normalize(path.join(fromDir, clean)));
keys.add(relative);
keys.add(withoutMd(relative));
} else {
keys.add(withoutMd(path.posix.basename(normalized)));
keys.add(path.posix.basename(normalized));
}
return [...keys].filter(Boolean);
}
walk(vaultRoot);
const byPath = new Map();
const byBase = new Map();
for (const file of allFiles) {
const rel = toPosix(path.relative(vaultRoot, file));
const lowerRel = rel.toLowerCase();
byPath.set(lowerRel, file);
if (rel.toLowerCase().endsWith(".md")) byPath.set(withoutMd(lowerRel), file);
const base = rel.toLowerCase().endsWith(".md")
? withoutMd(path.posix.basename(rel)).toLowerCase()
: path.posix.basename(rel).toLowerCase();
const list = byBase.get(base) || [];
list.push(file);
byBase.set(base, list);
}
const problems = [];
const wikilink = /!?\[\[([^\]]+)\]\]/g;
const markdownLink = /!?\[[^\]]*\]\(([^)]+)\)/g;
for (const file of markdownFiles) {
const text = fs.readFileSync(file, "utf8");
const rel = toPosix(path.relative(vaultRoot, file));
const targets = [];
let match;
while ((match = wikilink.exec(text))) targets.push(match[1]);
while ((match = markdownLink.exec(text))) {
const target = match[1].replace(/^<|>$/g, "").trim();
if (!isExternal(target)) targets.push(target);
}
for (const target of targets) {
const clean = stripTarget(target);
if (!clean || isExternal(clean)) continue;
const keys = candidateKeys(clean, file);
const pathHit = keys.some((key) => byPath.has(key.toLowerCase()));
const baseHits = keys.flatMap((key) => byBase.get(path.posix.basename(key).toLowerCase()) || []);
if (!pathHit && baseHits.length === 0) {
problems.push(`${rel} -> missing [[${clean}]]`);
} else if (!pathHit && baseHits.length > 1) {
problems.push(`${rel} -> ambiguous [[${clean}]] (${baseHits.length} matches)`);
}
}
}
if (problems.length) {
console.error("Broken or ambiguous internal links:");
for (const problem of problems) console.error(`- ${problem}`);
process.exit(1);
}
console.log(`OK: checked ${markdownFiles.length} Markdown files in ${vaultRoot}`);
실행:
node scripts/audit-wikilinks.cjs .
Claude Code prompt는 이렇게 제한합니다.
Read `daily/2026-06-02.md` and `projects/site-refresh.md`.
Create `daily/2026-06-03.md` from `templates/daily.md`.
Carry over unfinished tasks only when they still have an owner.
Do not edit `.obsidian/`, `archive/`, or `private/`.
After editing, run `node scripts/audit-wikilinks.cjs .` and report the result.
흔한 pitfall
첫째, .obsidian/을 수정하게 두는 것입니다. 플러그인 설정, 테마, 단축키는 일반 노트가 아닙니다.
둘째, Obsidian 밖에서 대량 rename을 하는 것입니다. backlinks가 깨질 수 있습니다.
셋째, YAML Properties에서 Wikilink를 따옴표 없이 쓰는 것입니다. related: "[[Project name]]"처럼 씁니다.
넷째, daily note를 너무 무겁게 만드는 것입니다. 매일 쓰지 못하는 템플릿은 노이즈가 됩니다.
다섯째, content operations를 글쓰기만으로 보는 것입니다. 공개 글에는 공식 링크, 내부 링크, CTA, 검증 기록이 필요합니다.
Masa의 테스트 결과
Masa의 테스트 Vault에서는 두 workflow가 가장 효과적이었습니다. 전날 미완료 작업을 오늘 daily log로 넘기는 것, 그리고 발행 전 link audit를 실행하는 것입니다. 가장 약한 실험은 “전체 Vault를 정리해줘”였습니다. Claude Code가 태그와 제목을 너무 많이 만들었습니다. 좁은 permissions, 얇은 templates, Node audit 조합이 더 안정적이었습니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Claude Code 권한 세이프티 래더: 통제력을 잃지 않고 allow 넓히기
read-only에서 제한 편집, 검증 명령, deploy 확인까지 권한을 단계적으로 넓히는 방법.
Claude Code Small PR Proof Pack: 작은 PR을 리뷰 가능한 상태로 만드는 증거 세트
Claude Code의 작은 PR에 diff, 검증, 공개 URL, CTA 경로, rollback을 붙이는 실무 체크리스트.
Claude Code 커밋 전 리뷰 게이트: diff, 테스트, 공개 URL, CTA 확인
Claude Code 작업을 커밋하기 전에 diff 범위, build, 공개 URL, Gumroad 링크, 상담 CTA, 테스트 누락과 무관한 파일을 확인하는 방법입니다.