This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
mdflow (md) is a CLI tool that executes AI agents defined as markdown files. It parses YAML frontmatter for configuration and passes keys directly as CLI flags to the specified command (claude, codex, gemini, copilot, or any other CLI tool).
md <file.md> [flags] # Run a flow
md init [-e <eng>] [-y] # Initialize a flow roster (guided by an agent CLI; -y scaffolds)
md create [name] # Create a new flow file
md explain <flow.md> # Show resolved config without executing (free)
md eval <flow.md> # Run the flow's eval suite (costs engine turns)
md complain <flow.md> "msg" # Record evolution evidence (free)
md evolve <flow.md> # Evidence-gated prompt evolution (--check is free)
md install <url|gh:...> # Install a flow into the registry (see src/registry.ts)
md remove <name> # Remove an installed registry flow
md list # List installed registry flows
md setup # Configure shell (PATH, aliases)
md logs # Show flow log directory
md help # Show help# Run tests (bail on first failure)
bun test --bail=1
# Run single test file
bun test src/cli.test.ts
# Run a specific test by name
bun test --test-name-pattern "parses command"
# Execute the CLI directly
bun run src/index.ts task.claude.md
# Or using the alias
bun run md task.claude.mdThe mdflow.dev landing page lives in site/ (Vite + React, deployed via
Vercel with Root Directory = site). Rules:
site/src/facts.jsonis generated byscripts/generate-facts.tsfrom the adapter registry,DEFAULT_ENGINE,cli-runner.tssubcommands, and package.json — never edit it by hand.bun run factsregenerates;bun run facts:checkruns in CI and fails on drift. Adding a subcommand tocli-runner.tsrequires a matchingCOMMAND_DOCSentry in the generator.- Site-only commits use
chore(site):/docs(site):scopes so semantic-release never cuts a CLI release for a visual change. site/content/*.mdare article pages (e.g. the auto-evolve deep dive, formerly an external here.now URL).scripts/build-content.mjsrenders each todist/<slug>/index.htmlduringnpm run buildso they're hosted on mdflow.dev — never link site copy to external here.now pages.- Factual site copy should render from facts.json; artistic copy (headlines,
shaders, easter eggs) is hand-written. See
docs/SITE-SYNC.md.
.md file → parseFrontmatter() → resolveEngine(ladder, see below)
→ loadFullConfig() → applyDefaults()
→ applyInteractiveMode() → expandImports()
→ substituteTemplateVars() → buildArgs() → runCommand()
-
command.ts- Engine resolution and executionparseCommandFromFilename(): Infers engine fromtask.claude.md→claudehasInteractiveMarker(): Detects.i.in filename (e.g.,task.i.claude.md)resolveEngine(): the v3 ladder (see Engine Resolution below); never throws for a missing engine —DEFAULT_ENGINE(pi) appliesbuildArgs(): Converts frontmatter to CLI flagsextractPositionalMappings(): Extracts $1, $2, etc. mappingsrunCommand(): Spawns the engine; calls the adapter's optionalprepareEnv()hook (pi uses it for the bridged auth dir)- CAUTION: command.ts exports its own
getAdapter(portable-key layer); the registry lookup is imported asgetEngineAdapter
-
evals.ts-md eval <flow.md>: behavioral eval suites (<flow>.eval.ts, export default EvalCase[]) run in hermetic temp dirs; trust ledger at~/.mdflow/eval-results.json; prints cost before running; eval runs redirect MDFLOW_RUNS_FILE so they never pollute telemetry -
init.ts-md init: project bootstrap. Default path launches an installed engine CLI interactively, pre-loaded withassets/init/guide.md(passed verbatim — never through the import/template pipeline); post-flight verifies whatever the session wrote.--yes/no-TTY scaffoldsassets/init/catalog/deterministically.bin/mdflow.mjsis the plain-Node npx launcher that bridges to bun. -
evolve.ts-md evolve/md complain: complaints + rough runs → maintainer-drafted revision of the prompt BODY only, applied iff the full eval suite passes and scores no worse than the ancestor's baseline; failures revert byte-identical to<flow>.pending.md. Trigger rule is pure (decideEvolve) and refuses without a suite or fresh evidence; eval runs can never trigger it (corpus isolation).evolve: autofrontmatter opts a flow into post-run auto-evolution (quick re-runs become implicit complaints), hard-gated on trust-ledgerlastCleanAt. Complaints are consumed only by evolution; rough runs also by a clean eval. Crash-safe via<flow>.md.evolve-backup. Verified inevolve.test.ts. -
adapters/pi-auth.ts- Codex-subscription auth bridge for the pi engine (~/.mdflow/pi-agent, pointed at via PI_CODING_AGENT_DIR); never writes the user's real credential files -
isolation.ts-_isolatedresolution: adapter-declared context-stripping flags layered config defaults < isolation < frontmatter; unsupported engines produce a stderr warning (see Isolation table below) -
system-prompt.ts-_system-prompt/_append-system-promptextraction and adapter translation (flags, codex-coverrides, gemini GEMINI_SYSTEM_MD temp file); unsupported engines fail the run -
compat.ts- Automatic frontmatter version/compatibility stamps:_mdflow_versionwritten at creation (md create/md init),_compatstamped/upgraded after successful local runs (skipped for remote flows andMDFLOW_EVAL_RUN=1); surgical line-level frontmatter edits, semver-aware, major-mismatch prints a dim stderr notice, never blocks execution -
config.ts- Global configuration- Loads defaults from
~/.mdflow/config.yaml - Built-in defaults: All commands default to print mode
getCommandDefaults(): Get defaults for a commandapplyDefaults(): Merge defaults with frontmatterapplyInteractiveMode(): Converts print defaults to interactive mode per command
- Loads defaults from
-
types.ts- Core TypeScript interfacesAgentFrontmatter: Simple interface with system keys + passthrough- System keys:
_varname(template vars),env,$1/$2/etc.
-
schema.ts- Minimal Zod validation (system keys only, rest passthrough) -
imports.ts- File imports with advanced features:- Basic:
@./path.md- inline file contents - Globs:
@./src/**/*.ts- multiple files (respects .gitignore) - Line ranges:
@./file.ts:10-50- extract specific lines - Symbols:
@./file.ts#InterfaceName- extract TypeScript symbols - Commands:
!`cmd`- inline command output - URLs:
@https://example.com/file.md- fetch remote content
- Basic:
-
env.ts- Environment variable loading from .env files -
template.ts- LiquidJS-powered template engine for variable substitution -
logger.ts- Structured logging with pino (logs to~/.mdflow/logs/<agent>/) -
history.ts- Frecency tracking and variable persistencerecordUsage(): Track agent file usage for frecency sortinggetFrecencyScore(): Calculate frecency score for a pathgetVariableHistory(): Get previous variable values for an agentsaveVariableValues(): Save prompted variable values for future runsgetPreviousVariableValue(): Get a specific variable's previous value
Engines resolve via the ladder, most explicit first:
--engineCLI flag (deprecated aliases:--_command/-_c,--tool)MDFLOW_ENGINEenvironment variable- Filename pattern:
task.claude.md→claude - Frontmatter
engine:(deprecated aliasestool:/_tool:warn) - Config
engine:(project config beats~/.mdflow/config.yaml) - Built-in default:
pi
Implicit resolution (env/config/default) prints a dim explanation line on stderr. A file with no frontmatter and only an implicit engine is treated as a document and printed, not executed.
System keys (consumed by md, not passed to command):
_varname: Template variables (e.g.,_name: "default"→{{ _name }}in body →--_nameCLI flag)_stdin: Auto-injected template variable containing piped input_1,_2, etc.: Auto-injected positional CLI args (e.g.,md task.md "foo"→{{ _1 }}= "foo")_args: Auto-injected numbered list of all positional args_inputs: Named positional arguments to consume from CLI (e.g.,_inputs: [_message])_env: Sets process.env before execution$1,$2, etc.: Map positional args to flags_interactive: Enable interactive mode (overrides print-mode defaults)_subcommand: Prepend subcommand(s) to CLI args (e.g.,_subcommand: exec)_cwd: Override working directory for inline commands (!`cmd`)_isolated: Isolation is ON BY DEFAULT — setfalseto opt back into ambient context (skills, MCP, memory/context files, plugins) — see Isolation below_system-prompt/_append-system-prompt: Replace/append the engine's system prompt, translated per engine — see System Prompt below_mdflow_version/_compat: Automatic compatibility stamps (see below) — never set these by hand
Isolation (src/isolation.ts + adapter getIsolationDefaults()):
ON BY DEFAULT for every engine — the flow file is the entire behavior;
skills/MCP/context a flow needs are referenced explicitly in frontmatter
(mcp-config:, plugin-dir:, add-dir:, extension paths), not inherited
from the machine. _isolated: false (frontmatter), --_isolated false
(CLI), or commands.<engine>._isolated: false (config) opts out. The
verified flags layer between config defaults and frontmatter, so an isolated
flow can still re-enable one layer (safe-mode: false). Per engine:
| Engine | Flags |
|---|---|
| claude | --safe-mode --no-session-persistence (the latter is print-only, stripped in interactive; --bare deliberately NOT used — it breaks OAuth auth) |
| codex | --ignore-user-config --ephemeral -c project_doc_max_bytes=0 (first two are exec-only, stripped in interactive) |
| gemini | --extensions none (GEMINI.md + settings MCP have no CLI kill-switch) |
| copilot | --no-custom-instructions --disable-builtin-mcps (user MCP config still loads) |
| opencode | --pure (AGENTS.md still loads) |
| pi | --no-extensions --no-skills --no-prompt-templates --no-context-files --no-session |
| droid / cursor-agent / agy | no controls exist — runs ambient; warns only on an explicit _isolated: true |
System prompt (src/system-prompt.ts + adapter applySystemPrompt()):
the flow body is always the user prompt; _system-prompt (replace) and
_append-system-prompt (string or list) control the system prompt.
Translations: claude/pi → --system-prompt/--append-system-prompt; codex →
-c model_instructions_file=<temp file> / -c developer_instructions=…;
gemini → GEMINI_SYSTEM_MD=<temp file> env (replace only — append errors).
Engines with no mechanism (copilot, droid, opencode, cursor-agent, agy) fail
the run — a silently dropped system prompt would be a different flow. Never
add a translation from an unverified flag; check the engine's --help first.
Compatibility stamps (src/compat.ts): every flow tracks which mdflow it
works with, with zero user involvement. md create/md init stamp
_mdflow_version (version created with); after any successful local run,
mdflow stamps/upgrades _compat (newest version verified to work) via a
surgical frontmatter edit that preserves the rest of the file byte for byte.
Upgrades only fire on major/minor skew — patch and prerelease bumps never
rewrite flows (no per-release git churn).
Remote flows and eval runs (MDFLOW_EVAL_RUN=1) are never stamped. A major
mismatch between the recorded version and the running mdflow prints a dim
stderr notice but never blocks. Frontmatter containing only these keys still
counts as "no frontmatter" for the document-vs-flow rule.
Note: --_varname CLI flags work without frontmatter declaration. If a _ prefixed variable is used in the body but not provided, you'll be prompted for it.
Variable History: When prompting for missing variables, previous values are shown as defaults (stored in ~/.mdflow/variable-history.json). Press Enter to accept the previous value or type to override. Use --_no-history to skip loading/saving variable history.
All other keys are passed directly as CLI flags:
---
model: opus # → --model opus
dangerously-skip-permissions: true # → --dangerously-skip-permissions
add-dir: # → --add-dir ./src --add-dir ./tests
- ./src
- ./tests
_env: # Sets process.env (underscore prefix = system key)
API_KEY: secret
---Map the body or positional args to specific flags:
---
$1: prompt # Body passed as --prompt <body> instead of positional
---All commands default to print mode (non-interactive). Use .i. filename marker or _interactive: true for interactive mode.
task.claude.md # Print mode: claude --print "..."
task.i.claude.md # Interactive: claude "..."
task.copilot.md # Print mode: copilot --silent --prompt "..."
task.i.copilot.md # Interactive: copilot --silent --interactive "..."
task.codex.md # Print mode: codex exec "..."
task.i.codex.md # Interactive: codex "..."
task.gemini.md # Print mode: gemini "..." (one-shot)
task.i.gemini.md # Interactive: gemini --prompt-interactive "..."
task.droid.md # Print mode: droid exec "..."
task.i.droid.md # Interactive: droid "..."
task.opencode.md # Print mode: opencode run "..."
task.i.opencode.md # Interactive: opencode "..."IMPORTANT: Use these exact model names. Do not guess or use deprecated model names.
Staleness note (2026-07): this table is a December 2025 snapshot and newer models have shipped since (e.g. the mdflow.dev examples use
gpt-5.5-codex-maxandgemini-3.1-pro). When a model name matters, verify against the engine CLI's own--help/docs instead of this table.
| Type | Values |
|---|---|
| Aliases | sonnet, opus, haiku, opusplan |
| Full names | claude-sonnet-4-5-20250929, claude-opus-4-5-20251101, claude-haiku-4-5-20251001, claude-opus-4-1-20250805 |
Environment variables for alias control:
ANTHROPIC_DEFAULT_SONNET_MODEL- Override sonnet aliasANTHROPIC_DEFAULT_OPUS_MODEL- Override opus aliasANTHROPIC_DEFAULT_HAIKU_MODEL- Override haiku alias
| Type | Values |
|---|---|
| Default | codex-mini-latest (o4-mini optimized for CLI) |
| Reasoning models | o3, o4-mini |
| GPT models | gpt-4.1 |
Codex works with any OpenAI model. Example: -m o3 or -c model="o3"
| Type | Values |
|---|---|
| Default (free) | gemini-2.5-pro |
| Preview | gemini-3-pro (requires subscription or paid API key) |
To enable Gemini 3 Pro: run /settings, toggle "Preview features" to true.
Explicit --model choices (from copilot --help):
| Category | Models |
|---|---|
| Claude | claude-sonnet-4.5, claude-haiku-4.5, claude-opus-4.5, claude-sonnet-4 |
| GPT | gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.2, gpt-5.1, gpt-5, gpt-5.1-codex-mini, gpt-5-mini, gpt-4.1 |
| Gemini | gemini-3-pro-preview |
Set default frontmatter per command:
commands:
claude:
model: sonnet # Default model for claudeUses LiquidJS for full template support:
- Variables:
{{ _varname }}(use_prefix for template vars) - Stdin:
{{ _stdin }}(auto-injected from piped input) - Conditionals:
{% if _force %}--force{% endif %} - Filters:
{{ _name | upcase }},{{ _value | default: "fallback" }} - CLI override:
--_varname valuematches_varnamein frontmatter
Tests use Bun's test runner with describe/it blocks:
import { describe, it, expect } from "bun:test";
describe("parseCliArgs", () => {
it("parses command flag", () => {
const result = parseCliArgs(["node", "script", "file.md"]);
expect(result.filePath).toBe("file.md");
});
});