Obsidian Knowledge Base – curl API Reference
When to use this skill
There are three natural moments to reach for Obsidian:
- Before a task — search for existing context so you don't repeat research or contradict prior decisions
- Mid-task — when you discover something non-obvious (a hidden bug cause, a framework gotcha, an architecture constraint, a deliberate tradeoff) that would cost future-you to rediscover
- After a task — persist new learnings, updated project status, or decisions made during the session
If something gets written to CLAUDE.md or project documentation, consider whether it also belongs in Obsidian — CLAUDE.md is project-scoped, Obsidian is cross-project and searchable.
Base Config
- URL:
$OBSIDIAN_BASE_URL - Auth:
$OBSIDIAN_API_KEY - Root folder:
$OBSIDIAN_ROOT(default:claude-memory) - SSL: self-signed cert → always use
curl -sk - Default subfolders:
private/,work/,misc/
Setup: Add these vars to
~/.claude/settings.jsonunder the"env"key — not to~/.claude/settings.local.json(that path is not a recognized Claude Code settings scope and is silently ignored):{ "env": { "OBSIDIAN_BASE_URL": "https://127.0.0.1:27124", "OBSIDIAN_API_KEY": "your-key" } }
Helper alias (reduces repetition)
obs() { curl -sk -H "Authorization: Bearer $OBSIDIAN_API_KEY" "$@"; }
ROOT="${OBSIDIAN_ROOT:-claude-memory}"
Note: Each Bash tool call runs in a fresh shell —
obs()andROOTdo not persist between invocations. Redefine both at the top of each command, or inline the fullcurl -sk -H "Authorization: Bearer $OBSIDIAN_API_KEY"directly.
Search
Uses Obsidian's built-in index — fast even on large vaults.
obs -X POST "$OBSIDIAN_BASE_URL/search/simple/?query=KEYWORD" \
| python3 -c "import sys,json; [print(r['filename']) for r in json.load(sys.stdin)]"
Use specific keywords that likely appear in note filenames (e.g. auth, docker, deployment).
Read a note
obs "$OBSIDIAN_BASE_URL/vault/${ROOT}/work/My-Note.md"
URL-encode spaces as %20. For complex paths:
python3 -c "import urllib.parse; print(urllib.parse.quote('PATH/WITH SPACES'))"
Create or update a note (PUT = upsert)
obs -X PUT \
-H "Content-Type: text/markdown" \
--data-binary "# Title
Content here." \
"$OBSIDIAN_BASE_URL/vault/${ROOT}/work/My-Note.md"
Append to an existing note
obs -X POST \
-H "Content-Type: text/markdown" \
--data-binary "
## New Section
Content." \
"$OBSIDIAN_BASE_URL/vault/${ROOT}/work/My-Note.md"
Delete a note
obs -X DELETE "$OBSIDIAN_BASE_URL/vault/${ROOT}/work/My-Note.md"
List folder contents
obs "$OBSIDIAN_BASE_URL/vault/${ROOT}/work/"
Note naming conventions
- One topic per note — no catch-all files
- Filename = searchable keyword (
Docker-Port-Conflicts.md,Auth-JWT-Setup.md) - Path:
$OBSIDIAN_ROOT/{private|work|misc}/{Topic}.mdwork/— project notes, architecture decisions, tickets, bug findingsprivate/— personal projects and private insightsmisc/— tools, plugins, general knowledge that doesn't fit elsewhere
Error codes
| Code | Meaning |
|---|---|
| 200/204 | OK |
| 401 | Wrong or missing API key |
| 404 | Note/path doesn't exist |
| 0 / refused | Obsidian not running or plugin disabled |
One-time Migration (old → new folder structure)
If your vault still uses a legacy structure (e.g. Claude Code/{Allgemein,Privat,Arbeit}/), run this script to move all notes into the new $OBSIDIAN_ROOT/{misc,private,work}/ layout:
python3 - <<'EOF'
import subprocess, json, urllib.parse, os, sys
BASE = os.environ["OBSIDIAN_BASE_URL"]
KEY = os.environ["OBSIDIAN_API_KEY"]
ROOT = os.environ.get("OBSIDIAN_ROOT", "claude-memory")
FOLDER_MAP = {
"Claude Code/Allgemein": f"{ROOT}/misc",
"Claude Code/Privat": f"{ROOT}/private",
"Claude Code/Arbeit": f"{ROOT}/work",
}
def api(method, path, **kwargs):
url = f"{BASE}/vault/{urllib.parse.quote(path, safe='/')}"
cmd = ["curl", "-sk", "-X", method, "-H", f"Authorization: Bearer {KEY}"]
if "data" in kwargs:
cmd += ["-H", "Content-Type: text/markdown", "--data-binary", kwargs["data"]]
cmd.append(url)
r = subprocess.run(cmd, capture_output=True, text=True)
return r.stdout
for old_folder, new_folder in FOLDER_MAP.items():
listing = api("GET", old_folder + "/")
try:
files = json.loads(listing).get("files", [])
except Exception:
print(f"Skip {old_folder} — not found or empty")
continue
for f in files:
if not f.endswith(".md"):
continue
old_path = f"{old_folder}/{f}"
new_path = f"{new_folder}/{f}"
content = api("GET", old_path)
api("PUT", new_path, data=content)
api("DELETE", old_path)
print(f"Moved: {old_path} → {new_path}")
print("Migration complete.")
EOF