Skip to content

Instantly share code, notes, and snippets.

@Drizzt321
Last active April 28, 2026 17:26
Show Gist options
  • Select an option

  • Save Drizzt321/a4c567f0d263701152bf4e13c17855f1 to your computer and use it in GitHub Desktop.

Select an option

Save Drizzt321/a4c567f0d263701152bf4e13c17855f1 to your computer and use it in GitHub Desktop.
Loading PAI Steering rules via `@import` rather than SessionStart hook

PAI Context Loading via CLAUDE.md @include — Architecture & Evidence

Audience: PAI users considering restructuring how steering rules and behavioral context reach the model. Scope: How Claude Code loads context, where PAI's current architecture falls short, and how the @include directive in CLAUDE.md resolves the authority gap. Out of scope: Defending the trust-chain files against prompt-injected writes. That threat is real and independent — covered in a separate document (pai-trust-chain-integrity-guard.md, in development). This guide is about authority routing (the model follows the rules it already has), not integrity (the rules don't get rewritten under the model). Evidence base: Source-code-level analysis of the Claude Code TypeScript implementation. File/line citations (e.g., claudemd.ts:89) refer to Claude Code internals and are included so claims can be independently verified.


1. Summary

PAI's most important behavioral rules — the "never assert without verification," "surgical fixes only," "read before modifying" style constraints in PAI/AISTEERINGRULES.md — are loaded at session start via a SessionStart hook. That hook emits them inside a <system-reminder> block in the messages array. Meanwhile, CLAUDE.md (which holds output format templates and mode routing) is loaded via a different <system-reminder> block that carries an explicit operator-delegation prefix: "These instructions OVERRIDE any default behavior and you MUST follow them exactly as written."

This is an authority inversion. The less-critical content (format templates) arrives with the stronger authority signal; the more-critical content (behavioral rules) arrives with the weaker one.

Claude Code provides an @include directive inside CLAUDE.md that inlines referenced files at parse time. Inlined content inherits the operator-delegation prefix of the enclosing CLAUDE.md block. Promoting steering rules into CLAUDE.md via @include is the only mechanism that both auto-loads the content and gives it the same delegated authority as CLAUDE.md's own contents.

The change is token-neutral — the same bytes move from one channel to another — but the authority wrapper changes, and the content now survives context compaction the way CLAUDE.md does.


2. How Claude Code Loads Context

2.1 System prompt vs messages — the core distinction

Claude Code assembles two separate payloads for the Anthropic Messages API: a system parameter and a messages array. PAI content enters through both channels, and the channel matters for authority.

system parameter (built by getSystemPrompt() at constants/prompts.ts:444):

  • Cacheable sections: attribution, core capabilities, tool guidelines, safety, output style
  • Dynamic sections: session guidance, memory prompt, environment info
  • System context: git status, cache breaker (appended via appendSystemContext() at utils/api.ts:437)

messages array:

  • User context prepended as first message via prependUserContext() at utils/api.ts:449
  • Wrapped in <system-reminder> XML tags
  • Contains CLAUDE.md content and current date
  • Followed by: actual conversation messages, tool calls/results

Key insight: Trust is about who is speaking, not where the bytes land. The Anthropic authority hierarchy distinguishes Anthropic (trained values) / Operators (Claude Code itself) / Users. The system parameter provides positional primacy and prompt caching, but no documented architectural privilege over messages content. The operator can delegate authority to user content — and does, via the CLAUDE.md prefix described next.

2.2 CLAUDE.md loading and the operator-delegation prefix

