Project Context
Automatically load project instructions from NOUMEN.md and CLAUDE.md files with hierarchical scoping, rules directories, and inline includes.
Project context files give your agent persistent, per-project instructions that are loaded into the system prompt automatically. This is the same concept as CLAUDE.md — and noumen supports CLAUDE.md natively — but also recognizes NOUMEN.md as its own convention. Both formats work, and both can coexist in the same project.
Why opt-in?
Project context loading scans the filesystem at startup — walking from the working directory up to the filesystem root, plus user home and managed directories — to discover instruction files. This adds initialization latency and injects content into the system prompt that increases token usage on every request. If your project doesn't have NOUMEN.md or CLAUDE.md files, this work is wasted. Keeping it opt-in means you only pay the scan and prompt overhead when you've actually set up project instructions.
Quick start
- Create a
NOUMEN.md(orCLAUDE.md) in your project root:
# Project instructions
This is a TypeScript monorepo using pnpm workspaces.
- Use strict TypeScript. Prefer `const` over `let`.
- Write vitest tests for all new code.
- Use 2-space indentation.- Enable project context loading in your
Agentoptions:
import { Agent, AiSdkProvider } from "noumen";
import { LocalSandbox } from "noumen/local";
import { createOpenAI } from "@ai-sdk/openai";
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const code = new Agent({
provider: new AiSdkProvider({ model: openai.chat("gpt-5") }),
sandbox: LocalSandbox({ cwd: "/my/project" }),
options: {
projectContext: true,
},
});
await code.init(); // discovers and loads context filesThat's it. The agent now sees your project instructions in every conversation.
How it works
The loader scans four layers in order, from lowest to highest priority. Each layer generates filename variants from the configured dotDirs list (defaults: [".noumen", ".claude"], which yields NOUMEN.* and CLAUDE.*):
| Layer | Scope | Files scanned |
|---|---|---|
| Managed | Enterprise/MDM | <managedDir>/NOUMEN.md, <managedDir>/CLAUDE.md, rules in <managedDir>/.noumen/rules/, <managedDir>/.claude/rules/ |
| User | Per-user home | ~/.noumen/NOUMEN.md, ~/.claude/CLAUDE.md, rules in ~/.noumen/rules/, ~/.claude/rules/ |
| Project | Repo ancestors | For each directory from filesystem root down to cwd: <dir>/NOUMEN.md, <dir>/CLAUDE.md, <dir>/.noumen/NOUMEN.md, <dir>/.claude/CLAUDE.md, rules in <dir>/.noumen/rules/, <dir>/.claude/rules/ |
| Local | Gitignored | <dir>/NOUMEN.local.md, <dir>/CLAUDE.local.md (same ancestor walk) |
Files loaded later in the list have higher priority. This means your project-level NOUMEN.md at the repo root overrides user-level defaults, and NOUMEN.local.md (which you gitignore for personal overrides) has the highest priority of all.
Customizing or restricting the dot-directory list
dotDirs accepts any ordered list of directory names. Names earlier in the list also win for tooling writes (sessions, checkpoints, config.json, etc.) — see the Dot Directories section below.
// Only scan .noumen (ignore .claude entirely):
options: {
projectContext: { cwd: "/my/project", dotDirs: { names: [".noumen"] } },
}
// Use a custom name first, fall back to .noumen:
options: {
projectContext: { cwd: "/my/project", dotDirs: { names: [".mine", ".noumen"] } },
}CLAUDE.md compatibility
If your project already has a CLAUDE.md or .claude/ directory, noumen picks it up automatically — no renaming needed. When both NOUMEN.md and CLAUDE.md exist in the same directory, both are loaded. This lets you maintain compatibility with Claude Code while also using noumen-specific instructions.
Rules directories
For fine-grained control, add markdown files under .noumen/rules/ or .claude/rules/:
my-project/
NOUMEN.md # main project instructions
.noumen/
rules/
style.md # coding style rules
testing.md # testing conventions
api/
rest.md # REST API patternsEach .md file in the rules directory is loaded as a separate context file. Subdirectories are scanned recursively.
Conditional rules
Rules can target specific file patterns using paths: frontmatter, just like Skills:
---
paths:
- "src/**/*.tsx"
- "src/**/*.jsx"
---
When working on React components:
- Use functional components with hooks
- Co-locate tests in __tests__ directories
- Export component props as a named typeThis rule only activates when the agent reads or edits files matching those globs. Conditional rules keep the system prompt focused — the agent only sees instructions relevant to the files it is working on.
@ includes
Context files can reference other files using inline @path syntax:
# Project instructions
See @./docs/architecture.md for the system design.
See @./docs/conventions.md for coding standards.Paths are resolved relative to the including file. The @ reference is detected anywhere in prose (outside fenced code blocks and inline code spans), and the referenced file's contents are loaded and attached to the parent.
- Maximum include depth: 5 levels (configurable via
maxIncludeDepth) - Cycles are detected and skipped automatically
- Missing referenced files are silently ignored
Configuration
Pass true for sensible defaults, or a ProjectContextConfig object for full control:
options: {
// Simple: use defaults (cwd from sandbox, load everything)
projectContext: true,
// Full control:
projectContext: {
cwd: "/my/project",
homeDir: "/home/user", // for sandboxed environments
managedDir: "/etc/noumen", // enterprise managed settings
dotDirs: { names: [".noumen", ".claude"] }, // which dot-dir names to scan
loadUserContext: true, // load from homeDir (default: true)
loadProjectContext: true, // load from cwd ancestors (default: true)
loadLocalContext: true, // load *.local.md files (default: true)
excludes: ["**/secret.md"], // glob patterns to skip
maxIncludeDepth: 5, // @ include depth limit (default: 5)
},
}ProjectContextConfig fields
| Field | Type | Default | Description |
|---|---|---|---|
cwd | string | required | Working directory (project root) |
homeDir | string | — | User home directory. Pass explicitly for sandboxed environments. |
managedDir | string | — | Enterprise/MDM settings directory |
dotDirs | DotDirConfig | { names: [".noumen", ".claude"] } | Ordered list of dot-dir names to scan. Each name produces matching <NAME>.md / .<name>/rules/ variants. |
loadUserContext | boolean | true | Load user-scope context from homeDir |
loadProjectContext | boolean | true | Load project-scope context from cwd ancestors |
loadLocalContext | boolean | true | Load *.local.md files |
excludes | string[] | [] | Glob patterns to exclude |
maxIncludeDepth | number | 5 | Maximum @ include recursion depth |
Dot directories
Noumen uses "dot directories" — .noumen/ and .claude/ by default — for agent-managed state: skills, rules, sessions, checkpoints, tool-result spillover, worktrees, OAuth tokens, and the CLI config.json. The same ordered list controls both reads (first match wins, with later fallbacks) and writes (always to dotDirs.names[0]).
Configure the list once at the Agent level and every subsystem follows:
const agent = new Agent({
provider,
sandbox,
options: {
// Default — same as omitting this field:
dotDirs: { names: [".noumen", ".claude"] },
// Or: write to .mine, fall back to .noumen and .claude for reads:
// dotDirs: { names: [".mine", ".noumen", ".claude"] },
},
});Reads (NOUMEN.md/CLAUDE.md, skills, config.json, OAuth tokens) check each directory in order. Writes (new sessions, checkpoints, noumen init, token refresh) always target dotDirs.names[0]. Dangerous-path protection in the tool permissions pipeline is also built from this list — every configured name is treated as protected.
ContextFile type
Each loaded context file is represented as:
interface ContextFile {
path: string;
scope: "managed" | "user" | "project" | "local";
content: string;
globs?: string[]; // from paths: frontmatter (conditional rules)
includes?: ContextFile[]; // files referenced via @
}Standalone usage
You can use the loader and formatter independently of Agent, for example to build your own prompt pipeline:
import { loadProjectContext, buildProjectContextSection } from "noumen";
import { LocalFs } from "noumen/local";
const fs = new LocalFs({ basePath: "/" });
const files = await loadProjectContext(fs, {
cwd: "/my/project",
homeDir: "/home/user",
});
const promptSection = buildProjectContextSection(files);
// Insert into your own system promptThe filterActiveContextFiles helper filters conditional rules by touched file paths:
import { filterActiveContextFiles } from "noumen";
const active = filterActiveContextFiles(files, ["/my/project/src/App.tsx"], "/my/project");
const section = buildProjectContextSection(active);