Skip to content
Agentmart
Submit a tool

obsidian-claude-export-ingest

INTERNAL — called by obsidian-ingest.

Skill by DoxaLogosGit

IndexedMIT
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/doxalogosgit-obsidian-claude-export-ingest

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

INTERNAL — called by obsidian-ingest. Processes Claude Desktop conversation export JSON files in _sources/, routes conversations to existing or new notes, and tracks ingestion state by UUID. Never invoke directly.

Use cases: productivity

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/obsidian-claude-export-ingest

What's inside

Skill name
obsidian-claude-export-ingest
Description
INTERNAL — called by obsidian-ingest. Processes Claude Desktop conversation export JSON files in _sources/, routes conversations to existing or new notes, and tracks ingestion state by UUID. Never invoke directly.
License
MIT
Instructions
472 lines in SKILL.md · view on GitHub

Files (1)

  • SKILL.md

SKILL.md (first lines)

# obsidian-claude-export-ingest (Internal Sub-skill)

Do NOT invoke this directly. It is called by `obsidian-ingest` when Claude export JSON files are detected in `_sources/`.

## Inputs (passed from entry point)

- **Source directory**: the vault-root `_sources/` — always. There is no project-scoped variant.
- **UUID inventory**: map of `{uuid → {note_path, message_count, last_ingested}}`
- **SHA256 inventory**: map of `{file_path → {sha256, note_path}}`
- **Ignored UUIDs**: set of `{uuid}` to skip silently
- **Owners**: map of `{note_path → human|agent|shared}` from the inventory — used by the §2b owner filter, so no note frontmatter is re-read
- **Search paths**: list of vault-relative folder paths to search for topic matching — all scanned project folders, since routing always spans the whole vault
- **Export files**: list of `*.json` (or `*.zip` archives wrapping one) paths in the source directory confirmed as Claude export format. The extraction script reads the export JSON transparently, so a `.zip` is passed through the same subcommands (`list`/`classify`/`extract`) as a bare `.json` — no manual extraction step.

> These inventories are built by the entry point (`obsidian-ingest`) before calling this sub-skill. It reads `ingested_uuids:` and `sources:` frontmatter from all relevant `.md` notes and passes the populated maps in.

## Workflow

### 1. Enumerate conversations in each JSON file

Run the extraction script to get a conversation list:

```bash
python3 .agents/skills/obsidian-ingest/extract_conversations.py <file> list
```

Output is tab-separated rows: `uuid | name | message_count | created_at | updated_at`. Conversations with empty names or no message text are already excluded by the script.

### 2. Route each conversation

First, build a classification against the UUID inventory. Construct the inventory as a JSON string (`{"uuid": message_count, ...}`) from the maps passed in by the entry point, then run:

```bash
python3 .agents/skills/obsidian-ingest/extract_conversations.py <file> classify '{"uuid1": N, ...}'
```

Output rows: `uuid | action | name | stored_count | current_count`
- `unchanged` — skip; add to skipped count
- `decreased` — skip; log as "skipped (count decreased)"
- `update` — append update section; no re-prompt needed
- `new` — topic-match and route (see below)

Additionally filter against the ignored UUIDs set before processing any `new` rows.

| Classification | Ignored set | Action |
|---|---|---|
| `unchanged` | — | Skip |
| `decreased` | — | Skip, log |
| `update` | — | Append update section |
| `new` | UUID in ignored set | Skip silently |
| `new` | UUID not in ignored set, topic match found | Ask user to confirm, then ingest |
| `new` | UUID not in ignored set, topic ambiguous (2+ candidates) | Ask user to choose target note |
| `new` | UUID not in ignored set, no match anywhere in the vault | **Interactive routing** — see §2a. Never auto-create and never silently drop. |

**Topic matching for unknown conversations:**
1. Extract keywords from conversation `name` and first 300 chars of the first assistant message (nouns, domain terms — ignore filler words like "the", "is", "how")
2. Search across all **search paths** using the Obsidian CLI (preferred) or grep (fallback):
   - **CLI (preferred):** `obsidian search "<keywords>"` — uses Obsidian's full-text index; handles aliases and frontmatter fields natively. Parse results for note paths within the search paths.
   - **Fallback (CLI unavailable):** grep note titles and `summary:` frontmatter fields within the search paths.