Discovery algorithm (utils/claudemd.ts), lowest to highest priority:

  1. Managed memory (/etc/claude-code/CLAUDE.md or platform equivalent)
  2. User memory (~/.claude/CLAUDE.md)
  3. Project memory (walks up from CWD): CLAUDE.md, .claude/CLAUDE.md, .claude/rules/*.md
  4. Local memory (CLAUDE.local.md, closest to CWD)

Injection point. CLAUDE.md content is injected into the messages array (NOT the system prompt) via prependUserContext() as a <system-reminder> block:

<system-reminder>
As you answer the user's questions, you can use the following context:
# claudeMd
Codebase and user instructions are shown below. Be sure to adhere to these instructions.
IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.
[CLAUDE.md content here]
# currentDate
Today's date is YYYY-MM-DD.
IMPORTANT: this context may or may not be relevant to your tasks...
</system-reminder>

The phrase "These instructions OVERRIDE any default behavior and you MUST follow them exactly as written" is hardcoded in claudemd.ts:89. This is the operator-delegation prefix — Anthropic's Claude Code explicitly telling the model to treat the wrapped content as operator-level instructions. It applies to everything inside this specific <system-reminder> block, and nothing else.

2.3 The @include directive

  • Syntax: @path, @./relative, @~/home, @/absolute
  • Resolution: Relative to the base path of the including file. ~/.claude/CLAUDE.md using @PAI/AISTEERINGRULES.md resolves to ~/.claude/PAI/AISTEERINGRULES.md.
  • Max depth: 5 levels of nested @include
  • File filter: File must have an extension in TEXT_FILE_EXTENSIONS (claudemd.ts:96)
  • Inlined at parse time: the content becomes part of the CLAUDE.md string before injection, which means it lands inside the same <system-reminder> block as CLAUDE.md itself and inherits the operator-delegation prefix
  • Silent skip on missing targets: claudemd.ts:404-405 treats ENOENT (file doesn't exist) and EISDIR (is a directory) as expected conditions; claudemd.ts:652 returns empty content for empty/missing files. Missing @include targets do not error — they silently resolve to nothing. This is load-bearing for shipping stock configurations that reference optional files.
  • Context: works only in text nodes (not inside code blocks)

@include is the only auto-loading mechanism in Claude Code that carries operator delegation to content stored in a separate file.

2.4 Hook-injected <system-reminder> blocks — no delegation

Hooks defined under settings.json → hooks.SessionStart execute at session startup. Their stdout is captured and injected into the messages array as separate initial messages. When a hook emits text wrapped in <system-reminder> tags, Claude Code surfaces it to the model as system-injected context.

These hook-emitted <system-reminder> blocks are separate from the CLAUDE.md one. They do NOT inherit the operator-delegation prefix. The model is instructed (by the core Claude Code system prompt) to pay attention to <system-reminder> tags, but the explicit "OVERRIDE any default behavior" delegation only applies to the CLAUDE.md block.

PAI's LoadContext.hook.ts uses this mechanism to force-load files listed in settings.json → loadAtStartup.files, plus dynamic context (relationship, learning, active work summaries). Everything loaded this way sits at this weaker authority tier.

2.5 Compaction survival

CLAUDE.md content has a second property that hook-injected content lacks: it survives context compaction. Claude Code re-injects CLAUDE.md via prependUserContext() on every turn as part of the user-context prefetch (context.ts:155, memoized). When the conversation gets long enough for Claude Code to compact prior messages, CLAUDE.md content is re-emitted fresh rather than being summarized along with conversation history.

Hook-emitted <system-reminder> blocks are treated as regular initial messages. When compaction runs, they can be summarized or dropped — which is why the PAI ecosystem has seen multiple community efforts to build "PostCompact recovery hooks" that re-inject state after compaction events. Users have independently observed that behavioral constraints loaded via hooks degrade in long sessions in a way that CLAUDE.md-loaded content does not.

Promoting steering rules into CLAUDE.md via @include means they automatically get the same compaction-survival guarantee as the rest of CLAUDE.md, with no additional recovery infrastructure. For long-running sessions — which is where rule-skipping tends to compound — this is a concrete behavioral improvement beyond the authority elevation.

2.6 Timing: getUserContext() runs before SessionStart hooks

getUserContext() at context.ts:155 is memoized and executes as a prefetch during request preparation, before SessionStart hooks run. That means Claude Code has already read CLAUDE.md, resolved all @include directives, and queued the combined content for injection by the time any SessionStart hook fires.

Implications:

  • Any SessionStart hook that regenerates CLAUDE.md (e.g., a template build step) is writing for the next session, not the current one. The current session already has the pre-existing CLAUDE.md loaded.
  • This same timing fact has implications for integrity-defense mechanisms, which are covered in the separate integrity-guard document.

2.7 Memory system (MEMORY.md)

  • Location: ~/.claude/projects/<sanitized-git-root>/memory/MEMORY.md (auto memory index)
  • Injection point: the system prompt (not messages), via loadMemoryPrompt() at memdir/memdir.ts:419, called from getSystemPrompt() as systemPromptSection('memory', ...) at prompts.ts:495
  • Truncation limits: 200 lines OR 25KB, whichever hits first; truncated at the last newline boundary
  • Freshness: memories older than 1 day get a staleness caveat wrapped in <system-reminder> tags

MEMORY.md has positional advantage (system prompt, early in context) but is an index — pointers to memory files with one-line descriptions. Individual memory files are not auto-loaded; the model must choose to Read them.

The 200-line cap makes MEMORY.md impractical as a carrier for large rule sets, which is why @include from CLAUDE.md (no documented cap) is the right mechanism for steering rules.


3. Authority Model

3.1 Principal hierarchy

  1. Anthropic — highest; trained into model weights via constitutional AI
  2. Operators — API users and developers (Claude Code itself is the operator here); typically communicate via system prompt, but the Anthropic guidance explicitly notes operators may also inject text into the human turn
  3. Users — end users; typically communicate via messages

3.2 What the system parameter provides — and doesn't

Provides:

  • Positional primacy (first in context window, attention advantage)
  • Role association (conventionally where operator instructions live)
  • Persistence across turns
  • Prompt caching (performance optimization)

Does NOT provide:

  • No documented architectural privilege over messages content
  • No evidence of different attention weights or a separate processing pathway
  • Anthropic's own prompt-engineering guidance recommends task-specific instructions in the user turn, not the system prompt
  • The system prompt is NOT a security boundary — Anthropic explicitly recommends defense in depth

Trust is about speaker identity, not API parameter. When Claude Code injects CLAUDE.md with the "OVERRIDE" prefix, it is the operator explicitly delegating authority to user-authored content in a user-turn message. That delegation is structurally meaningful — it's an operator statement the model is trained to weight — but it is per-block, not global.

3.3 The operator-delegation gap

Hook-injected <system-reminder> blocks are recognized by the model as system-injected context, but they lack the explicit "OVERRIDE" delegation. This is the gap:

Channel Auto-loaded? Operator delegation? Survives compaction?
System prompt (Claude Code core) yes yes (implicit) yes
System prompt → MEMORY.md index yes yes (implicit) yes
Messages → CLAUDE.md (+ @include contents) yes yes (explicit) yes
Messages → hook-emitted <system-reminder> yes no no
Prose-referenced files ("read X for details") no no n/a

The strongest auto-loaded authority available to user-authored content is CLAUDE.md (and, transitively, its @include targets). Anything loaded via a hook sits one tier below and is additionally vulnerable to compaction.


4. PAI Architecture — Before

Current layout

CLAUDE.md (WITH operator delegation — "OVERRIDE any default behavior")
  └── Mode routing, output formats, critical rules, context routing prose ref

Hook: loadAtStartup (NO operator delegation — raw <system-reminder>)
  ├── PAI/AISTEERINGRULES.md          ← system-level behavioral rules
  ├── PAI/USER/AISTEERINGRULES.md     ← personal behavioral rules (if present)
  └── PAI/USER/PROJECTS/PROJECTS.md   ← project registry

Hook: dynamic context (NO operator delegation — raw <system-reminder>)
  └── Relationship context, learning readback, active work summary

MEMORY.md (system prompt, 200-line cap)
  └── Index of memory files (pointers only)

Model discretion (not auto-loaded)
  └── CONTEXT_ROUTING.md, Algorithm, DA identity, individual memories

Note on stock layout: stock PAI v4.0.3 ships PAI/AISTEERINGRULES.md at the system level. PAI/USER/AISTEERINGRULES.md is a common personal extension but is not part of the stock install — users may or may not have one.

Problems

  1. Authority inversion. The most important behavioral constraints sit at the weakest auto-loaded tier. Output format templates in CLAUDE.md carry full operator delegation; behavioral rules in the hook block do not.
  2. Compaction fragility. Steering rules loaded via hooks can be summarized or dropped during compaction in long sessions — exactly the sessions where rule-skipping is most likely to compound.
  3. Prose indirection for critical files. CLAUDE.md refers to context routing and the Algorithm file via prose instructions ("read this file"). These work but require a tool-call round trip per session and depend on the model choosing to comply.
  4. Steering rules and format templates loaded through separate mechanisms. A reader of CLAUDE.md alone can't see the rules that constrain behavior; a reader of the hook output alone can't see the format the rules operate inside. The split obscures the full operator-delegated contract.

5. PAI Architecture — After

Proposed layout

CLAUDE.md (WITH operator delegation — all @included content inherits)
  ├── Mode routing, output formats, critical rules
  ├── @PAI/AISTEERINGRULES.md          ← PROMOTED from hook
  └── @PAI/USER/AISTEERINGRULES.md     ← PROMOTED from hook (if present)

Hook: loadAtStartup (NO operator delegation — always-available context)
  └── PAI/USER/PROJECTS/PROJECTS.md    ← STAYS (useful context, not a behavioral rule)

Hook: dynamic context (NO operator delegation — ephemeral, runtime-computed)
  ├── Relationship context
  ├── Learning readback (signal trends, wisdom, failure patterns)
  └── Active work summary

MEMORY.md (system prompt, 200-line cap)
  └── Index of memory files (unchanged)

Model discretion (not auto-loaded)
  ├── CONTEXT_ROUTING.md (unchanged — on-demand lookup table)
  ├── Algorithm, DA identity, skills
  └── Individual memory files

Three-tier model

Tier Mechanism What belongs here
1 @include in CLAUDE.md Behavioral rules, must-obey constraints
2 Hook (loadAtStartup / dynamic) Always-available context, ephemeral runtime-computed data
3 Prose reference / on-demand Read Lookup tables, Algorithm, identity, individual memories

Tier 1 is the only tier with operator delegation. It's for content the model must obey regardless of conversational pressure. It also survives compaction.

Tier 2 is for content the model should always have but doesn't need override authority — project registries, relationship context, learning signals, active work summaries. Hook loading is also the correct mechanism for anything runtime-computed: filtering by confidence threshold, date-based selection, or aggregation across multiple files can't be expressed as a static @include.

Tier 3 is everything else. Prose references in a Tier-1 file can direct the model to read a Tier-3 file on demand (e.g., "Use the Read tool to load PAI/Algorithm/v3.7.0.md"), combining Tier-1 authority for the instruction with Tier-3 on-demand loading for the content.

What moves, what stays

  • PAI/AISTEERINGRULES.md and PAI/USER/AISTEERINGRULES.md (if present) move from loadAtStartup.files into CLAUDE.md via @include. They must be removed from loadAtStartup.files to avoid double-loading.
  • PAI/USER/PROJECTS/PROJECTS.md stays in loadAtStartup.files. It's context, not a behavioral rule. It shouldn't gain operator-override authority just because it needs to be available.
  • Dynamic context (relationship, learning, active work) stays hook-loaded because it's runtime-computed. As the learning loop closes, processed learnings should graduate from the dynamic block into PAI/USER/AISTEERINGRULES.md (Tier 1), leaving only unprocessed signals in the hook-loaded delta.
  • CONTEXT_ROUTING.md stays on-demand. Inlining it would save one Read call per session at the cost of roughly 275 tokens every turn — not worth it.

Token budget

Promoting steering rules from hook to @include is token-neutral. The same content lands in the same context window; only the authority wrapper changes. CLAUDE.md's reported size grows (the content is now inside it), and the hook's reported output shrinks by the same amount. Total per-turn cost is unchanged.

Claude Code has no documented size cap on CLAUDE.md (unlike MEMORY.md's 200-line / 25KB cap). Very large CLAUDE.md files can dilute attention, but steering rules at typical PAI sizes (low-thousands of tokens) sit well within reasonable bounds.

@include path-resolution verification

Claude Code resolves @include paths relative to the including file's directory (verified in claudemd.ts). ~/.claude/CLAUDE.md containing @PAI/AISTEERINGRULES.md resolves to ~/.claude/PAI/AISTEERINGRULES.md. No absolute paths required.

Template-variable compatibility

If CLAUDE.md is generated from a template by a build step using {{VAR}} or {VAR} substitution, the @ prefix used by @include directives is not matched by those patterns and will pass through unchanged. Add @include lines to the template; the build step emits them literally, Claude Code resolves them at query time.

Order of operations:

  1. Template build writes CLAUDE.md with @PAI/... directives as literal text
  2. Claude Code reads CLAUDE.md, resolves @include, inlines referenced files
  3. Inlined content is NOT re-processed by the template build — it already ran

This means template-variable placeholders inside the @included files (e.g., {PRINCIPAL.NAME} as a prose reference telling the model what name to use) appear as literal strings. If the existing architecture relies on that literal appearance, behavior is unchanged.

Stock-safe shipping of optional @include targets

Because @include silently skips missing files (§2.3), a stock CLAUDE.md can safely contain @PAI/USER/AISTEERINGRULES.md even if the file doesn't ship in the stock install. Users who create the file get it auto-loaded; users who don't get nothing — no error, no warning, no broken CLAUDE.md. This matches how PAI already treats PAI/USER/ as a user-created extension directory.


6. Migration — Converting an Existing PAI Install to @include

This section is a step-by-step migration guide for an existing stock PAI v4.x install. It assumes the standard layout shipped in Releases/v4.0.3/.claude/:

  • CLAUDE.md.template is the source of truth for CLAUDE.md
  • hooks/handlers/BuildCLAUDE.ts runs as a SessionStart hook and rebuilds CLAUDE.md from the template each session
  • LoadContext.hook.ts runs as a SessionStart hook and loads the files listed in settings.json → loadAtStartup.files
  • Stock loadAtStartup.files contains PAI/AISTEERINGRULES.md, PAI/USER/AISTEERINGRULES.md, and PAI/USER/PROJECTS/PROJECTS.md

If your install has been customized beyond stock layout, adapt the paths accordingly.

6.1 Pre-flight check

Before touching anything, capture a baseline so you can verify the change was token-neutral and the rules didn't silently drop.

  1. Confirm you're on a stock-compatible layout:

    ls ~/.claude/CLAUDE.md.template ~/.claude/hooks/handlers/BuildCLAUDE.ts
    grep -A3 '"loadAtStartup"' ~/.claude/settings.json

    You should see the template, the build hook, and the loadAtStartup.files block listing the steering-rule files.

  2. Check which steering-rule files actually exist:

    ls -la ~/.claude/PAI/AISTEERINGRULES.md ~/.claude/PAI/USER/AISTEERINGRULES.md 2>&1

    PAI/AISTEERINGRULES.md ships with stock. PAI/USER/AISTEERINGRULES.md is user-created and may not exist — that's fine. @include silently skips missing targets (§2.3), so the directive is safe to add unconditionally.

  3. Capture a before-snapshot inside Claude Code — start a fresh session and run /context and /memory. Save the output. After the migration, CLAUDE.md token count will grow, LoadContext.hook.ts output will shrink by the same amount, and the steering-rule files will appear in /memory as Memory files (they don't today). Total tokens should be unchanged.

  4. Make a backup of the three files you'll touch:

    cp ~/.claude/CLAUDE.md.template ~/.claude/CLAUDE.md.template.bak
    cp ~/.claude/CLAUDE.md ~/.claude/CLAUDE.md.bak
    cp ~/.claude/settings.json ~/.claude/settings.json.bak

6.2 Edit the template

Open ~/.claude/CLAUDE.md.template in your editor. Find the section that looks like this (around line 50 in stock v4.0.3):

### Critical Rules (Zero Exceptions)

- **Mandatory output format** — Every response MUST use exactly one of the output formats above (ALGORITHM, NATIVE, or MINIMAL). No freeform output.
- **Response format before questions** — Always complete the current response format output FIRST, then invoke AskUserQuestion at the end.

---

### Context Routing

Insert a new "Behavioral Rules" section between Critical Rules and Context Routing:

### Critical Rules (Zero Exceptions)

- **Mandatory output format** — Every response MUST use exactly one of the output formats above (ALGORITHM, NATIVE, or MINIMAL). No freeform output.
- **Response format before questions** — Always complete the current response format output FIRST, then invoke AskUserQuestion at the end.

---

### Behavioral Rules

@PAI/AISTEERINGRULES.md

@PAI/USER/AISTEERINGRULES.md

---

### Context Routing

Notes on the @include directives:

  • Paths are relative to the including file's directory. Because CLAUDE.md lives at ~/.claude/CLAUDE.md, @PAI/AISTEERINGRULES.md resolves to ~/.claude/PAI/AISTEERINGRULES.md. No leading ./, no absolute path needed.
  • Each @include must be on its own line, in a text node (not inside a fenced code block — Claude Code does not resolve @include inside code blocks).
  • The blank lines around the directives are not required by Claude Code but improve readability of the rendered CLAUDE.md.
  • The @ prefix is not matched by BuildCLAUDE's {{VAR}} / {VAR} template substitution, so these lines pass through the build unchanged. See §5 "Template-variable compatibility."
  • @PAI/USER/AISTEERINGRULES.md is safe to include unconditionally even if the file doesn't exist on your system — Claude Code silently skips missing targets.

Save the template.

6.3 Rebuild CLAUDE.md

BuildCLAUDE runs automatically at session start, but you should rebuild manually now so you can inspect the output before starting a new session:

bun ~/.claude/hooks/handlers/BuildCLAUDE.ts

Then verify the generated CLAUDE.md contains the @include directives as literal text:

grep -n "^@PAI" ~/.claude/CLAUDE.md

You should see both lines in the output. If you don't, the template edit didn't land or BuildCLAUDE didn't run — re-check the template and re-run the build.

Important: The @ directives appear as literal text in the built CLAUDE.md. Claude Code resolves them at query time when it reads the file, not at build time. Do not try to manually inline the steering-rule content into CLAUDE.md — that would defeat the point of the mechanism and would be overwritten on the next template rebuild.

6.4 Remove the promoted files from loadAtStartup.files

Open ~/.claude/settings.json and find the loadAtStartup block (around line 888 in stock v4.0.3):

"loadAtStartup": {
  "_docs": "Files force-loaded into session context at startup by LoadContext.hook.ts. Paths relative to PAI_DIR. Injected as <system-reminder> blocks.",
  "files": [
    "PAI/AISTEERINGRULES.md",
    "PAI/USER/AISTEERINGRULES.md",
    "PAI/USER/PROJECTS/PROJECTS.md"
  ]
},

Remove the two steering-rule files, leaving PAI/USER/PROJECTS/PROJECTS.md in place:

"loadAtStartup": {
  "_docs": "Files force-loaded into session context at startup by LoadContext.hook.ts. Paths relative to PAI_DIR. Injected as <system-reminder> blocks.",
  "files": [
    "PAI/USER/PROJECTS/PROJECTS.md"
  ]
},

Why PROJECTS.md stays: it's context, not a behavioral rule. It doesn't need operator-override authority; it just needs to be available every session. Hook loading is the correct tier for it (Tier 2 in §5).

Why the steering files must be removed, not just left alongside @include: double-loading wastes tokens and creates two copies of the same content at different authority tiers — exactly the confusion this migration is trying to eliminate. Leave them in @include only.

Save settings.json.

6.5 Verify

Start a fresh Claude Code session (fully exit and re-launch — don't just /clear).

  1. Run /memory. You should now see PAI/AISTEERINGRULES.md and PAI/USER/AISTEERINGRULES.md (if present) listed as Memory files. They were not shown there before — Claude Code reports @include-resolved files in the memory listing, which confirms they're now being loaded through the CLAUDE.md channel with the operator-delegation prefix.

  2. Run /context. Compare against the before-snapshot from §6.1:

    • CLAUDE.md token count has grown by approximately the size of the steering-rule files.
    • The hook output portion (from LoadContext.hook.ts) has shrunk by roughly the same amount.
    • Total tokens should be unchanged — this is the "token-neutral" claim from §5.
  3. Test a behavioral rule. Ask the assistant to do something that should trigger one of the steering rules (e.g., make a claim about file contents without reading them — "Never assert without verification" should kick in). The rule should still fire. If it doesn't, re-check that the @include directives are present in CLAUDE.md and not inside a code block.

  4. Look for the operator-delegation prefix. If you want direct confirmation that the rules are now inside the delegated block, inspect the actual request Claude Code sends (e.g., via Claude Code's debug logging, or by examining the transcript). The steering-rule content should appear inside the <system-reminder> block that begins with "IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written." Before the migration, it appeared in a different <system-reminder> block from a hook, without that prefix.

6.6 Rollback

If anything goes wrong, the rollback is exactly three file operations:

# Restore the template and rebuild
cp ~/.claude/CLAUDE.md.template.bak ~/.claude/CLAUDE.md.template
bun ~/.claude/hooks/handlers/BuildCLAUDE.ts

# Restore settings.json
cp ~/.claude/settings.json.bak ~/.claude/settings.json

Verify that grep -n "^@PAI" ~/.claude/CLAUDE.md returns nothing and that loadAtStartup.files again contains all three entries. Restart Claude Code.

Alternatively, for a non-destructive rollback, you can just revert the template edit and rebuild — leaving the steering files in loadAtStartup.files means they'll be double-loaded temporarily, which is wasteful but not broken.

6.7 Common gotchas

  • @include inside a code block. Claude Code does not resolve @include inside fenced code blocks. If you put the directive inside ``` fences in the template, nothing happens and you'll see no steering rules in /memory. The directives must be in plain text.
  • Wrong path base. Paths resolve relative to the including file's directory, not the current working directory. ~/.claude/CLAUDE.md with @PAI/AISTEERINGRULES.md~/.claude/PAI/AISTEERINGRULES.md. ~/.claude/CLAUDE.md with @./PAI/AISTEERINGRULES.md → same thing. ~/.claude/CLAUDE.md with @/PAI/AISTEERINGRULES.md/PAI/AISTEERINGRULES.md (wrong — that's an absolute path to the filesystem root).
  • Forgot to rebuild. If you edit CLAUDE.md directly (not the template), your changes will be overwritten on the next session start by BuildCLAUDE. Always edit the template.
  • Forgot to remove from loadAtStartup.files. The rules still work, but you're now paying double token cost and holding two copies of the same content at different authority tiers. /context will show the steering rules counted twice; use that as your check.
  • Template-variable placeholders inside steering rules. If PAI/AISTEERINGRULES.md contains literal {PRINCIPAL.NAME} or similar, those will appear to the model as literal strings. This matches the pre-migration behavior (hooks also don't substitute template variables in their loaded files), so it's not a regression — but flag it if you haven't seen it before.
  • Missing file, but not silent. If you mistype a path (e.g., @PAI/STEERINGRULES.md instead of @PAI/AISTEERINGRULES.md), Claude Code silently skips it as if it doesn't exist. No error, no warning. Verify your paths carefully, and use /memory to confirm both files appear after the migration — if one is missing, the directive resolved to nothing.

7. Known Risks and Trade-offs

Consolidation risk

Promoting steering rules into CLAUDE.md via @include creates a single point of failure. Previously, CLAUDE.md parsing failure would lose format instructions but steering rules would still load via the hook; after the change, a parse failure or missing @include target loses both.

Why accept it: the previous "independent loading" isn't true redundancy — it's divergent-authority duplication of the same content at different trust levels. A clean failure from a single authoritative path is architecturally preferable to silent divergence. And @include specifically fails open on missing targets (silent skip), so the common failure mode is "file not found → nothing loaded from that include," which is the same as the current behavior when a hook file is missing.

Attention dilution at large CLAUDE.md sizes

Claude Code has no documented size cap on CLAUDE.md, but very large context windows can dilute model attention across irrelevant content. Steering rules at typical PAI sizes (low-thousands of tokens, plus CLAUDE.md's existing few hundred) sit well within reasonable bounds. If future steering-rule growth pushes CLAUDE.md into the tens of thousands of tokens, the right response is to split content into focused files loaded via multiple @include directives, not to abandon @include.

Trust-chain concentration

A side effect of consolidating behavioral-rule authority into CLAUDE.md + its @include targets is that those files become higher-value targets for prompt-injection attacks delivered via WebFetch, WebSearch, or MCP tool results. This is not a new threat — any file the model can write is already a potential persistence target — but the restructure raises the stakes on the specific files now carrying operator-delegated authority. Addressing that threat is out of scope for this guide; see the separate trust-chain integrity-guard document for the threat model and defense design.


8. Sources

Claude Code source code (file:line references throughout)

Analysis based on inspection of the Claude Code TypeScript implementation. Key entry points cited:

File Purpose
constants/prompts.ts:444 getSystemPrompt() — builds the system prompt array
constants/prompts.ts:495 Where the memory prompt section is assembled
context.ts:116 getSystemContext() — git status and cache breaker
context.ts:155 getUserContext() — CLAUDE.md and current date (memoized prefetch)
utils/api.ts:437 appendSystemContext() — system context → system prompt
utils/api.ts:449 prependUserContext() — user context → messages as <system-reminder>
utils/claudemd.ts CLAUDE.md discovery, @include resolution, operator prefix
utils/claudemd.ts:89 Hardcoded operator-delegation prefix text
utils/claudemd.ts:96 TEXT_FILE_EXTENSIONS filter for @include targets
utils/claudemd.ts:404-405 ENOENT/EISDIR treated as expected — missing include silently skipped
utils/claudemd.ts:652 Empty/missing memory file returns empty info
memdir/memdir.ts:419 loadMemoryPrompt() — MEMORY.md injection into system prompt
services/api/claude.ts:864 anthropic.beta.messages.create() — the actual API call
services/api/claude.ts:1376 buildSystemPromptBlocks() — system prompt → TextBlockParam[]
query.ts:450 Where appendSystemContext is called
query.ts:660 Where prependUserContext is called

Anthropic documentation

  • System prompts — platform.claude.com/docs/en/build-with-claude/prompt-engineering/system-prompts
  • Context windows — platform.claude.com/docs/en/build-with-claude/context-windows
  • Prompting best practices — platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices
  • Mitigate jailbreaks — platform.claude.com/docs/en/test-and-evaluate/strengthen-guardrails/mitigate-jailbreaks
  • Constitutional AI: Harmlessness from AI Feedback — anthropic.com/research/constitutional-ai-harmlessness-from-ai-feedback
  • Effective context engineering for AI agents — anthropic.com/engineering/effective-context-engineering-for-ai-agents

Other references

  • The Claude soul document (publicly discussed by Amanda Askell) — principal-hierarchy framing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment