CLI tool
zoni/obsidian-export avatar
zoni/obsidian-export

obsidian-export: turning an Obsidian vault into plain CommonMark

Rust library and CLI to export an Obsidian vault to regular Markdown

1,340 stars95 forksRustNOASSERTION

At a glance

What is it?
A Rust library and command line tool that recursively exports an Obsidian vault to regular Markdown, resolving wiki-style links and file embeds into CommonMark, honouring gitignore-style exclusions and frontmatter tags. The design choices are the interesting part, particularly the decision to treat the destination as a CommonMark target rather than as a lossy Markdown approximation.
Who is it for?
obsidian-export suits someone moving notes out of Obsidian for a static site generator, a search index or another editor, and who wants the output to be valid CommonMark rather than a bag of wiki syntax. It does not suit someone who wants Obsidian's flavour preserved, since the README says it supports most but not all of Obsidian's Markdown flavour and is not officially endorsed by the Obsidian team.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What it exports and what it resolves

Obsidian Export is a CLI program and a Rust library to export an Obsidian vault to regular Markdown. The README lists what that means in practice.

It recursively exports Obsidian Markdown files to CommonMark, so the target format is the CommonMark specification rather than a dialect of Markdown.

It supports `[[note]]`-style references as well as `![[note]]` file includes. That is the substantive feature. A wiki link becomes a real Markdown link, and an embed, which in Obsidian is a transclusion that inlines the content of another note, becomes either an inline block or a link depending on the target. Without that, an exported vault is a directory of files with unresolvable placeholders in them.

It supports gitignore-style exclude patterns, with the default ignore file named `.export-ignore`, and it automatically excludes files that are ignored by Git when the vault is located in a Git repository. That second one is a nice touch for anyone who keeps their vault in version control, since the private notes and the large attachments that are in `.gitignore` are excluded without a second configuration file.

It runs on all major platforms, Windows, Mac, Linux and the BSDs.

The README is careful about two limits. It is not officially endorsed by the Obsidian team, and it supports most but not all of Obsidian's Markdown flavour.

The project is by Nick Groenen, the primary language in the metadata is Rust, the default branch is main, and the last push was on 2026-10-13. The releases are versioned by year and month, v25.3.0 from 2025-03-25, v24.11.0 from 2024-11-23 and v23.12.0 from 2023-12-03, which is a calendar versioning scheme for a tool whose compatibility surface changes with the Markdown conventions it targets.

Installing, and the two ways to upgrade

Pre-compiled binaries for all major platforms are available on the releases page, and the README notes that these releases are also suitable for installation with cargo-binstall.

Building from source is for the case where binary releases are unavailable for your platform, or where you do not trust the pre-built binaries, and the README says it can be compiled with relatively little effort. The steps are to install the Rust toolchain and then run:

bash
cargo install obsidian-export

There is a note about the PATH variable, which is the usual Rust first-install advice, and a second note about a pre-built binary downloaded to a location not on the PATH, in which case you have to give the full path to the binary.

Upgrading is described the same way for both installation methods. If you downloaded a pre-built binary, upgrade by downloading the latest version to replace the old one. If you built from source, upgrade by running `cargo install obsidian-export` again.

That symmetry is the practical advantage of the cargo install route for a tool that targets several platforms, because a binary you downloaded has to be replaced by hand each time.

The package manifest also declares the library alongside the binary, with a `[lib]` section named `obsidian_export` pointing at `src/lib.rs` and a `[[bin]]` section named `obsidian-export` pointing at `src/main.rs` with `doc = false`. The library is published on docs.rs under the `obsidian-export` package name, so the exporter can be embedded in another Rust program rather than only shelled out to.

Running it, and the two modes that surprise people

The main interface is the `obsidian-export` command, which has to be run from a terminal or Windows PowerShell, and the README assumes basic familiarity with command line interfaces.

The basic form takes a source and a destination:

bash
obsidian-export /path/to/my-obsidian-vault /path/to/exported-notes/

