Select Markdown sections by heading path.
mdselect is a small, zero-runtime-dependency command-line tool for inspecting
a Markdown outline and extracting complete sections without losing their
original formatting.
Line-oriented tools such as grep can find a heading, but they do not know
where that section ends. A full Markdown AST library understands much more of
the format, but usually requires writing a program and choosing rendering
semantics.
mdselect fills the gap:
- query the heading hierarchy directly from the shell;
- return a matched heading together with its body and descendants;
- preserve the original Markdown content without parsing and re-rendering it;
- use regular expressions in a compact heading path expression;
- run with only the Python standard library.
mdselect requires Python 3.10 or newer.
pipx install mdselect
# or
uv tool install mdselect
# or
python -m pip install mdselectTo install a development checkout:
python -m pip install -e ".[test]"This repository also provides the recall-markdown-memory skill as an
installable Codex plugin. The skill uses the mdselect executable from the
task environment; installing the plugin does not install Python or the PyPI
package automatically.
Install the CLI first and verify that it is on PATH:
pipx install mdselect
mdselect --versionThen add the repository marketplace and install the plugin:
codex plugin marketplace add stevenybw/mdselect
codex plugin add mdselect@mdselectStart a new Codex session after installation. Invoke the skill explicitly with
$mdselect:recall-markdown-memory, or ask Codex to recall a decision, preference,
or other context from Markdown memory and let the skill trigger automatically.
The first release targets shell-enabled Codex CLI, IDE, and local desktop tasks. For a Codex Cloud environment, add CLI installation to its setup script, for example:
python -m pip install mdselectThe skill is read-only. It searches only user-specified or project-configured
memory locations, plus the conventional MEMORY.md, memory/, and
.agents/memory/ paths; it does not scan every Markdown file by default.
Given handbook.md:
# Engineering Handbook
## Getting Started
Introductory material.
### Local Setup
Create a virtual environment.
## Operations
Operational notes.Print its outline:
mdselect outline handbook.md# Engineering Handbook
## Getting Started
### Local Setup
## Operations
Extract the complete Getting Started subtree:
mdselect select --mpath '/Engineering/Getting Started' handbook.md## Getting Started
Introductory material.
### Local Setup
Create a virtual environment.Add a trailing slash to omit the matched heading line:
mdselect select --mpath '/Engineering/Getting Started/' handbook.mdAn mpath is a slash-separated heading path:
| Form | Meaning |
|---|---|
/Guide/Install |
Absolute query: the first segment only matches level-one headings. |
Install |
Relative query: the first segment matches headings at any level. |
/^Guide$/^Install$/ |
Each segment is a Python regular expression; anchors make matches exact. The trailing slash omits the matched heading. |
/ |
Return the original document unchanged. |
The complete rules are:
- Each non-empty segment is compiled as a Python regular expression and
matched with
re.search. - A leading slash makes the query absolute: its first segment is tested only
against level-one (
#) headings. - Without a leading slash, the first segment is tested against every heading.
- Every later segment is tested against descendants of the current matches, at any depth.
- A trailing slash excludes the final matched heading line while keeping its body and descendants.
/,//, and an empty expression are no-ops that return the original document.- A slash cannot appear inside a segment.
- Multiple matches retain document order and are separated by one blank line.
- When both an ancestor and its descendant match, only the ancestor is rendered as a result because its subtree already contains the descendant.
mdselect select -h prints these rules without requiring the README.
| Situation | Status |
|---|---|
| Successful outline or selection | 0 |
| No matching section | 0, with empty standard output |
| Invalid arguments or regular expression | 2 |
| File cannot be read | non-zero |
The no-match behavior is intentional so selection can be used in shell pipelines without treating an optional section as a command failure.
- ATX headings from
#through######are supported when the marker starts in column one and is followed by whitespace. - Optional closing hash markers are removed from the heading title used for matching.
- Backtick and tilde fenced code blocks are recognized; heading-like lines inside a fence are ignored.
- Selected content is rendered from the original lines, preserving tables, blockquotes, blank lines, inline markup, and fences.
- Files are read as UTF-8.
- Setext headings are not supported.
mdselectdoes not interpret front matter, links, inline Markdown, or HTML as an AST.
| Tool | Best for | Structural section extraction |
|---|---|---|
grep / rg |
Finding matching lines across files | No |
| Markdown AST library | Full parsing, transformation, and custom rendering | Yes, with application code |
mdselect |
Shell-friendly outline and heading-subtree selection | Yes, directly from the CLI |
Use grep or rg to discover candidate files, then use mdselect when
the unit you need is a Markdown section rather than a line.
from mdselect import outline, select
print(outline(markdown_text))
section = select(markdown_text, "/Guide/Install")The package also exports Section, parse, render_subtree, and
InvalidPattern.
See CONTRIBUTING.md for development and pull request guidance. Report security issues privately as described in SECURITY.md.
MIT