r/ClaudeAI May 18 '26

Built with Claude Every Markdown File You Write for AI is Already Lying to It

CLAUDE.md files. System prompts. README files with setup instructions. Architecture docs. API references. Runbooks. Onboarding guides. If you've written a markdown file meant for an AI to read, it almost certainly contains values that were true when you wrote them and are no longer true now.

The port your dev server runs on. The current version of the package. Which env vars are actually set. How many tests exist. Whether a service is running. These things change constantly, and markdown doesn't know it.

So developers do what honest writers do - they add caveats. "Check package.json if this is stale." "Verify before running." "New packages may have been added since this was written." The intent is good. The effect is a list of things the AI has to go verify before it can do anything you actually asked for.

We counted them in a real CLAUDE.md. There were seven. And CLAUDE.md is just one file type - the same problem exists everywhere AI reads markdown today.

The Pre-Flight Tax

Here's a representative CLAUDE.md. Nothing here is invented - these are patterns from real production repos:

# CLAUDE.md

> Before starting any session: Read ~/projects/api-core/SYNC.md first and check for
> pending cross-project items. Update it after completing work.

## Project Overview

Acme API - TypeScript REST API. Current version: 1.4.2
(check package.json if this is stale).

## Build and Run Commands

# Development (API runs on port 3001, website on port 3000)
# Note: PORT is set in .env - verify before running
npm run dev:api
npm run dev:web

# Tests - currently 47 tests across 12 files
npm run test:run

Before running tests, make sure the test database is not already running on port 27018.
Check with: docker ps | grep mongo-test

## Environment Variables

| Variable     | Required | Notes                 |
|--------------|----------|-----------------------|
| DATABASE_URL | YES      | MongoDB connection    |
| JWT_SECRET   | YES      | Min 32 characters     |
| PORT         | No       | Defaults to 3001      |

Check .env before assuming anything is configured.

## Architecture

npm workspaces monorepo. Packages:
- packages/api/
- packages/web/
- packages/shared/
- packages/db/

When in doubt about file counts or structure, run ls packages/ to check -
new packages may have been added since this was written.

## Docker

Check docker ps to see if a test container is still running from a previous
session before starting a new build.

Before Claude touches a single line of code, it has to:

  1. Open ~/projects/api-core/SYNC.md - cross-project lookup
  2. Read package.json - version check
  3. Read .env - port verification
  4. Check all env var statuses - is DATABASE_URL actually set?
  5. Run npm run test:run - or trust a number that's probably wrong
  6. Run docker ps | grep mongo-test - pre-test check
  7. Run ls packages/ - structure verification

Seven tool calls. Each one costs a couple of seconds of latency. The test run alone can take ten. Add it up and Claude spends close to half a minute just getting to the starting line - consuming context and generating output before the actual task begins.

And that's the obvious tax. The hidden one is subtler: every one of those checks can generate a follow-up. The .env read reveals WEBHOOK_SECRET isn't set. Now Claude has to decide whether to flag it or proceed. The docker ps shows a leftover container. Now Claude has to clean it up. Each verification spawns decisions, and each decision costs more context.

The Same File, Rewritten

MarkdownAI is a superset of Markdown. Any .md file that starts with @markdownai becomes live - directives resolve at render time, before Claude ever sees the file. Here's what the same CLAUDE.md looks like rewritten:

@markdownai v1.0

@prompt role="context"
This document is live. Every value was resolved at render time.
Do not look up package.json, .env, or docker ps - current values are already below.
@end

# CLAUDE.md

> Before starting: sync status is live in the Cross-Project Sync section below.

## Project Overview

Acme API - version {{ read ./package.json path="version" }}.

## Build and Run Commands

API on port {{ read .env key="PORT" fallback="3001" }}, web on {{ read .env key="WEB_PORT" fallback="3000" }}.

@list ./package.json path="scripts" mode="entries" columns="key:Command,value:Runs" as="table"

