Mo Sharif
← ~/blog

CLAUDE.md Best Practices: The File I Actually Ship

The CLAUDE.md in this repository is 35 lines long. I know the exact number because I checked before writing this, and because the length is most of the point: it is the shortest file I could write that stops me re-explaining the same handful of things at the start of every session.

A CLAUDE.md file

is a markdown file that Claude Code loads into context at the start of every session, giving an agent the project facts it would otherwise have to rediscover, guess at, or ask you for.

The most useful sentence I have read about these files is in Anthropic's own memory documentation, current as of July 2026: CLAUDE.md content is delivered as a user message after the system prompt rather than as part of it, and Claude treats it as "context, not enforced configuration." That explains most of the disappointment people have with these files. You are not writing config. You are writing the top of a conversation, and every line you add competes for attention with the actual task.

Here is the whole file, then the reasoning behind it.

A CLAUDE.md file is a plain markdown file that Claude Code reads at the start of every session to learn project-specific facts: build commands, conventions, layout, and traps. It loads automatically from a small set of known locations, and it shapes behaviour rather than enforcing it.

Four locations matter, and they load from broadest scope to most specific. Everything discovered gets concatenated into context rather than overriding what came before, so a project instruction lands after a user instruction and gets read last.

ScopeWhere it livesWhat belongs in itWho sees it
Managed policy/Library/Application Support/ClaudeCode/CLAUDE.md on macOS, /etc/claude-code/CLAUDE.md on LinuxOrg-wide standards, compliance remindersEvery session on the machine
User~/.claude/CLAUDE.mdYour personal preferences across every projectJust you, everywhere
Project./CLAUDE.md or ./.claude/CLAUDE.mdBuild commands, layout, conventions, invariantsYour team, via source control
Local./CLAUDE.local.mdSandbox URLs, preferred test data. Add it to .gitignoreJust you, in this repo

Files in directories above your working directory load in full at launch. Files in subdirectories below it load on demand, when Claude actually reads something in that directory.

The part people skip is the enforcement gap. If a rule genuinely must hold every time, prose will not get you there — a PreToolUse hook will, because it runs as a shell command regardless of what the model decides. This is the same lesson I learned the hard way getting schema-valid JSON out of a language model: you stop asking nicely and start constraining.

Below is this repository's CLAUDE.md in full — four sections, 35 lines, 387 words, nothing redacted. It governs the site you are reading right now.

Markdown
# codelit-io — Mo Sharif's portfolio (mosharif.me)

Next.js 16 App Router + Once UI ("Magic Portfolio" base). Personal portfolio — distinct from the Codelit SaaS repo (`codelit-gatsby`).

## Commands

- `npm run dev` — dev server
- `npm run build` — production build (also type-checks)
- `npm run type-check` — tsc only
- `npm run lint` / `lint:fix` — ESLint 9 flat config (`eslint src`; `next lint` no longer exists in Next 16)
- `npm run quality` — type-check + lint + format check + build

## Architecture

- `src/resources/content.js` — the "CMS": all copy, person/social data, page metadata. Edit copy here, not in pages.
- `src/resources/once-ui.config.js` — theme/style/effects config, routes map (RouteGuard reads it), fonts.
- `src/resources/custom.css` — the entire design system ("Workspace OS"). Theme-scoped CSS vars under `[data-theme="dark"|"light"]`: `--ink-*` (text), `--hairline*` (borders), `--chip-*`, `--panel*`, `--brand-hi`, `--spot`. New UI should use these vars so both themes work with one rule.
- `src/app/blog/posts/*.mdx`, `src/app/work/projects/*.mdx` — content collections read by `getPosts()` (`src/utils/utils.ts`).
- OG images: `/api/og/generate?title=...` edge route. RSS: `/api/rss`.

## Design language ("Workspace OS")

Portfolio-as-dev-environment: terminal nav tabs, mono "filename" card headers, glass bento cards, violet/cyan palette.

