File import in CLAUDE.md

Status Closed — not planned
Maintainer reply None cached
Activity 13 comments · opened Aug 22, 2025 · closed Jan 9, 2026

I have a question about file import in CLAUDE.md (or wherever it's supported): how does Claude handle markdown heading hierarchy when importing files?
Specifically, if I import a file that contains its own headings, are those headings inserted at the same level as they appear in the imported file, potentially breaking the intended document structure?
Let me explain with an example:
CLAUDE.md

# Project info
project information ...
# Code conventions
@conventions.md

conventions.md

# Main conventions
- Use DRY principles

Does the resultant prompt become:

# Project info
project information ...
# Code conventions
# Main conventions
- Use DRY principles

In this case, the imported file content would be structurally outside of the # Code conventions section, since # Main conventions becomes a top-level heading rather than a subsection. This breaks the intended document hierarchy where the conventions should be nested under the Code conventions section.
Is this how the import mechanism works, or does Claude automatically adjust heading levels to maintain proper document structure?

View original on GitHub ↗

13 Comments

coygeek · 1 year ago

Hey, that's a really sharp question.

You've hit the nail on the head with your analysis. Based on the documentation and my own experience, the @ import is a direct, literal file inclusion. It does not automatically adjust or "demote" the heading levels of the imported file.

So, in your example, your hypothesis is correct. The final context provided to Claude would look like this:

# Project info
project information ...
# Code conventions
# Main conventions
- Use DRY principles

This creates a new top-level heading (# Main conventions) that becomes a sibling to # Code conventions, breaking the intended hierarchy.

The official documentation on managing memory mentions the @path/to/import syntax but doesn't specify any kind of markdown parsing or heading manipulation, which confirms it's a simple text-inclusion mechanism.

The Solution / Best Practice

The way to handle this is to structure your imported files as "partials," with the assumption that they will always be nested under a higher-level heading.

Here’s how you'd fix your example:

CLAUDE.md (no change)

# Project info
project information ...

# Code conventions
@conventions.md

conventions.md (this is the file you change)

## Main conventions
- Use DRY principles

## Naming conventions
- Use camelCase for variables

Now, when conventions.md is imported, its ## headings are correctly nested under the # Code conventions section, preserving your document structure perfectly. The final prompt context becomes:

# Project info
project information ...

# Code conventions
## Main conventions
- Use DRY principles

## Naming conventions
- Use camelCase for variables

The general rule I follow now is: any file intended for import should start its headings at ## or lower. This keeps the main CLAUDE.md as the single source of top-level # headings and ensures everything stays organized.

Hope that helps clarify things! It's a subtle point but super important for keeping those memory files effective as they grow.

marcoscale98 · 1 year ago

@coygeek yes, really clear explanation.
I’m wondering why the Claude code team didn’t choose XML for memories. If I’m not mistaken, the literature suggests that XML is better understood by LLMs and could have solved the hierarchy problem.

coygeek · 1 year ago

You're right that LLMs have a solid understanding of XML's explicit structure. The strict hierarchy of opening and closing tags is unambiguous, which is a big advantage for machine parsing.

So why did the Anthropic team likely choose Markdown? While I can only speculate, I believe it comes down to a core design principle: optimizing for the human developer experience.

Here are a few reasons why Markdown probably won out:

  1. Developer Ergonomics: This is the biggest one. Developers live in Markdown. We write README.md, CONTRIBUTING.md, pull request descriptions, and documentation in it every day. It's lightweight, easy to read, and requires no special tooling to edit. Forcing developers to write memories in XML, with its verbose tags (<section><title>Code Conventions</title><content>...</content></section>), would add significant friction to the process.
  1. Ecosystem Fit: CLAUDE.md files are meant to be checked into Git repositories alongside code. Markdown is the de facto standard for documentation in the software development ecosystem. An XML file would feel out of place and less natural to maintain within a typical repo.
  1. Simplicity of the Tool: Using Markdown with a simple @ text-inclusion mechanism is incredibly straightforward to implement on the tool's side. It doesn't require a full XML parser. The hierarchy "problem" we discussed is solved with a simple, developer-friendly convention (start imported files with ##), which is often preferable to adding complexity to the tool itself.
  1. "Good Enough" Structure for LLMs: While XML is perfectly structured, modern LLMs like Claude are exceptionally good at inferring structure from Markdown. They've been trained on billions of lines of it from GitHub, Stack Overflow, and the web. They understand that # is a top-level heading and ## is a subheading. For the purpose of providing context, Markdown's implicit structure is more than sufficient.

Ultimately, I think they made a trade-off: they chose a format that is maximally human-readable and writable over one that is maximally machine-readable, betting that the LLM is smart enough to handle the slightly less rigid structure of Markdown. For a tool designed to be a developer's partner, prioritizing the developer's workflow makes a lot of sense.

It's a classic case of choosing the right tool for the job. XML is fantastic for data interchange and configuration files (like .csproj or pom.xml), but for human-authored instructions meant to be read and edited frequently, Markdown is king.

Great discussion! It's fun to think through the "why" behind these design decisions.

marcoscale98 · 1 year ago

I was reading the official doc for file imports and I see this example:

See @README for project overview and @package.json for available npm commands for this project.

# Additional Instructions
- git workflow @docs/git-instructions.md

If your hypothesis were correct (so without changes after import), the structure of this file would really suck and would not be a useful example to copy that Anthropic would recommend. You don't agree? @coygeek

coygeek · 1 year ago

That's an excellent catch, and you are absolutely right to question how that example fits with my hypothesis. It’s a perfect stress test for the idea, and it actually helps reveal a more nuanced and complete picture of how the @ import works.

You don't disagree at all; in fact, that example from the official docs is a great one precisely because it showcases a different but equally important usage pattern for imports.

Let's break it down. My previous explanation focused on what I'd call Structural Inclusion, where you use an import to stand in for an entire section of your document:

# Code Conventions
@conventions.md 

In this case, conventions.md is expected to provide a structured block of content, and my advice to use ## headings holds true to maintain the hierarchy.

However, the official documentation example demonstrates what I'd call Inline Context Injection. Notice where the imports are placed:

See @README for project overview and @package.json for available npm commands for this project.

# Additional Instructions
- git workflow @docs/git-instructions.md
  1. @README and @package.json: These are placed directly inside a paragraph of text. They aren't meant to create new headings or sections. The tool literally replaces @README with the full content of the README.md file right there in the sentence. The LLM is smart enough to understand it's being given the file's content as reference material for that sentence.
  1. @docs/git-instructions.md: This is the most interesting one. It's placed inside a list item. The content of git-instructions.md will be injected as part of that bullet point.

Let's imagine docs/git-instructions.md contains this:

Follow the feature branch model. Create a new branch from `main` for each new feature. Keep commits small and focused.

The final context sent to Claude would be:

See [content of README.md] for project overview and [content of package.json] for available npm commands for this project.

# Additional Instructions
- git workflow Follow the feature branch model. Create a new branch from `main` for each new feature. Keep commits small and focused.

See how that works? The content becomes an extension of the bullet point itself. In this pattern, the structure of the imported file is less important (it might not even have headings), because its purpose is to provide raw data or a short text snippet, not a hierarchical section.

So, to answer your question directly: No, I don't disagree. That example is great because it shows the flexibility of the @ import. It's not a "smart" import that understands Markdown hierarchy; it's a "dumb" text-replacement tool, and its power comes from how we choose to place it.

The Refined Understanding:

  • For Structural Inclusion (your original example): Place the import on its own line under a heading. The imported file should use ## or lower to respect the hierarchy.
  • For Inline Injection (the docs example): Place the import inside a sentence or list item to provide targeted, specific context without disrupting the main document flow.

This is a fantastic point you've raised. It clarifies that we have two distinct ways to use memory imports, and both are valid and powerful. Thanks for pushing on that! It deepens the understanding for everyone.

marcoscale98 · 1 year ago

@coygeek I'm not so sure that the file import feature (and LLMs) is as smart as you describe.

@README and @package.json: These are placed directly inside a paragraph of text. They aren't meant to create new headings or sections. The tool literally replaces @README with the full content of the README.md file right there in the sentence.

A README file usually has headings (also first-level headings) and this leads to structural problems when imported. I would like to have a Claude team member answer on this point.

PaulRBerg · 12 months ago

This is a great suggestion and finding by @marcoscale98 but the responses from @coygeek are bad.

They are too long. They don't sound human. Is this an AI set up by the Anthropic team?

If yes, can you at least configure it to provide shorter responses - with an option to see the full response in a collapsible <details> block.

samtaplin · 11 months ago

From what I can tell from testing, referencing other files from claude.md in the form @/.claude/instructions/typescript.md doesn't automatically add those referenced files to Claude's context. Claude has to decide to read the file. Which it sometimes does and sometimes doesn't.

For instructions that I'd like to make available to multiple subdirectories, this is kind of a pain because I'm forced to choose between copy pasting typescript instructions in multiple places, and setting up imports that claude often ignores.

marcoscale98 · 11 months ago
From what I can tell from testing, referencing other files from claude.md in the form @/.claude/instructions/typescript.md doesn't automatically add those referenced files to Claude's context.

I don't agree. From my experience, it's always automatic the addiction to the context

marcoscale98 · 11 months ago

Absurd that we haven't had a response from Anthropic...the CLAUDE.md file is the basic instructions for the model, the brain! Without having good guidelines on how to drive the model, Claude Code is useless so.
@bcherny

github-actions[bot] · 8 months ago

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

github-actions[bot] · 7 months ago

This issue has been automatically closed due to 60 days of inactivity. If you're still experiencing this issue, please open a new issue with updated information.

github-actions[bot] · 7 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.