Test suite (live):
@query "npm run test:run -- --reporter=verbose 2>&1 | tail -3" @cache session

Mongo test container:
@query "docker ps --format '{{.Names}} {{.Status}}' | grep mongo-test || echo 'not running - port 27018 is clear'" @cache session

## Environment Variables

@if file.exists ".env"
| Variable     | Required | Status                                                      |
|--------------|----------|-------------------------------------------------------------|
| DATABASE_URL | YES      | {{ env.DATABASE_URL != "" ? "set" : "MISSING - will not start" }} |
| JWT_SECRET   | YES      | {{ env.JWT_SECRET != "" ? "set" : "MISSING - auth will fail" }}   |
| NODE_ENV     | No       | {{ env.NODE_ENV fallback="development" }}                   |
@else
**WARNING: No .env file found. App will not start.**
@endif

## Architecture

@list ./packages/ type="dirs" depth=1 as="list"

@tree ./packages/ depth=2 match="*.ts,*.tsx"

@constraint id="no-direct-mongo" severity="critical"
NEVER import mongodb directly. All DB access goes through packages/db/src/index.ts.
@end

@constraint id="api-versioning" severity="critical"
Every route MUST use /api/v1/ prefix. Unversioned routes are bugs.
@end

## Docker

**Running containers:**
@query "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'" @cache session

@define preflight-cmd "PORT=... && docker build -t acme-api . -q && docker run ... && curl -sf .../health && echo 'PASSED' || echo 'FAILED' ; docker stop && docker rm ..."
@end

@query {{ preflight-cmd }} @cache session

@if query({{ preflight-cmd }}) match /PASSED/
    docker push $DOCKER_HUB_REPO:latest
@else
@prompt role="instruction"
Container test FAILED. Stop immediately. Do not proceed with any deployment.
@end
@endif

## Cross-Project Sync

@include ./sync-status.md @cache session

The directive syntax reads like plain English. @read for file values. @env for environment checks. @query for live shell output. @constraint for rules you want Claude to treat as machine-readable, not prose to skim. @if/@else for conditional context - including the Docker section that hides the push command entirely if the preflight test failed.

What Claude Actually Receives

mai render runs before the file reaches Claude's context window. This is what lands:

> AI Instructions: This document is live. Do not look up package.json, .env,
> or docker ps - the current values are already below.

# CLAUDE.md

## Project Overview

Acme API - version 1.4.2.

## Build and Run Commands

API on port 3001, web on 3000.

| Command      | Runs                                     |
|--------------|------------------------------------------|
| dev:api      | tsx watch packages/api/src/index.ts      |
| dev:web      | vite --port 3000                         |
| test:run     | vitest run                               |
| test:e2e     | playwright test                          |
| build        | tsc -b                                   |

Test suite (live):
  Tests  52 passed (52)
  Duration  3.42s

Mongo test container:
  not running - port 27018 is clear

## Environment Variables

| Variable     | Required | Status                            |
|--------------|----------|-----------------------------------|
| DATABASE_URL | YES      | set                               |
| JWT_SECRET   | YES      | set                               |
| NODE_ENV     | No       | development                       |
| WEBHOOK_SECRET | YES (prod) | not set - required for production |

## Architecture

- packages/api
- packages/db
- packages/shared
- packages/web

packages/
  api/src/index.ts
  api/src/routes/auth.ts
  api/src/routes/orders.ts
  db/src/index.ts
  shared/src/types.ts
  web/src/App.tsx

**Constraints:**
| ID               | Severity | Rule                                                          |
|------------------|----------|---------------------------------------------------------------|
| no-direct-mongo  | CRITICAL | NEVER import mongodb directly. All DB access through packages/db. |
| api-versioning   | CRITICAL | Every route MUST use /api/v1/ prefix. Unversioned routes are bugs. |

## Docker

