Replace the per-group "written once at init, owned by the group" CLAUDE.md
with a host-regenerated entry point that imports:
- a shared base (`container/CLAUDE.md` mounted RO at `/app/CLAUDE.md`)
- optional per-skill fragments (skills that ship `instructions.md`)
- optional per-MCP-server fragments (inline `instructions` field in
`container.json`)
- per-group agent memory (`CLAUDE.local.md`, auto-loaded by Claude Code)
Principle: RW = per-group memory, RO = shared content. Source/skills/base
are shared; personality, config, working files, and Claude state stay
per-group.
Key changes:
- New `src/claude-md-compose.ts` — per-spawn composition +
`migrateGroupsToClaudeLocal()` one-time cutover.
- New `container/CLAUDE.md` — shared base, seeded verbatim from the
former `groups/global/CLAUDE.md`.
- `src/container-runner.ts` — swap `/workspace/global` mount for RO
`/app/CLAUDE.md`; call `composeGroupClaudeMd()` after
`initGroupFilesystem()`.
- `src/group-init.ts` — drop `.claude-global.md` symlink + initial
`CLAUDE.md` write; seed `CLAUDE.local.md` from `opts.instructions`.
- `src/index.ts` — call `migrateGroupsToClaudeLocal()` at startup.
- `src/container-config.ts` — add optional `instructions` field to
`McpServerConfig` (inline per-MCP guidance fragment).
- `container/Dockerfile` — drop dead `/workspace/global` mkdir.
- Remove obsolete `scripts/migrate-group-claude-md.ts`.
Migration (runs once at host startup, idempotent):
- Delete `.claude-global.md` symlinks in each group.
- Rename each `groups/<folder>/CLAUDE.md` → `CLAUDE.local.md`
(preserves existing per-group content as memory).
- Delete `groups/global/` directory.
Design docs: `docs/claude-md-composition.md` and `docs/shared-source.md`
(the latter is the sibling design discussion this refactor builds on).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.8 KiB
CLAUDE.md Composition
Compose agent instructions from a shared base, skill/tool fragments, and per-group memory — replacing the current per-group CLAUDE.md with a host-regenerated entry point.
Problem
Today each agent group has a single RW groups/<folder>/CLAUDE.md, written once at init and never updated. Consequences:
- Upstream improvements to shared agent guidance don't propagate to existing groups
- No way to ship tool-specific guidance with the tool itself (e.g., an agent-browser usage fragment)
- Human-authored identity and agent-accumulated memory live in the same file with no separation
- The
.claude-global.mdsymlink +groups/global/CLAUDE.mdpattern handled the shared base but not per-module fragments
Design
Principle: RW = per-group memory, RO = shared content. Same rule that governs the shared-source refactor, applied to agent instructions.
Three tiers
| Tier | File | Location | Mount | Editor | Change rate |
|---|---|---|---|---|---|
| Shared base | CLAUDE.md |
container/CLAUDE.md |
RO at /app/CLAUDE.md |
Owner (via git) | Rare |
| Module fragments | instructions.md |
Inside each module | RO via shared skills mount, or inline in container.json |
Module author | Ships with module |
| Per-group memory | CLAUDE.local.md |
groups/<folder>/ |
RW at /workspace/agent/ |
Agent + owner | Continuous |
| Composed entry | CLAUDE.md |
groups/<folder>/ |
RW but host-regenerated | Host, not human | Every spawn |
Composition
At every spawn, the host regenerates groups/<folder>/CLAUDE.md as an import-only file:
<!-- Composed at spawn — do not edit. Edit CLAUDE.local.md for per-group content. -->
@./.claude-shared.md
@./.claude-fragments/welcome.md
@./.claude-fragments/agent-browser.md
@./.claude-fragments/<enabled-skill-with-fragment>.md
@./.claude-fragments/mcp-<server-name>.md
Symlinks are created alongside, following the .claude-global.md pattern (dangling on host, valid in container via the RO mount):
groups/<folder>/.claude-shared.md→/app/CLAUDE.mdgroups/<folder>/.claude-fragments/<name>.md→/app/skills/<name>/instructions.md(for each enabled skill that ships a fragment)
Claude Code auto-loads CLAUDE.local.md from cwd without an import line — native behavior. Agent memory works natively; composition only wraps around it.
Module fragment contract
Skills. A skill optionally ships an instructions.md at the top of its directory:
container/skills/welcome/
SKILL.md — description + when-to-use (existing)
instructions.md — always-in-context guidance (optional, new)
When the skill is enabled for a group, the host imports instructions.md into the composed CLAUDE.md. SKILL.md semantics are unchanged — Claude Code still uses it for skill discovery and on-demand invocation. Most skills won't need an instructions.md (SKILL.md is sufficient for on-demand skills); it's only for guidance that should be in context at all times.
MCP servers. A container.json MCP server entry can contribute a fragment inline:
{
"mcpServers": {
"my-db": {
"command": "...",
"instructions": "Read-only access to the production DB. Never run UPDATE/DELETE without admin approval."
}
}
}
Host writes the inline content to .claude-fragments/mcp-<server-name>.md at spawn and imports it.
Global CLIs baked into the image (agent-browser, vercel, claude-code) have always-present guidance; it belongs in container/CLAUDE.md, not as a conditional fragment. Don't try to make universally-present tools dynamic.
Identity vs memory
All per-group content — human-authored identity ("you are the research agent, be terse") and agent-accumulated memory (inventories, user preferences, learned patterns) — lives in a single CLAUDE.local.md. Both humans and agents can edit it.
If the distinction becomes operationally important later (agents confused about what they were told vs. what they learned), split into identity.md (human-authored, imported into composed CLAUDE.md) + CLAUDE.local.md (agent memory only). Starting with one file.
Changes
container/CLAUDE.md (new)
Write the shared base: general NanoClaw context, how to engage with users, output conventions, anything that should apply to every agent across every group. Seed from current groups/global/CLAUDE.md.
container/skills/<name>/instructions.md (optional, per skill)
Add for any skill that warrants always-in-context guidance. Optional.
container.json schema
Add optional instructions field (string) to each MCP server entry.
container-runner.ts spawn-time sync
Extend the skill-symlink sync function (added in the shared-source refactor) to also compose CLAUDE.md. On every spawn:
- Sync
.claude-shared/skills/<name>symlinks fromcontainer.jsonskill selection. - Sync
.claude-shared.mdsymlink →/app/CLAUDE.md. - For each enabled skill with an
instructions.md, create.claude-fragments/<name>.mdsymlink →/app/skills/<name>/instructions.md. - For each
container.jsonMCP server with aninstructionsfield, write the inline content to.claude-fragments/mcp-<server-name>.md. - Write
groups/<folder>/CLAUDE.mdatomically (temp + rename) with import lines in a deterministic order: shared base → skill fragments (alphabetical) → MCP fragments (alphabetical). - Remove stale symlinks and fragment files for modules no longer enabled.
group-init.ts
- Stop writing an initial
groups/<folder>/CLAUDE.mdat group creation — host regenerates at first spawn. - Stop creating the
.claude-global.mdsymlink — replaced by.claude-shared.mdin the composition step. - Optionally create an empty
groups/<folder>/CLAUDE.local.mdat init as a clear affordance for humans and agents.
groups/global/
Eliminate. The shared base moves to container/CLAUDE.md. Any deployment-specific overrides live in the owner's customized container/CLAUDE.md (same pattern as any other codebase customization).
Migration
Breaking change, one-time cutover:
- For every group, rename
groups/<folder>/CLAUDE.md→groups/<folder>/CLAUDE.local.md. Preserves all existing per-group content as memory. - Move content from
groups/global/CLAUDE.md(beyond the default stub) intocontainer/CLAUDE.md. Deletegroups/global/. - Delete stale
.claude-global.mdsymlinks in each group dir — the spawn pass creates.claude-shared.mdinstead. - First spawn after cutover regenerates
CLAUDE.mdwith proper imports.
Interaction with shared-source refactor
This refactor depends on the shared skills mount (/app/skills/ RO) from the shared-source refactor landing first. It extends the spawn-time sync from "just skill symlinks" to "skill symlinks + CLAUDE.md composition" — both passes share the same helper.
After this refactor, the "Personality / instructions" row in the shared-source per-group customization table splits:
| Resource | Location | Mechanism |
|---|---|---|
| Agent memory | groups/<folder>/CLAUDE.local.md |
RW at /workspace/agent/, auto-loaded by Claude Code |
| Composed entry | groups/<folder>/CLAUDE.md |
Host-regenerated at every spawn |
What triggers what
| Change | Action | Scope |
|---|---|---|
Edit container/CLAUDE.md |
Kill running containers (next spawn recomposes) | All groups |
Add/edit a skill's instructions.md |
Kill running containers | All groups with the skill enabled |
Enable/disable a skill in container.json |
Kill that group's containers | One group |
Add MCP server with instructions field |
Kill that group's containers | One group |
Edit CLAUDE.local.md |
Nothing — live via RW mount; Claude Code re-reads at next prompt | One group |
| Add a new agent group | Spawn writes CLAUDE.md fresh from the composition pass |
One group |