- **⌘K command palette** — `CommandPalette.tsx`, opened by ⌘K, `/`, or `window.dispatchEvent(new Event("cmdk:open"))` (via `CmdkButton`). Add new destinations to its `ACTIONS` array.
- **Scroll reveal** — put `data-reveal` on any section; `PageEffects.tsx` (mounted in layout) reveals it on intersection. Hiding is gated on `html.js` (class set by the inline theme-init script in `layout.tsx`) so no-JS visitors see content. `.bento-grid[data-reveal]` staggers children instead of fading the container.
- **Cursor spotlight** — `.bento-card` gets `--mx/--my` from a delegated pointermove listener in `PageEffects.tsx`; the glow is `.bento-card::after`.
- **Page headers** — use `PageHeader` (path chip + ordinal + display title; wrap a word in `<span className="ink">` for gradient).

## Gotchas

- `RouteGuard` must stay synchronous. It used to gate children behind a mount effect — caused a spinner flash on every load and an empty DOM for sibling effects at hydration.
- Hydration-sensitive UI (clocks etc.) goes client-side like `LocalTime.tsx` (render placeholder, fill in effect).
- The repo has both `package-lock.json` and `pnpm-lock.yaml`; npm is what's been used/tested.
- Visual verification: no Playwright dep here — borrow `playwright-core` from a sibling repo's node_modules with globally cached browsers; when screenshotting, scroll in ≤350px steps with ≥100ms delays or `data-reveal` content stays at opacity 0.

Because a line in a CLAUDE.md costs tokens in every session forever and buys you only the corrections it prevents. A line earns its place when it changes what an agent would otherwise do by default. A line that merely describes the repository does not, because the repository is right there.

Sort the file by that test and five categories fall out.

Line, abbreviatedCategoryWhat it prevents
eslint src; next lint no longer exists in Next 16A tool default that changedAn agent reaching for next lint, which Next.js removed in version 16, then "fixing" the lint script back to something that cannot run
content.js is the CMS — edit copy here, not in pagesA single source of truthCopy edited directly in a page component, which looks correct in the diff and silently diverges from the file that actually feeds every other surface
New UI should use the theme-scoped CSS vars so both themes work with one ruleA naming and token conventionHard-coded hex values that look right in dark mode and unreadable in light, discovered by a visitor rather than by me
RouteGuard must stay synchronousA load-bearing invariantA refactor that reintroduces a bug already fixed once
Both lockfiles exist; npm is what's been used and testedAn environment ambiguityAn install with the wrong package manager, producing a lockfile nobody wanted in a diff nobody asked for
Borrow playwright-core from a sibling repo; scroll in small steps when screenshottingAn undiscoverable local workflowTwenty minutes of an agent installing a browser stack, then screenshots of sections sitting at opacity 0

Notice what is absent. There is no code style section, no "always write tests", no instructions about tone or thoroughness. ESLint, Prettier and tsc already enforce the first, npm run quality runs all of them in one command, and the rest is decoration that dilutes the lines around it.

One line in my CLAUDE.md exists because of a specific bug in this repository, and it is the line I would keep if I could keep only one:

RouteGuard must stay synchronous. It used to gate children behind a mount effect — caused a spinner flash on every load and an empty DOM for sibling effects at hydration.

RouteGuard decides whether a route is allowed to render. Gating its children behind a mount effect is a reasonable-looking pattern that produces two symptoms no diff will show you: a spinner flashes on every single page load, and the effects mounted alongside it hydrate against a DOM with nothing in it. Scroll reveal and the cursor spotlight both need elements to exist at hydration time.

Left undocumented, the tidiest available refactor is exactly the one that broke it. That is the shape of a line worth writing — a fix whose reason is invisible in the code that contains it.

Most repositories have several of these and write down none of them. The layout engine on Codelit's architecture canvas went through six rewrites, and the config that finally shipped looks arbitrary until you know which five layouts it replaced. A fresh session has none of that history, and neither does a new hire, which is a decent test for whether a line belongs.

The other line worth writing is a pointer that saves a search. Mine says the ⌘K palette lives in CommandPalette.tsx and that new destinations go in its ACTIONS array. I have argued before that a command palette is the cheapest retention feature you can ship; it is also the feature an agent will cheerfully rebuild from scratch if you never say where it already lives.