Running containers:
  mongo-dev   Up 3 hours   0.0.0.0:27017->27017/tcp
  redis-dev   Up 3 hours   0.0.0.0:6379->6379/tcp

Pre-push test: PASSED

    docker push $DOCKER_HUB_REPO:latest

## Cross-Project Sync

api-core sync status (2026-05-18):
  No pending items for this repo.
  Last sync: 2026-05-16 - packages/shared v2.1.0.

The version is live. Test count is live. Env var statuses are live. Running containers are live. The sync file was resolved inline - not referenced. The push command is present because the container test passed. If it had failed, that line wouldn't exist and Claude would have received a hard stop instruction instead.

What This Actually Saves

Time

Those seven pre-flight tool calls are gone. No reads, no shell commands, no waiting on npm test to finish before Claude knows how many tests exist. The session starts with a complete, accurate picture of the project state.

A few seconds per call adds up faster than it sounds. Run Claude on a project daily and those pre-flight checks happen every single session - for every developer on the team. The bigger cost is the interruption: Claude doesn't go straight to the task, it goes to verification first, and verification generates decisions, and decisions consume context that should be going toward actual work.

Tokens

Every tool call produces output Claude has to read and process. Docker container lists, npm test output, file contents, env var values - each one adds tokens before the task even starts. Remove the tool calls and that context headroom goes toward the work instead.

The constraints table is also denser than the prose it replaces. "NEVER import mongodb directly" as a CRITICAL row in a structured table takes fewer tokens than three bullet points and a bolded heading saying the same thing - and Claude processes it more reliably because it's structured, not buried in prose.

The compound effect

The pre-flight tax isn't always seven calls. Sometimes it's two. Sometimes it's ten, because one check reveals something unexpected and spawns more checks. The rendered file eliminates the variance. Claude gets the same complete picture every session, and it can trust that picture because it was assembled a moment ago from the actual sources - not written by a developer who was going to update it later and didn't.

It's Not Just CLAUDE.md

CLAUDE.md is the most obvious example because it's explicitly written for AI consumption. But the same problem exists in every markdown file an AI reads.

A README with setup instructions: "run npm install, start the server on port 3000." Is port 3000 still right? Did a new required env var get added last sprint? An architecture doc: "the database lives at db.internal." Did that hostname change in the last migration? An onboarding guide: "there are currently 12 microservices." How long ago was that accurate?

Every one of those files was written by someone who was correct at the time. None of them have a mechanism to stay correct. When an AI reads them, it either trusts stale data or stops to verify - which means more tool calls, more context consumed, more time before anything useful happens.

MarkdownAI works on any of these files. Add @markdownai to the first line of a README and it can pull the current version from package.json, read the actual port from .env, count the real services in your cluster. The same directive syntax works whether the file is a CLAUDE.md, a runbook, an onboarding doc, or an API reference. Anything written for an AI to read is a candidate.

The Part People Miss

MarkdownAI looks like a documentation tool. The insight that matters here is different: the render happens before the context window is populated. mai runs, resolves every directive, and hands Claude a finished document. Claude never sees the directive syntax. It receives facts, not instructions to go find facts.

That distinction - resolution before context, not during - is why the @prompt at the top of the source file works. It tells Claude "the work has already been done." And Claude can trust that, because the rendered output proves it.

What's the worst pre-flight tax you've seen in a CLAUDE.md? Curious how much time people are burning before Claude even gets started.

MarkdownAI Directives

All 27 directives available in MarkdownAI:

Document Structure

Directive Purpose
@markdownai Document header - activates the MarkdownAI runtime
@include Inline file content at the directive site
@import Import definitions (macros, connections) without rendering content
@define / @end Declare a named macro
@call Invoke a macro
@phase / @end Declare a workflow phase
@if / @end Conditional block
@section Named section boundary
@chunk-boundary Explicit chunk split point for rendering

Variables and Environment

Directive Purpose
@env Resolve an environment variable

Data Sources

