feat(export): add --with-sources to wire Obsidian notes to source content (#1968) by SinghAman21 · Pull Request #1973 · Graphify-Labs/graphify · GitHub
Skip to content

feat(export): add --with-sources to wire Obsidian notes to source content (#1968) - #1973

Open
SinghAman21 wants to merge 3 commits into
Graphify-Labs:v8from
SinghAman21:fix/1968-obsidian-with-sources
Open

SinghAman21 wants to merge 3 commits into
Graphify-Labs:v8from
SinghAman21:fix/1968-obsidian-with-sources

Conversation

@SinghAman21

Copy link
Copy Markdown
Contributor

Wire Obsidian node notes to their source content (--with-sources) — #1968

Why

Right now every note in an exported Obsidian vault is a bare stub: frontmatter, the H1 title, and a ## Connections list of wikilinks. The source_file field exists in frontmatter but it's just a dead string — it doesn't link to anything. So anyone browsing the vault (or an agent hopping between notes) has to leave the vault entirely to learn what a node actually is. The graph structure is great, but the notes carry no substance.

Users were working around this with a post-processing script that copies sources in and rewires every note — but the exporter overwrites the notes on each re-export, so they had to re-run the script every single time.

What

A new opt-in flag on the Obsidian export:

graphify export obsidian --with-sources

With it, the vault becomes usable standalone:

  • Every distinct source_file is copied into the vault under sources/ as a real note (src.md).
  • Each node note gets a callout right under its title linking to that source:
  • Optional --source-root PATH to override where source paths are resolved from disk.

How

  • graphify/export.py — to_obsidian gains with_sources / source_root params plus four small helpers:
    • resolve each source_file (stored relative to the scan root) to a real file on disk;
    • flatten nested/Windows paths into one collision-safe note name;
    • copy markdown/text verbatim, and embed code/other files in a syntax-highlighted fence;
    • write one deduped note per distinct source, then insert the callout after each node's H1.
  • graphify/cli.py — parses the two new flags and defaults resolution to [scan root, out dir] (the out-dir fallback keeps older pre-0.9.17 graphs working).

Behavior chosen deliberately:

  • Source notes are graphify-owned, so the existing manifest/prune logic cleans them up automatically — a re-export without the flag removes them and the vault returns to plain notes.
  • A node whose source can't be found on disk (or exceeds a 2 MB cap) simply gets no callout rather than a dangling link.
  • Nodes sharing a source_file all link to the same single source note.

Conclusion

This solves the issue's core ask — turn source_file into a resolvable link and bring source content into the vault — without any manual post-processing. It's fully opt-in, so default exports are unchanged, and it plays cleanly with the existing re-export/prune machinery. Covered by a new test file (tests/test_obsidian_with_sources.py, 5 tests) with the existing export suite (91 tests) still green.

@SinghAman21

Copy link
Copy Markdown
Contributor Author

@SinghAman21

Copy link
Copy Markdown
Contributor Author

@safishamsi it will be great if you could review this, else PR will become stale and conflicts would occur thanks.

@SinghAman21

Copy link
Copy Markdown
Contributor Author

@nothariharan nothariharan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature works. I ran it end to end on a scratch project: graphify export obsidian --with-sources copies each source under sources/, markdown goes in verbatim, code goes in a fenced block with the right language hint, and every node note gets a > [!info] Source callout under the H1 with the original path as the alias. Re-exporting without the flag prunes the source notes and strips the callouts, so the manifest ownership behaviour holds. 439 existing export tests plus your 5 new ones pass on current v8.

Two asks:

1. CHANGELOG lands in the wrong section. The diff adds the entry under ## 0.9.18 (2026-07-17), which is already released. It needs to move to the current unreleased heading, otherwise it ships inside an old version's notes.

2. _resolve_source_path follows .. and absolute paths. Since source_file can hold an absolute path for a file outside the scan root, and the join is unclamped, I confirmed that a node with source_file="../secret.txt" results in that file's contents being embedded into the vault. Given the arbitrary-read hardening already in the Fortran extractor (GHSA-pcc4-rvhr-2pr8), I would clamp resolution to the scan root and skip anything that escapes it. It is opt-in so not urgent, but it is the kind of thing a reviewer here will always ask about.

Neither changes the design, which I like. Graphify-owned notes mean the existing prune machinery cleans up after the flag for free, and a re-export without the flag genuinely returns the vault to plain notes. The 2 MB cap and the no-callout-rather-than-dangling-link choice are both the right call.

@SinghAman21
SinghAman21 force-pushed the fix/1968-obsidian-with-sources branch from cd8550c to 3f9179e Compare October 20, 2026 18:40
…bs#1968)

Resolve source paths before copying and require the canonical path to remain beneath a selected scan root. This rejects traversal paths and symlinks that escape the scan tree. Add a regression test and record the feature under the current changelog section.
@SinghAman21
SinghAman21 force-pushed the fix/1968-obsidian-with-sources branch from 3f9179e to 3fe5828 Compare October 20, 2026 18:42
Reject source_file values that resolve outside the configured scan root, including absolute paths.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants