Claude Code x Obsidian 集成指南:Vault 结构、权限、模板与链接审计
用 Claude Code 安全管理 Obsidian Vault:daily logs、handoff、snippets、content ops 与链接审计。
Obsidian Vault 用得越久,价值越高,但维护成本也会增加。daily notes 会堆积,项目笔记会过期,code snippets 会丢失版本和失败背景,内容选题也可能不再连接到正确的内部链接或 CTA。Claude Code 可以帮忙,因为 Vault 本质上是一组 Markdown 文件。
安全的做法不是让 AI 随意重组整个 Vault。更可靠的 workflow 是限定范围:小而清晰的目录结构、收紧的 permissions、可重复使用的模板,以及每次修改后的 link audit。对初学者来说,Vault 是笔记文件夹,harness 是给 agent 的工作边界,告诉它哪里能读、哪里能写、必须运行什么检查。
本文覆盖四个 use case:daily logs、project handoff、code snippets 和 content operations。也会写清楚常见 pitfall 与 risk,因为真正危险的不是一句话写得不够漂亮,而是静默损坏的链接、错误的 YAML Properties,或不该读取的 private notes。
集成模型
Obsidian 官方帮助说明,internal links 可以使用 [[Note name]] 这样的 Wikilink,也可以使用 Markdown link。Daily notes 是按日期创建笔记的核心插件。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 的主人。
从小型 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 与决策 | 更新交接笔记 |
content-ops/ | 文章想法、CTA、发布检查 | 整理草稿与链接 |
snippets/ | 命令与代码示例 | 添加上下文、标签、失败例 |
templates/ | Obsidian 模板 | 可以读取,修改需确认 |
archive/ | 已完成或冻结笔记 | 不修改 |
private/ | 私人或敏感数据 | 不读取 |
这个结构支持具体 workflow。Daily logs 可以承接昨天未完成任务。Project handoff 记录目标、现状、下一步和 risk。Snippets 保存可运行命令、依赖版本和 failure case。Content operations 检查官方链接、内部链接、/products/ 和 /training/。
在 Vault 根目录添加 CLAUDE.md
这个文件应该写操作规则,而不是抽象愿景。
# 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 模式,除非是在隔离的测试环境。
准备三个 Obsidian 模板
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 .
推荐 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
第一,让 Claude Code 修改 .obsidian/。插件设置、主题、快捷键不是普通笔记。
第二,在 Obsidian 外部批量 rename。这样可能破坏 backlinks。
第三,YAML Properties 中的 Wikilink 没有引号。安全写法是 related: "[[Project name]]"。
第四,daily note 太重。每天填不完的模板会变成噪音。
第五,把 content operations 当成纯写作。公开文章还需要官方链接、内部链接、CTA 和验证记录。
Masa 的测试结果
在 Masa 的测试 Vault 中,收益最高的两个 workflow 是:把昨天未完成任务带入今天的 daily log,以及发布前运行 link audit。最弱的一次实验是要求 Claude Code“清理整个 Vault”;它创建了太多标签和标题。收紧 permissions、使用轻量 templates,再加上 Node audit,得到的 Obsidian workflow 更稳定。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code权限安全阶梯:逐步放开访问而不失控
从只读到有限编辑、验证命令和部署检查的 Claude Code 权限升级流程。
Claude Code 小PR证据包:让小改动真正可审查
用差异、验证命令、公开URL、CTA路径和回滚说明,把Claude Code的小PR变得可审查。
Claude Code 提交前 Review Gate:同时检查差异、测试、公开 URL 和 CTA
提交前用 Claude Code 审查差异范围、build、公开 URL、Gumroad 链接、咨询 CTA、缺少测试和无关文件。