Anything an agent can work out by reading the repository. Anthropic ships this heuristic itself: the doctor checkup, from Claude Code v2.1.206 onward, proposes trims to a checked-in CLAUDE.md by cutting directory layouts, dependency lists and architecture overviews, while keeping pitfalls, rationale, and conventions that differ from tool defaults.

Apply that honestly to my own file and the weakest section is the design language one. A capable agent could open custom.css and infer the palette without my help. What keeps those bullets alive is the second half of each: the event name that opens the palette, the class that gates hiding so no-JS visitors still see content, the detail that a bento grid staggers its children instead of fading the container. Those are decisions. The sentences around them are descriptions, and the descriptions are what would go if I had to halve the file.

The architecture section sits on the same line. A list of file paths is derivable. "Edit copy here, not in pages" is not.

Four things I keep out of the file entirely:

  • Anything ESLint, Prettier or the type checker already enforces. The tool is the enforcement; prose is a second copy that will drift.
  • Tone, persona, and encouragement. It reads as instruction and behaves as noise.
  • Secrets, sandbox URLs, personal test data. Those go in a gitignored CLAUDE.local.md, which loads alongside the main file and stays out of the repo.
  • Anything you find yourself writing as ALWAYS or NEVER in capitals. If you are shouting at a context file, you have found a hook.

Claude Code reads CLAUDE.md and not AGENTS.md — Anthropic's memory docs state that plainly as of July 2026. If your repository already has an AGENTS.md, the documented bridge is a CLAUDE.md whose first line imports it, with any Claude-specific instructions written underneath.

The context worth having: AGENTS.md is an open format for guiding coding agents, stewarded by the Agentic AI Foundation under the Linux Foundation, and as of July 2026 its site reports use by over 60,000 open-source projects, with native support listed from Codex, Cursor, GitHub Copilot's coding agent, Gemini CLI, Zed, Aider, Windsurf and Jules among others. It is the closest thing the space has to a shared convention, and Claude Code is the conspicuous holdout.

SetupWho reads whatBest forWhere it breaks
CLAUDE.md onlyClaude CodeSolo repos, or teams standardised on one agentEvery other agent starts from zero
AGENTS.md onlyEverything except Claude CodeRepos where Claude Code is not in the toolchainClaude Code ignores the file entirely
AGENTS.md plus a CLAUDE.md that imports itBoth, from one sourceMixed toolchains, most open sourceTwo files to keep honest, and the import path has to resolve
CLAUDE.md symlinked to AGENTS.mdBoth, identicallyTeams with nothing Claude-specific to saySymlinks on Windows need Administrator or Developer Mode, and there is nowhere to put Claude-only rules

Full disclosure on my own setup: this repo has a CLAUDE.md and no AGENTS.md, which is the least defensible row in that table for anyone but me. I am the only person in the repository, the same solo constraint that shaped how Codelit got built, and Claude Code is the only agent I run against it. The day a second agent touches this codebase, the import row is the one I move to.

Two agents reading two different instruction files drift apart, and the repository quietly acquires two versions of the truth: the file you updated and the file you forgot. The failure is never dramatic. One agent knows the lint command changed, the other keeps reaching for the old one, and you spend a week wondering why results vary by tool.

The import fixes that by making one file canonical and the other a wrapper. Put the shared content in AGENTS.md, make CLAUDE.md a single import line plus whatever is genuinely Claude-specific, and there is only one place a stale instruction can live. When you want to know what actually loaded rather than what should have, run the context command and read the memory files list; there is also an InstructionsLoaded hook that logs which instruction files loaded and when.

Anthropic's documented target is under 200 lines per CLAUDE.md file, on the stated grounds that longer files consume more context and reduce adherence. Mine is 35. I would not defend 35 as an optimum, but I would defend the direction, because the failure mode of a long file is invisible.

What drives files past 200 lines is fear. Something dumb happens, you add a line to prevent it, and the line is free in isolation and expensive in aggregate. Adherence is not additive: every rule guarding against an unlikely mistake makes the rules guarding against likely ones a little quieter.

