Skip to navigation

Importing existing knowledge

Bring an Obsidian vault, Claude projects, and ChatGPT or Claude memory into a brain.

You don’t have to start a brain from nothing. Notes you’ve already written and what your AI assistants already remember can go in on the first day. The brain compiles them into connected pages, the same way it handles new captures, and every page links back to the note it came from.

Bring inHow
An Obsidian vaultA script sends every note in the vault to the brain. See Obsidian vault.
Claude or ChatGPT memoryAsk the assistant to write out what it remembers, then save it. See Claude and ChatGPT memory.
A Claude projectSave the project’s instructions, files and memory. See Claude projects.

Before you import

  • Fill in the brain profile first. It shapes how captures are compiled, but only the ones that arrive after it.
  • Try a small sample. Import part of your material into a test brain, check the pages, then run the rest. Each capture is compiled by a model, which counts toward your usage.
  • Give each capture an originRef. The brain skips a capture whose originRef it has already compiled, so you can run an import again without creating duplicates.
  • Mind the limits. One capture holds up to 50,000 characters. A batch request holds up to 100 captures on most plans.

Obsidian vault

An Obsidian vault is a folder of Markdown files, so each note can go in as one capture. Frontmatter, tags and [[links]] stay in the text, and the brain uses them along with the rest of the note when it connects pages.

The script below sends every note in a vault:

  • It skips .obsidian, .trash and other hidden folders, and empty notes.
  • It splits a note longer than 50,000 characters at paragraph breaks.
  • It uses the note’s path as the label, so each page’s sources show which note they came from.
  • It sets originRef to the note’s path plus a hash of its text. When you run the script again, the brain skips notes you haven’t changed and captures the ones you’ve edited, then updates their pages.
  • It skips files that aren’t UTF-8 text, and tells you which ones.

Save it as a file. It needs Python 3.9 or later, or Node 20.12 or later, and no packages.

"""Import an Obsidian vault into a Living Brain.
Usage: python import_obsidian.py <vault folder> <brain id> [--dry-run]
Needs LIVING_BRAIN_API_KEY (and LIVING_BRAIN_SUBJECT_ID if your key isn't tied to one subject).
"""
import hashlib
import json
import os
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
API = "https://api.livingbrain.com/v1"
MAX_CHARS = 50_000 # the API's limit for one capture
BATCH_SIZE = 100 # the per-request limit on most plans
def split_note(text):
"""Split text longer than MAX_CHARS at paragraph breaks."""
if len(text) <= MAX_CHARS:
return [text]
parts, current = [], ""
for paragraph in text.split("\n\n"):
if current and len(current) + len(paragraph) + 2 > MAX_CHARS:
parts.append(current)
current = ""
current = f"{current}\n\n{paragraph}" if current else paragraph
while len(current) > MAX_CHARS: # a single paragraph over the limit
parts.append(current[:MAX_CHARS])
current = current[MAX_CHARS:]
return parts + [current] if current else parts
def vault_items(vault):
notes = (p for p in vault.rglob("*") if p.suffix.lower() == ".md" and p.is_file() and not p.is_symlink())
for path in sorted(notes):
relative = path.relative_to(vault)
# Skip .obsidian (settings), .trash and any other hidden folder.
if any(part.startswith(".") for part in relative.parts):
continue
try:
text = path.read_bytes().decode("utf-8-sig")
except UnicodeDecodeError:
print(f" skipped {relative}: not UTF-8 text")
continue
# Normalize Windows line endings so the paragraph split and hashes match on every OS.
text = text.replace("\r\n", "\n").replace("\r", "\n").strip()
if not text:
continue
name = relative.with_suffix("").as_posix()
parts = split_note(text)
for index, part in enumerate(parts, start=1):
digest = hashlib.sha256(part.encode("utf-8")).hexdigest()[:16]
suffix = f" (part {index} of {len(parts)})" if len(parts) > 1 else ""
yield {
"kind": "note",
"content": part,
"label": f"{name}{suffix}"[:256],
# The brain skips a capture whose originRef it has already compiled.
# The hash of the text means an edited note is captured again.
"originRef": f"obsidian:{index}:{digest}:{vault.name}/{name}"[:512],
}
def post_batch(brain_id, items):
headers = {"x-api-key": os.environ["LIVING_BRAIN_API_KEY"], "content-type": "application/json"}
if os.environ.get("LIVING_BRAIN_SUBJECT_ID"):
headers["x-subject-id"] = os.environ["LIVING_BRAIN_SUBJECT_ID"]
body = json.dumps({"items": items}).encode("utf-8")
for attempt in range(5):
request = urllib.request.Request(
f"{API}/brains/{brain_id}/captures/batch", data=body, headers=headers, method="POST"
)
try:
with urllib.request.urlopen(request, timeout=120) as response:
return json.load(response)
except urllib.error.HTTPError as error:
# Retry rate limits and server errors; anything else needs fixing first.
if (error.code != 429 and error.code < 500) or attempt == 4:
raise SystemExit(f"HTTP {error.code}: {error.read().decode('utf-8', 'replace')}")
retry_after = error.headers.get("retry-after", "")
time.sleep(int(retry_after) if retry_after.isdigit() else 30)
except (urllib.error.URLError, TimeoutError) as error:
if attempt == 4:
raise SystemExit(f"Network error: {error}")
time.sleep(30)
def main():
args = [arg for arg in sys.argv[1:] if arg != "--dry-run"]
if len(args) != 2:
raise SystemExit(__doc__)
vault, brain_id = Path(os.path.abspath(os.path.expanduser(args[0]))), args[1]
items = list(vault_items(vault))
print(f"{len(items)} captures from {vault}")
if "--dry-run" in sys.argv:
for item in items:
print(f" {item['label']} ({len(item['content']):,} characters)")
return
if not os.environ.get("LIVING_BRAIN_API_KEY"):
raise SystemExit("Set LIVING_BRAIN_API_KEY first.")
for start in range(0, len(items), BATCH_SIZE):
batch = items[start : start + BATCH_SIZE]
post_batch(brain_id, batch)
print(f" sent {start + len(batch)} of {len(items)}")
if __name__ == "__main__":
main()

Then run it:

1

Find the brain ID

List your brains and copy the id of the one to import into. To keep the vault separate, create a new brain for it first.

curl https://api.livingbrain.com/v1/brains -H "x-api-key: YOUR_API_KEY"
2

Preview the import

Set your API key, then list what would be sent without sending it:

export LIVING_BRAIN_API_KEY="YOUR_API_KEY"
python import_obsidian.py ~/Documents/MyVault YOUR_BRAIN_ID --dry-run

In Windows PowerShell, set the key with $env:LIVING_BRAIN_API_KEY = "YOUR_API_KEY" instead. If your API key isn’t tied to one subject, also set LIVING_BRAIN_SUBJECT_ID. See Authentication.

3

Run the import

Run the same command without --dry-run. Captures compile in the background, so pages appear over the next few minutes. Track the sources to find any that failed. If the script stops partway, run it again. Notes that were already sent are skipped.

To keep the brain up to date, run the script again whenever you like, for example on a schedule. The brain only compiles notes it hasn’t seen in their current form. Deleting or renaming a note in the vault doesn’t remove what the brain already compiled from it.

The script only reads Markdown files, so images, PDFs and other attachments stay behind. To add a PDF, capture it with kind: "file" and a fetchUrl, a link the brain can download, such as a temporary signed link from your file storage. See Capturing knowledge.

Claude and ChatGPT memory

Claude and ChatGPT keep a memory of you: your work, projects, preferences and instructions. The simplest way to move it is to ask the assistant to write out what it remembers.

1

Ask the assistant to write out its memory

Start a new chat and send:

Write out everything you remember about me from your memory, exactly as stored: my work,
projects, preferences, tools and any instructions I've given you. Put one item per line in a
single code block, with the date you learned it if you know it. Then tell me if the list is
complete.

To see what the assistant has stored, open its memory settings. At the time of writing, that’s Settings > Your Claude, then Manage next to Your memory, in Claude, and Settings > Personalization > Memory in ChatGPT.

2

Read the list

Compare the list with the memory settings, since an assistant can leave items out. Remove anything you don’t want in the brain before you save it.

3

Save it to the brain

If the assistant is connected to Living Brain, ask it to save the list itself:

Save that list to my Living Brain called "Personal".

It saves the list with living_brain_capture. Otherwise, paste the list into a file such as ai-memory/chatgpt-memory.md and run the vault script on the ai-memory folder. When you import an updated list later, the script captures the new version.

Claude projects

A Claude project has three parts worth keeping:

PartHow to bring it in
InstructionsCopy them from the project page into a Markdown file.
Files in the project’s knowledgeUse the original files. Save text files as Markdown. Capture PDF, Word (.docx) and PowerPoint (.pptx) files with kind: "file" and a fetchUrl the brain can download.
Project memoryInside the project, ask Claude to write out its memory, as in Claude and ChatGPT memory.

Put each project’s Markdown files in a folder and run the vault script on it. To tell projects apart, give each its own brain, or start each file name with the project’s name, which then appears in the source labels.

Check the result

Open the brain on livingbrain.com to see the new pages and how they connect, or ask a connected assistant about something from your notes. Each page lists the sources it came from, so you can trace an answer back to the note or memory behind it.