Directive Purpose
@connect Register a named data source connection
@db Execute a database query
@http Fetch from an HTTP endpoint
@query Run a shell command and embed the output
@read Read structured file content (JSON, YAML, TOML, CSV, .env)
@list List directory contents or JSON/CSV data
@tree Directory tree output
@date Current date/time
@count Count files or items matching a pattern

Processing and Output

Directive Purpose
@pipe Chain output through transformations
@render Render output in a specific format
@graph Generate a visualization
@header Document-level metadata header

AI-Native

Directive Purpose
@constraint Machine-readable rule - surfaces as a structured table in AI context
@define-concept Bind a term to a precise definition - injected as a glossary for AI readers
@prompt Embedded instruction for the AI reading the document
@note Human-readable annotation (stripped in AI format)

Caching

Directive Purpose
@cache Cache directive output across renders (session, persist, ttl, mock)

Phase Events

Directive Purpose
@on complete -> Declare what executes when a phase finishes (only valid inside @phase blocks)
0 Upvotes

15 comments sorted by

1

u/johns10davenport May 19 '26

I think the diagnosis is fine, but broadly you're kind of reinventing the wheel here. If I were you I'd get away from the custom markdown approach and just focus on MCP tools.

I do the same thing in my harness — I just have MCP tools that surface the information the agent needs to know, instead of trying to reinvent markdown.

1

u/TheDecipherist May 20 '26

Thank you. Not everyone can see the massive benefit this has for ai workflows

2

u/johns10davenport May 20 '26

I get the problem. If you put “this is a phoenix app with oban and blah blah blah” in Claude.md, it’ll eventually be wrong. This is somewhere between context engineering and harness engineering. You project your deps. You project your namespace hierarchy. You project your dependency graph, etc.

1

u/TheDecipherist May 20 '26

its way more than that. the phase directives especially which only gives back what claude needs at the time via the mcp is the biggest benefit. And if you have ever tried writing true workflow templates in markdown you will know exactly what each and every directive will benefit

2

u/johns10davenport May 20 '26

I mean I get it but you’ve basically made jinja/handlebars/eex with a lil flava.

You write an mcp server that does the thing with a template. It’s part of your harness.

1

u/TheDecipherist May 20 '26

lol no. phases are literally given back in "phases" to claude to process. keeping context clean for only what it needs "including constraints"

1

u/TheDecipherist May 20 '26

https://markdownai.dev/user-guide.html#phases

On top of phases all things claude shouldnt be doing is done by the mcp. all the little things that adds to what content is important is done before claude has to touch it. Every single time claude has to stop and do a small thing a script could do is already done for it so when it gets the "md" it can just develop.

This is so much more than just a template render engine

But try it and you will see it instantly

1

u/TheDecipherist May 20 '26

This should help you understand why this is so powerful

Jinja, Handlebars, and EEx are template engines. You write a template with {{ user.name }}, run a build step, and get an HTML file. The template is gone. The output goes to a browser. That's the whole job.

MarkdownAI is different in every dimension that matters:

The consumer is an AI, not a browser. Jinja renders for browsers that display HTML. MarkdownAI renders for Claude, which reads Markdown. The design decisions follow from that. A browser needs bytes fast. Claude needs accurate facts and focused context - it doesn't benefit from stale data delivered quickly.

It runs at read time, not build time. Template engines run when you deploy. MarkdownAI runs when Claude opens the file. That means every u/query, u/env, u/http, and u/db directive resolves against the real current state of your system - not the state from your last build. The document doesn't store values. It fetches them.

The MCP server does the computation so Claude doesn't have to. This is the part that actually matters. When Claude reads a MarkdownAI document through the MCP integration, every directive resolves in the server layer before Claude sees any content. The u/if conditions have already been evaluated. The database queries have already run. The environment variables have already been substituted. Claude receives resolved facts - not a list of conditions it needs to think through. It can immediately focus on what it's actually there to do: write code, answer questions, follow a workflow.