This exports all the files from the vault to the destination, except those listed in `.export-ignore` or `.gitignore`. Two caveats follow. The destination directory must exist, so you may need to create an empty one first, and if you give it an existing directory, files under it may be overwritten.

Individual files can be exported too:

bash
obsidian-export my-obsidian-vault/some-note.md /tmp/export/
obsidian-export my-obsidian-vault/some-note.md /tmp/exported-note.md

And here is the surprise the README spells out. In that mode, obsidian-export sees `some-note.md` as being the only file that exists in the vault, so references to other notes are not resolved. The README says this is by design.

The reason is that resolution needs the vault. If the tool is given a single file as the source, it has nothing to resolve against, and pretending otherwise would produce links to nothing. The design choice is to fail in a defined way rather than to guess.

The third mode is the answer for anyone who wants one note with its links resolved. The `--start-at` argument exports a subset of the vault while keeping the rest of the vault visible to the resolver, so references to files outside the exported set still work. In that mode all notes under the source are considered part of the vault, so any references to them remain intact even when they are not part of the exported notes.

That three-way distinction, whole vault, single file, and subset with `--start-at`, is the part of the interface worth understanding before you script it.

Exclusions, tags and the fate of links to skipped notes

The ignoring section is where the tool's behaviour gets interesting, because skipping a file has consequences for the files that referenced it.

Five kinds of file are excluded by default, each with a flag to change it. Hidden files, adjustable with `--hidden`. Files matching a pattern in `.export-ignore`, adjustable with `--ignore-file`. Any files ignored by Git, adjustable with `--no-git`. And then the tag filters, where `--skip-tags foo --skip-tags bar` skips any files that have the tags foo or bar in their frontmatter, and `--only-tags foo --only-tags bar` skips any files that do not have those tags.

The consequence is stated next, and it is the detail that matters for a vault with notes pointing in both directions. Notes linking to ignored notes will be unlinked, meaning they keep only the link text, and embeds of ignored notes are skipped entirely.

So an excluded note does not leave a broken reference behind, it leaves a sentence with the link text and no target, and a transclusion that silently becomes nothing. That is a defensible choice for a publishing pipeline, and it is also a silent one, which is why the tag filters need to be used with some care.

The `.export-ignore` syntax is identical to gitignore files, and the README gives an example that shows the cases, an anchored path for a directory at the top of the export tree, a bare name that matches a file or directory called test anywhere, a glob for PDF files, and a negation to include one specific PDF back in.

For anyone who has written a `.gitignore`, the syntax needs no explanation, and the README points at the gitignore manpage for the full documentation.

Frontmatter, encodings and recursive embeds

Three behaviours that are worth knowing about before you run the tool on a vault you care about.

Frontmatter is copied over as-is by default. The README then explains the two adjustments you are likely to need. Some static site generators are picky about frontmatter and require it to be present, and some get tripped up when Markdown files have no frontmatter but start with a list item or a horizontal rule, and `--frontmatter=always` inserts an empty frontmatter entry to satisfy them. To remove frontmatter from the exported notes entirely, `--frontmatter=never` does that.

Character encodings are more of a constraint. At present UTF-8 is assumed for all note text and for filenames, and all text and file handling performs lossy conversion to Unicode strings. The README warns that using non-UTF-8 encodings may lead to issues like incorrect text replacement and failure to find linked notes, and says that while this may change in the future, there are no plans to change the behaviour in the short term.

The lossy part is the point to notice. A vault with Latin-1 encoded notes will export with replacement characters, and a filename that is not valid UTF-8 will not be found, which means its links will not resolve either. The `unicode-normalization` dependency handles normalisation within UTF-8, which is a different problem from the encoding assumption.

Recursive embeds are the third. It is possible to end up with recursive embeds when two notes embed each other, for example when a Note A contains `![[Note B]]` and Note B contains `![[Note A]]`, and by default this triggers an error.

A cycle in a transclusion graph is a real situation in a vault where notes quote each other, and the tool chooses to stop rather than to expand forever, which is the only safe default.

The repository, and the tooling behind the project