3. Score by keyword overlap. One clear winner = strong match. Two or more candidates where the top two scores are within 20% of each other = ambiguous — ask user to choose.

### 2a. Interactive Unmatched Routing

For each conversation with no topic match anywhere in the vault, prompt the user with a brief summary:

```
Unmatched conversation: "<conversation name>"
  Preview: <first 150 chars of first assistant message>

Route this conversation:
  1. Add to an existing project note
  2. Create a new note (you'll name it and choose a project folder)
  3. Ignore — skip now and never ask again
  4. Skip for now — ask again next run

Choice:
```

**Option 1 — Route to existing note:** show a numbered list of all project notes from the search paths. User picks one. Ingest the full conversation into that note.

**Option 2 — Create new note:** ask for a project folder (show available `Projects/`, `Ideas/`, etc.) and a note name. Create the note with `owner: shared` and ingest the full conversation.

**Option 3 — Ignore permanently:** add the UUID to the pending ignored list (persisted in §Ignored UUID Persistence). Skip this conversation.

**Option 4 — Skip for now:** do nothing. The UUID is not recorded anywhere, so the next ingest run will ask again.

**Batch the prompts. This is mandatory, not a preference.** Resolve the routing of every unmatched conversation before you write any note. Use `AskUserQuestion`, which carries up to 4 questions per call, in two passes:

1. **Pass 1 — the four-way choice.** One question per unmatched conversation, four per call. The question text is the conversation name plus the preview. The options are the four choices above.
2. **Pass 2 — destinations.** Ask only for the conversations that chose option 1 or option 2, again four per call. For option 1, the options are the top candidate notes, and the user can type any other note through "Other". For option 2, ask for the folder, and take the note name from "Other".

A 16-conversation export must cost about 4 to 8 prompt calls, not 16. If you find yourself asking about one conversation, stopping, then asking about the next, you are doing this wrong. The one-at-a-time pattern is what made the 2026-08-09 run tedious.

### 2b. Choose the execution path — inline or parallel subagents

Routing is now settled. Every conversation that will be written has a target note. Before you extract anything, decide how to execute the writes.

**Build the work list.** One item per conversation with an action of `new` (that survived §2a as routed) or `update`. Conversations that are unchanged, decreased, ignored, or declined are not work items.

**Filter by owner first, in this session.** The entry point passed an `owners` map from the inventory. For any work item whose target note has `owner: human`, do not dispatch it. Handle it here: extract the conversation, print the diff of what you would have added, and record it as an owner conflict. A subagent cannot do this, because its report never reaches the user.

> **A target absent from `owners` is not an `owner: shared` target.** The map covers only the `writable_roots` in `_meta/vault-config.yml`. A folder outside that list can hold `owner: human` notes. If §2a routed a conversation to a note or folder that the map does not list, read that note's frontmatter here, in this session, before you decide. Never let a missing key default to writable.

**Group the remaining items by target note.** One group per distinct note. A group can hold several conversations. Grouping prevents two writers from touching one note's frontmatter at the same time.

**Apply the size floor first, then the threshold:**

```
MIN_DISPATCH_MESSAGES    = 10     # a group at or below this always writes inline
PARALLEL_INGEST_THRESHOLD = 5     # dispatchable groups; retune these two numbers
```

Split the groups in two before you count anything:

- **Small groups** — total `current_count` across the group is at or below
  `MIN_DISPATCH_MESSAGES`. These **always** write inline, however many items the
  run holds. They never count toward the threshold.
- **Dispatchable groups** — everything else. Only these are counted and only
  these are ever dispatched.

From DoxaLogosGit/obsidian-agent-vault, MIT.

Signals

Stars
0
Forks
0
Open issues
0
Last commit (this folder)
Sep 26, 2026 (1 month ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from DoxaLogosGit

Cite this page

obsidian-claude-export-ingest skill. Agent Mart. https://agentmart.dev/skills/doxalogosgit-obsidian-claude-export-ingest (last updated 2026-10-24)