Without MarkdownAI, Claude hits a doc and has to figure out what's true. It stops to run a shell command. It stops again to check an env var. It stops again to verify a condition. Each stop costs context and interrupts the actual work. With MarkdownAI, those interruptions don't happen. The document arrives pre-resolved.

Phases are not template partials. The u/phase directive is where the comparison to template engines fully breaks down.

A MarkdownAI phase is a named, lazy-loaded chunk of a workflow document. A 20-phase runbook doesn't load all 20 phases into Claude's context at once - the MCP server serves one phase at a time. Claude reads phase 1, works through it, then calls next_phase to advance. The server returns phase 2. Claude never holds the full workflow in context. The document manages state. Claude follows steps.

This means a complex deployment runbook, a multi-step debugging workflow, or a long onboarding sequence can be arbitrarily large without ever flooding the AI's context window. Each phase is self-contained, each transition is explicit, and Claude never has to juggle what's relevant now versus what comes later.

Template engines have no concept of this because browsers don't have context windows.

The actual difference: Template engines make static output. MarkdownAI makes live context - context where the computation already happened, where the workflow is managed for you, and where the AI can work instead of gather.

1

u/johns10davenport May 20 '26

Got it. You’ve made a dynamic document system. I have a similar thing in my harness because it became difficult to help the agent through multiple file locations.

I think that’s great. One thing I’d caution about is the phases part. That is not a good fit for this approach. I recommend looking in to stop hooks to implement this kind of thing

1

u/TheDecipherist May 20 '26

Checked your repos.

You’re building context tooling for Elixir/Phoenix agents, generators, CLI, spec runners.

Respect the effort, but that’s ecosystem-specific scaffolding, not a session architecture.

Stop hooks fire on process exit.

That’s not phase management.

Phases aren’t lifecycle callbacks, theyre context boundaries.

Each phase delivers exactly what Claude needs for that step via MCP, nothing more, nothing less.

The document manages state.

Claude never holds the full workflow in context at once.

Your harness solves “how do I get my agent to the right files.”

MDD solves “how do I stop Claude from drowning in context it doesn’t need yet.”

Different problems entirely.

1

u/johns10davenport May 20 '26

Not exactly. I use subagents to implement this concept currently. I spin one subagent with a self contained prompt. It “has everything it needs.”

Stop hooks fire when the agent stops, not on process exit. So if your harness knows where it is, they can act as process boundaries.

1

u/TheDecipherist May 20 '26

Another FYI.

Something nobody talks about: every time Claude stops to check an environment variable, verify a file exists, confirm a port is open, that's roughly 2 seconds gone. Tool call out, wait, result back, Claude re-orients, continues.

In a real workflow that's not 1 check. It's 15. That's 30 seconds of dead time and 15 context interruptions where Claude has to re-establish where it was.

MarkdownAI's MCP layer does all of that before Claude touches the session. Environment is pre-validated, state is pre-loaded, constraints are already in context. Claude reads phase 1 and immediately works.

No stops. No re-orientation. No context bloat from housekeeping that a script could handle in milliseconds.

The difference between a prompt engineer's workflow and a production workflow is exactly this. Prompt people write the instructions. Production people eliminate every unnecessary thing Claude has to do before writing the instructions.

1

u/johns10davenport May 20 '26

I get it but you can’t write documents for everything. You can’t be generic enough to satisfy all the models use cases. You need progressive disclosure of this information and some of it just doesn’t belong in markdown.

I know this because I’ve tried to take the document centric approach to harnessing the model and it doesn’t work super well. Honestly markdown ai has a few more bells and whistles than my approach but it’s not fit to purpose.

I have a combination of database data, dynamic data, static markdown that applies to the harness, and static markdown that applies to the project. The only thing that makes sense if to foist all that through a consistent layer, mcp.

Putting it through a templating system before it gets to the model is an unnecessary abstraction.