Flowmark is a Markdown auto-formatter, written in Python with an auto-synced Rust port, designed for better LLM workflows, clean git diffs, and flexible use from CLI, from IDEs, or as a library.
With AI tools increasingly producing Markdown, consistent and diff-friendly formatting has become essential. It improves collaborative editing and LLM workflows, especially when committing documents to git repositories.
For CLI auto-formatting, the Python and Rust builds produce identical output: the Rust port is a fast single native binary, while the Python version is the reference and is sometimes ahead on the newest features. Pick whichever fits your environment; for heavy or latency-sensitive formatting, choose the Rust binary.
Both Python and Rust versions are best installed with uv.
No install needed for one-off usage:
uvx flowmark-rs --help # For native-binary Rust
uvx flowmark-rs --auto somefile.md
uvx flowmark --help # For Python
uvx flowmark --auto somefile.mduv tool install --upgrade flowmark-rs # For native-binary Rust
uv tool install --upgrade flowmark # For Python
flowmark --auto somefile.md # One file
flowmark --auto . # Whole tree (respects .gitignore / .flowmarkignore)Run flowmark --help, flowmark --docs, or flowmark --skill for more.
Hand your agent this one instruction:
Set up Flowmark to keep this project’s Markdown auto-formatted. Run
uvx --from flowmark==0.7.2 flowmark --skillfor details.
Or run uvx --from flowmark==0.7.2 flowmark --install-skill to manually install the
skill into .agents/, .claude/, and AGENTS.md (see
How to Install the Skill).
For consistency across users and supply chain security, it’s recommended to pin the version when installing within a skill or project build.
Flowmark supports both CommonMark and GitHub-Flavored Markdown (GFM) via Marko.
The key differences from other Markdown formatters:
-
Carefully chosen default formatting rules that are effective for use in editors/IDEs, in agent pipelines, and also when paging through docs in a terminal.
-
Extensive Markdown feature support. “Just works” support including GFM-style tables, footnotes, YAML frontmatter, template tags (Markdoc, Jinja, Nunjucks), and inline HTML and HTML comments.
-
All line wrapping is Markdown-aware. Flowmark offers advanced and customizable line-wrapping capabilities, including semantic line breaks, a feature that is especially helpful in managing diffs and allowing collaborative edits on a Markdown document while avoiding git conflicts.
-
Optional typographic fixes such as automatic smart quotes for professional-looking typography.
-
Full-featured globbing, including git-ignore support.
-
A fast, exact Rust port of the Python reference implementation, compiled to a single native binary. With the Rust port’s caching feature, it can auto-format thousands of documents in milliseconds.
Some general philosophy:
-
Be conservative about changes so that it is safe to run automatically on save or after any stage of a document pipeline.
-
Be opinionated about sensible defaults but not dogmatic by preventing customization. You can adjust or disable most settings. And if you are using it as a library, you can fully control anything you want (including more complex things like custom line wrapping for HTML).
-
Be as small and simple as possible, with few dependencies.
The main ways to use Flowmark are:
-
To autoformat Markdown on save in VSCode/Cursor or any other editor that supports running a command on save. See below for recommended VSCode/Cursor setup.
-
As a command line formatter to format text or Markdown files using the
flowmarkcommand. -
As a library to autoformat Markdown from document pipelines. For example, it is great to normalize the outputs from LLMs to be consistent, or to run on the inputs and outputs of LLM transformations that edit text, so that the resulting diffs are clean.
-
As a more powerful drop-in replacement library for Python’s default
textwrapbut with more options. It simplifies and generalizes that library, offering better control over initial and subsequent indentation and when to split words and lines, e.g. using a word splitter that won’t break lines within HTML tags, template tags ({% %},{# #},{{ }}), Markdown links (including links with multi-word text), inline code spans (`code with spaces`), or HTML comments. Seewrap_paragraph_lines.
Tip
For an example of what an auto-formatted Markdown doc looks with semantic line breaks looks like, see the Markdown source of this readme file.
Some Markdown auto-formatters never wrap lines, while others wrap at a fixed width.
Flowmark supports both, via the --width option.
Default line wrapping behavior is 88 columns. The “90-ish columns” compromise was popularized by Black and also works well for Markdown.
However, in addition, unlike traditional formatters, Flowmark also offers the option to use a heuristic that prefers line breaks at sentence boundaries. This is a small change that can dramatically improve diff readability when collaborating or working with AI tools.
This idea of semantic line breaks, which is breaking lines in ways that make sense logically when possible (much like with code) is an old one. But it usually requires people to agree on how to break lines, which is both difficult and sometimes controversial.
However, now we are using versioned Markdown more than ever, it’s a good time to revisit this idea, as it can make diffs in git much more readable. The change may seem subtle but avoids having paragraphs reflow for very small edits, which does a lot to minimize merge conflicts.
This is my own refinement of traditional semantic line breaks. Instead of just allowing you to break lines as you wish, it auto-applies fixed conventions about likely sentence boundaries in a conservative and reasonable way. It uses simple and fast regex-based sentence splitting. While not perfect, this works well for these purposes (and is much faster and simpler than a proper sentence parser like SpaCy). It should work fine for English and many other Latin/Cyrillic languages, but hasn’t been tested on CJK. You can see some old discussion of this idea with the markdownfmt author.
While this approach to line wrapping may not be familiar, I suggest you just try
flowmark --auto on a document and you will begin to see the benefits as you
edit/commit documents.
This feature is enabled with the --semantic flag or the --auto convenience flag.
Flowmark offers optional automatic smart quotes to convert "non-oriented quotes" to “oriented quotes” and apostrophes intelligently.
This is a robust way to ensure Markdown text can be converted directly to HTML with professional-looking typography.
Smart quotes are applied conservatively and won’t affect code blocks, so they don’t break code snippets. It only applies them within single paragraphs of text, and only applies to ' and " quote marks around regular text.
This feature is enabled with the --smartquotes flag or the --auto convenience flag.
There is a similar feature for converting ... to an ellipsis character … when it
appears to be appropriate (i.e., not in code blocks and when adjacent to words or
punctuation).
This feature is enabled with the --ellipses flag or the --auto convenience flag.
Because YAML frontmatter is common on Markdown files, any YAML frontmatter (content
between --- delimiters at the front of a file) is always preserved exactly.
YAML is not normalized.
Tip
See the frontmatter format repo for more discussion of YAML frontmatter and its benefits.
Flowmark can be used as a library or as a CLI.
# Format all Markdown files in current directory recursively
flowmark --auto .
# Format a single file in-place with all auto-formatting options
flowmark --auto README.md
# List files that would be formatted (without formatting)
flowmark --list-files .
# Format to stdout
flowmark README.md
# Format from stdin (use '-' explicitly)
echo "Some text" | flowmark -The simplest way to format all Markdown in a project:
flowmark --auto .This recursively discovers all .md files, skips common non-content directories
(node_modules, .venv, build, etc.), respects .gitignore, and formats everything
in-place with semantic line breaks, smart quotes, ellipses, and cleanups.
For a legacy alternative (pre-v1.0 behavior):
find . -name "*.md" -exec flowmark --auto {} \;The main flags:
| Flag | Description |
|---|---|
-o, --output FILE |
Output file (use - for stdout) |
-w, --width WIDTH |
Line width (default: 88, 0 = disable wrapping) |
-p, --plaintext |
Process as plaintext (no Markdown parsing) |
-s, --semantic |
Semantic (sentence-based) line breaks |
-c, --cleanups |
Safe cleanups (unbold headings, etc.) |
--smartquotes |
Convert straight quotes to typographic quotes |
--ellipses |
Convert ... to … |
--list-spacing |
Control list spacing: preserve, loose, tight |
-i, --inplace |
Edit in place |
--nobackup |
Skip .orig backup with --inplace |
--check |
Don’t write; exit non-zero if any file would be reformatted (for CI / pre-commit) |
--auto |
All auto-formatting: --inplace --nobackup --semantic --cleanups --smartquotes --ellipses. Requires file/directory args (use . for current directory) |
File discovery flags:
| Flag | Description |
|---|---|
--list-files |
Print resolved file paths, don’t format |
--extend-include PATTERN |
Additional file patterns (e.g., *.mdx) |
--exclude PATTERN |
Replace all default exclusions |
--extend-exclude PATTERN |
Add to default exclusions (e.g., drafts/) |
--no-respect-gitignore |
Disable .gitignore integration |
--force-exclude |
Apply exclusions (incl. .flowmarkignore) to explicitly-named files too (for pre-commit) |
--files-max-size BYTES |
Skip files larger than this (default: 1 MiB) |
When you pass a directory to Flowmark (e.g., flowmark --auto .), it recursively
discovers files using a smart filter pipeline:
-
Default includes: Only
*.mdfiles by default. Use--extend-include "*.mdx"to add patterns. -
Default exclusions: ~45 directories are automatically skipped, including
.git,node_modules,.venv,venv,__pycache__,build,dist,.tox,.nox,.idea,.vscode,vendor,third_party, and more. These directories are pruned during traversal for performance. -
.gitignoreintegration: Enabled by default. Reads.gitignoreat every directory level during traversal. Disable with--no-respect-gitignore. -
.flowmarkignore: A tool-specific ignore file using gitignore syntax. Place it in your project root to exclude paths specific to Flowmark formatting.
Exclusions (default patterns, --exclude/--extend-exclude, .gitignore, and
.flowmarkignore) apply when Flowmark discovers files by walking a directory or glob.
Files named explicitly on the command line override exclusions by default (matching
Black and Ruff), so flowmark README.md always formats that file.
Pass --force-exclude to apply exclusions to explicitly-named files too — this is what
the pre-commit hooks set, since pre-commit passes every changed file by name.
- Max file size: Files over 1 MiB are skipped by default.
Change with
--files-max-size(0 = no limit).
The exclusions above apply when Flowmark discovers files (you pass a directory or glob). But when you name a file explicitly on the command line, Flowmark formats it by default even if it matches an exclusion — naming a file is taken as “I mean this one”.
This matches how Black and Ruff behave, and it exists for the same reason: tools like pre-commit pass every changed file as an explicit argument, so a formatter that always honored exclusions on explicit files would be surprising on the command line, while one that never did would reformat files you deliberately ignored in pre-commit.
The --force-exclude flag resolves this: with it, all exclusion sources
(.flowmarkignore, --exclude/--extend-exclude, and the built-in defaults) are
applied to explicitly-named files too.
This is why Flowmark’s published pre-commit hooks set
--force-exclude — exactly as
ruff-pre-commit does — so your
.flowmarkignore is respected on the staged files pre-commit hands the hook.
# Also format .mdx files
flowmark --auto --extend-include "*.mdx" .
# Skip a specific directory
flowmark --auto --extend-exclude "drafts/" .
# Replace ALL default exclusions with your own
flowmark --auto --exclude "my_custom_dir/" .
# Debug: see exactly which files would be formatted
flowmark --list-files .When passing glob patterns as arguments, always quote them so Flowmark can handle expansion internally:
# Correct: Flowmark expands the glob (** works for recursive matching)
flowmark --auto 'docs/**/*.md'
# Risky: shell may expand ** incorrectly if globstar is off (the default in bash)
flowmark --auto docs/**/*.mdWithout quoting, the shell may expand ** as a single * (matching only one directory
level) or pass nothing if there are no matches.
Flowmark uses Python’s pathlib.Path.glob() internally, which always supports ** for
recursive matching regardless of shell settings.
Note: The --extend-include and --extend-exclude flags use gitignore-style patterns
(e.g., *.mdx, drafts/), not shell globs.
During recursive directory traversal, symlinks are not followed. This prevents infinite loops from circular symlinks and avoids accidentally formatting files outside the project tree.
However, if you pass a symlink explicitly as an argument (e.g.,
flowmark --auto link-to-readme.md), the symlink is resolved and the target file is
processed.
Flowmark supports TOML-based configuration files. It searches for config files in this order (first match wins, walking up directories):
.flowmark.tomlflowmark.tomlpyproject.toml(only if it has a[tool.flowmark]section)
# flowmark.toml (or .flowmark.toml)
[formatting]
width = 100
semantic = true
smartquotes = true
ellipses = true
list-spacing = "preserve"
[file-discovery]
extend-include = ["*.mdx", "*.markdown"]
extend-exclude = ["drafts/", "archive/"]
files-max-size = 2097152 # 2 MiBOr in pyproject.toml:
[tool.flowmark]
width = 100
semantic = true
extend-exclude = ["drafts/"]The --auto flag is a fixed formatting preset that always enables --semantic,
--cleanups, --smartquotes, and --ellipses. It ignores formatting settings from
config files.
However, width and file discovery settings (excludes, max size, etc.)
are always read from config regardless of --auto.
When not using --auto, all formatting settings can be configured via the config file
and overridden by explicit CLI flags.
Flowmark is a flexible Python library, not just a CLI. Add it with uv add flowmark (or
pip install flowmark) and use the high-level helpers or the lower-level building
blocks.
Format Markdown text or files with the same engine as the CLI:
from flowmark import reformat_text, reformat_file
# Normalize a Markdown string (semantic line breaks on by default; opt into typography)
clean = reformat_text(messy_markdown, smartquotes=True, ellipses=True)
# Reformat a file in place, atomically (pass output=None with inplace=True)
reformat_file("README.md", None, inplace=True, semantic=True)Use it as a smarter textwrap. wrap_paragraph / wrap_paragraph_lines (with the
Wrap enum) generalize the stdlib textwrap with control over initial vs.
subsequent indentation and pluggable word splitters that never break inside Markdown
links, code spans, HTML/template tags, or URLs.
Inspect Markdown inline structure with the public inline API (new in v0.7.0), exposed so downstream tools can reuse Flowmark’s own primitives instead of re-implementing them:
from flowmark import flowmark_markdown, extract_links
doc = flowmark_markdown().parse(markdown_text)
for link in extract_links(doc): # -> list[Link(text, url, title)], reference links resolved
print(link.text, link.url)flowmark.markdown_ast:walk_elements,extract_links, theLinktype, andblock_spanfor AST-aware inspection of a parsed document.flowmark.atomic_spans: the atomic-construct patterns Flowmark uses internally (code spans, links, autolinks, bare URLs, HTML/Jinja tags), the offset-preserving tokenizersiter_atomic_spans/iter_atomic_words, and the atomic-aware sentence splittersplit_sentences_with_spans/split_sentences_atomic.
Map parsed blocks back to source. Every block element produced by
flowmark_markdown().parse(text) carries an authoritative element.span = (start, end)
half-open offset pair, recorded straight from marko’s parser state (no regex, no
heuristic) at every nesting level.
Offsets index the source after marko’s \r\n -> \n normalization, so slice against an
LF-normalized copy of the input:
from flowmark import flowmark_markdown
from flowmark.markdown_ast import block_span
source = markdown_text.replace("\r\n", "\n")
doc = flowmark_markdown().parse(source)
for block in doc.children:
start, end = block_span(block)
print(type(block).__name__, source[start:end])You can use Flowmark to auto-format Markdown on save in VSCode or Cursor.
Install the “Run on Save” (emeraldwalk.runonsave) extension.
Then add to your settings.json:
"emeraldwalk.runonsave": {
"commands": [
{
"match": "(\\.md|\\.md\\.jinja|\\.mdc)$",
"cmd": "flowmark --auto ${file}"
}
]
}The --auto option is just the same as
--inplace --nobackup --semantic --cleanups --smartquotes --ellipses.
For batch formatting an entire project, use flowmark --auto . from the terminal.
To keep a repo’s Markdown consistently formatted across contributors and CI, pin a flowmark version and wire it into your existing build/hook plumbing. The same pattern works whether you reach for the Python build or the Rust port.
Avoid unpinned flowmark@latest: different contributors then silently run different
versions and produce noisy diffs.
- Rust port (fastest): install the
flowmark-rsbinary at a specific release. Identical formatting to the Python version; great when speed matters in hooks/CI. - Python via
uvx(zero-install): invoke asuvx --from flowmark==<X.Y.Z> flowmark --auto. First call caches the wheel; subsequent calls are fast. - Python tool install:
uv tool install flowmark==<X.Y.Z>(orpip install flowmark==<X.Y.Z>in a venv) putsflowmarkonPATH.
A single command everyone (and CI) runs. Makefile target:
FLOWMARK := uvx --from flowmark==0.7.2 flowmark
format-docs:
$(FLOWMARK) --auto .Or as an npm script in package.json:
{
"scripts": {
"format:docs": "uvx --from flowmark==0.7.2 flowmark --auto ."
}
}lefthook example (lefthook.yml):
pre-commit:
commands:
flowmark:
glob: "*.{md,mdc,markdown}"
run: uvx --from flowmark==0.7.2 flowmark --auto {staged_files}
stage_fixed: trueFlowmark also ships pre-commit hooks, so you can use it
directly from your .pre-commit-config.yaml without writing a local hook:
repos:
- repo: https://github.com/jlevy/flowmark
rev: v0.7.2
hooks:
- id: flowmark # auto-format Markdown in place
# - id: flowmark-check # or: check only, fail if files would change (for CI)These run via the pre-commit framework (not GitHub-specific):
install it once with pre-commit install, and it builds Flowmark in an isolated
environment on first use — no global install or extra dependency needed.
Both hooks pass --force-exclude, so your .flowmarkignore and other exclusions are
respected on the staged files pre-commit hands them (the same approach ruff-pre-commit
uses); the flowmark-check hook also mirrors --auto so it validates exactly what the
auto-fix hook would write.
A husky setup works the same way; the key is the pinned invocation.
Use --check (or the flowmark-check pre-commit hook) to fail if anything would
change, without writing.
Pair it with --auto so CI validates the same formatting the auto-fix path applies:
- run: uvx --from flowmark==0.7.2 flowmark --auto --check .Add a .flowmarkignore (same syntax as .gitignore) so batch formatting only touches
files you own:
docs/api/_generated/
attic/
third_party/
flowmark --auto . always respects .flowmarkignore and .gitignore. For editor-side
on-save formatting, see Use in VSCode/Cursor above.
Flowmark is built to be the default Markdown auto-formatter for agent workflows. Its deterministic, diff-friendly output and semantic line breaks keep LLM-generated and LLM-edited Markdown clean in git, and the Rust port makes it fast enough to run on every save or every agent turn. It works with any agent that can run a shell command, and ships a SKILL.md so capable agents discover when to use it on their own.
There are three install paths, ordered by what most users want first:
1. Cross-agent package manager (no flowmark prerequisite). If you just want the
skill on disk for any supported agent and don’t already have flowmark, use the
skills.sh installer.
It copies the published discovery copy into .agents/skills/ and symlinks it into each
agent’s native location (Claude Code, Codex, Cursor, Copilot, Gemini, …). The discovery
copy bootstraps its own pinned uvx invocation, so no prior flowmark install is
required:
npx skills add jlevy/flowmark2. Direct install via the flowmark CLI (recommended once you have flowmark). Run
from the repo root.
By default this writes all three project-local surfaces: the portable
.agents/skills/flowmark/ (read by Codex, Gemini CLI, pi, and others), the
.claude/skills/flowmark/ mirror (Claude Code reads only that path), and a compact
marker-bounded block in AGENTS.md:
flowmark --install-skill # all three surfaces (default)
flowmark --install-skill --surfaces=portable,agents-md # skip the Claude mirror
flowmark --install-skill --surfaces=claude # only the Claude mirror
flowmark --install-skill --agent-base ~/.claude # single explicit base (global)The --surfaces flag is a comma-separated subset of portable, claude, agents-md,
or the all alias. Installs are idempotent (re-running an up-to-date install changes
nothing), version-pinned to the installed flowmark, and generated files are marked
DO NOT EDIT. A forward-compat guard refuses to clobber any artifact stamped with a
newer format than this build understands.
3. Manual copy from the public discovery copy. Every release publishes a
spec-compliant SKILL.md at the repo root:
skills/flowmark/SKILL.md.
You can drop it into your project at .agents/skills/flowmark/SKILL.md (and mirror to
.claude/skills/flowmark/SKILL.md for Claude Code).
Useful in air-gapped or no-Node-no-Python environments.
Flowmark is also indexed automatically by GitHub-scraping skill discoverers (SkillsMP,
ClaudeSkills.info, LobeHub, claudemarketplaces) just by being a public repo with a
SKILL.md, with no extra setup.
| Flag | Description |
|---|---|
--skill |
Print the composed skill (SKILL.md content) |
--install-skill |
Install the flowmark skill (project-local cross-agent by default) |
--surfaces LIST |
Subset of portable, claude, agents-md, or all (default) |
--agent-base DIR |
Install to a single explicit base dir (e.g. ~/.claude); incompatible with --surfaces |
--docs |
Print full documentation |
Any agent with a shell can call Flowmark directly, no skill required:
# Format with all auto-formatting options
flowmark --auto README.md
# Preview formatted output
flowmark README.md
# Format LLM output (use '-' for stdin)
echo "$llm_output" | flowmark --semantic -In ephemeral or cloud agent environments where nothing is installed, run it via a version-pinned zero-install runner (pin the version so the agent can’t silently pull a newer release):
uvx --from flowmark==<version> flowmark --auto README.md # Python
# or use the Rust binary (flowmark-rs) for maximum speedThere are several other Markdown auto-formatters. All of these are worth looking at, but none offer the more advanced line-breaking features of Flowmark or have the “just works” CLI defaults and library usage I found most useful.
-
dprint-plugin-markdown is a Markdown plugin for dprint, the fast Rust/WASM engine. It is a good, modern option but does not auto-apply semantic line breaks.
-
markdownfmt is one of the oldest and most popular Markdown formatters and works well for basic formatting.
-
mdformat is probably the closest alternative to Flowmark and it also uses Python. It preserves line breaks in order to support semantic line breaks, but does not auto-apply them as Flowmark does and has somewhat different features.
-
Prettier is the ubiquitous Node formatter that handles Markdown/MDX.
-
Rule-based linters like markdownlint-cli2 catch violations or sometimes fix, but tend to be far too clumsy in my experience.
-
Finally, the remark ecosystem is by far the most powerful library ecosystem for building your own Markdown tooling in JavaScript/TypeScript. You can build auto-formatters with it but there isn’t one that’s broadly used as a CLI tool.
On speed, Flowmark’s auto-synced Rust port (flowmark-rs) compiles to a single native binary and is among the fastest Markdown formatters available, in the same performance class as Rust-based tools like dprint, while keeping the same formatting behavior as the Python reference implementation. So you get Flowmark’s formatting either way: the Python library/CLI for flexibility and embedding, or the Rust binary when you want maximum CLI speed (large repos, hot paths, latency-sensitive agent loops).
For development workflows, see development.md.