The tempting escape hatch does not work. Imports organise a file without shrinking its footprint, because imported content still loads at launch, recursing up to four hops deep. The mechanism that genuinely defers is path-scoped rules — markdown files in .claude/rules/ with a paths glob in their frontmatter, which only enter context when Claude reads a matching file. That is the right home for a rule that only applies to your API handlers or your migrations.

A fact that is true in every session belongs in CLAUDE.md, a procedure that only matters occasionally belongs in a skill, a rule scoped to one part of the tree belongs in a path-scoped rule, and anything that must happen whether or not the model agrees belongs in a hook. One question sorts them: when does this need to be true?

MechanismWhen it loadsCan the model skip itBest for
CLAUDE.mdEvery session, in fullYes — it is contextBuild commands, conventions, invariants, traps
Rules with a paths globWhen Claude reads a matching fileYesRules that only apply to one area of the codebase
SkillOn demand, when invoked or judged relevantYesMulti-step procedures that do not belong in every session
HookAt a fixed lifecycle eventNo — it is a shell commandFormat gates, test gates, blocking a path outright

The rule of thumb I use: if I am writing a sentence that assumes the agent will remember it under pressure, I am writing the wrong artifact. Instructions shape behaviour. Hooks decide it.

Run the init command, then delete most of what it produces. The generated file is a good description of the repository, and a description is precisely what the agent can already get for itself. What you want is the difference between your repository and an agent's defaults: the commands that are not standard, the conventions that are not obvious, the fixes whose reasons are not in the code.

After that, add exactly one line each time you correct the same thing twice. Once is noise. Twice is a pattern, and a pattern is cheaper as a line in a file than as a correction you retype forever.

The side effect is that the file turns into a changelog of your own repeated mistakes, which makes deletion easy — when a correction stops being necessary, the line goes. Mine is 35 lines because I keep asking which of them I would still bet money on. Most files I see have never been asked that question once.

Questions people actually ask

What is a CLAUDE.md file and when does Claude Code read it?
A CLAUDE.md file is a markdown file of project instructions that Claude Code loads at the start of every session. It can live at the project root, under a .claude directory, in your home directory for personal preferences, or at a managed policy location for an entire organisation. Anthropic's memory documentation is explicit that the content arrives as a user message after the system prompt, and that it is treated as context rather than as enforced configuration.
Does Claude Code read AGENTS.md?
No. Anthropic's memory documentation states plainly that Claude Code reads CLAUDE.md and not AGENTS.md. If your repository already uses AGENTS.md for other coding agents, the documented bridge is to create a CLAUDE.md whose first line imports it with an at-sign path reference, then add any Claude-specific instructions underneath. A symlink from CLAUDE.md to AGENTS.md also works, although on Windows that requires Administrator privileges or Developer Mode.
How long should a CLAUDE.md file be?
Anthropic's guidance is to target under 200 lines per file, because longer files consume more context and reduce how consistently the instructions get followed. The file I ship in my portfolio repository is 35 lines. Splitting a long file into imports organises it but does not shrink it, since imported files still load at launch. Path-scoped rules are the only mechanism that genuinely defers loading until a matching file is opened.
What should you not put in a CLAUDE.md file?
Anything the agent can work out by reading the repository. Anthropic's own trim heuristic, shipped in the doctor checkup, cuts directory layouts, dependency lists and architecture overviews, and keeps pitfalls, rationale and conventions that differ from tool defaults. Also leave out anything your linter or type checker already enforces, tone and persona instructions, and private values such as sandbox URLs, which belong in a gitignored CLAUDE.local.md instead.
Why is Claude ignoring my CLAUDE.md?
Usually one of three reasons. The file is not in a location that loads for your session, which you can check by running the context command and reading the memory files list. The instruction is vague enough to be interpreted away, so write something verifiable instead. Or two instruction files contradict each other, in which case the model may pick one arbitrarily. Anything that must run every time belongs in a hook rather than in prose.
Should a rule go in CLAUDE.md, a skill, or a hook?
Put it in CLAUDE.md if it is a fact that is true in every session, such as a build command or a naming convention. Put it in a skill if it is a multi-step procedure that only matters occasionally, because skills load on demand instead of every session. Put it in a hook if it must happen regardless of what the model decides, because hooks run as shell commands at fixed lifecycle events.