The repository layout shows a project with a real release process rather than a script someone runs locally.

The README itself is generated. A comment at the top says not to edit it directly, that it is automatically generated from files in the `docs` directory, and that the way to change it is to edit the corresponding Markdown files there and run `generate.sh`, and to add new sections by creating new files under `docs` and adding them to `_combined.md`.

There is a `Justfile` rather than a Makefile, a `cliff.toml` for git-cliff, a `towncrier.toml` with a `changelog.d/` directory for the changelog fragments, a `deny.toml` for cargo-deny, and a `dist-workspace.toml` for cargo-dist, which is the release tooling that produces the pre-built binaries. There is a `rust-toolchain.toml`, a `rustfmt.toml` and a `.pre-commit-config.yaml`.

The `Cargo.toml` is where the design shows. The dependencies are the exact set the job needs and not much more. `ignore` at 0.4.22 does the gitignore-style walking, `pulldown-cmark` and `pulldown-cmark-to-cmark` parse and re-serialise CommonMark, `serde_yaml` handles frontmatter, `regex` and `slug` handle matching and filenames, `percent-encoding` and `pathdiff` handle URLs and paths, `snafu` provides the error types, `eyre` is the error reporting, `filetime` preserves timestamps, and `rayon` is there for parallelism across files.

The lint configuration is the other part worth reading. The rustc lints include nonstandard_style, rust_2018_idioms and rust_2024_compatibility, plus noop_method_call, redundant-lifetimes, unsafe_op_in_unsafe_fn and unused_qualifications, and the clippy configuration enables the pedantic and nursery groups. Two clippy lints are explicitly allowed, missing_errors_doc and missing_panics_doc, with a comment saying they should probably be changed back to warn in the future but that it is a low-priority issue at the moment. That comment is a small and honest piece of project history.

One oddity: the package edition is 2018, while the lint set includes rust_2024_compatibility. The code is on an old edition but is being checked against the newest one's compatibility rules.

Editorial conclusion

obsidian-export suits someone moving notes out of Obsidian for a static site generator, a search index or another editor, and who wants the output to be valid CommonMark rather than a bag of wiki syntax. It does not suit someone who wants Obsidian's flavour preserved, since the README says it supports most but not all of Obsidian's Markdown flavour and is not officially endorsed by the Obsidian team. Before adopting it, read the encoding section, because UTF-8 is assumed for all note text and filenames and the conversion is lossy, and check the tag filtering behaviour, since notes linking to a skipped note are unlinked down to their link text and their embeds are dropped entirely.

Frequently asked questions

What does obsidian-export do with wiki links and embeds?

It supports `[[note]]` style references and `![[note]]` file includes, and it exports the result as CommonMark, so the vault leaves as regular Markdown rather than as a directory of Obsidian-specific syntax.

How do I export only part of an Obsidian vault?

Use the `--start-at` argument with the vault root as the source. All notes under the source are treated as part of the vault, so references to files outside the exported set still resolve, unlike exporting a single file, where references are not resolved by design.

What happens to links pointing at files that are excluded from the export?

Notes linking to ignored notes are unlinked, keeping only the link text, and embeds of ignored notes are skipped entirely. Exclusions come from hidden files, the `.export-ignore` file, files ignored by Git, and the `--skip-tags` and `--only-tags` frontmatter filters.

Does obsidian-export handle non-UTF-8 vaults?

Not reliably. UTF-8 is assumed for all note text and for filenames, and the conversion is lossy, so the README warns of incorrect text replacement and failure to find linked notes, and says there are no plans to change that in the short term.

How is obsidian-export licensed and released?

The manifest declares BSD-2-Clause-Patent, with a LICENSE file at the repository root. Releases are pre-built binaries published on GitHub, versioned by year and month, and the project also builds from source with `cargo install obsidian-export`.

Official sources

  1. Issues
  2. README
  3. Releases
  4. zoni/obsidian-export on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/zoni-obsidian-export.svg)](https://hysenlabs.com/projects/zoni-obsidian-export)

Was this useful?