Skip to content

bug(runtime): a CLAUDE.md symlinked to AGENTS.md is injected into the prompt twice #3577

Description

@shaokeyibb

What happened

A repository that follows the mainstream way of sharing one instruction file across agent CLIs gets that file injected into Maka's system prompt twice.

readWorkspaceInstructions reads every candidate name in a directory and never compares the results:

// packages/runtime/src/system-prompt/workspace-instructions.ts
export const WORKSPACE_INSTRUCTION_FILES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'] as const;

for (const file of WORKSPACE_INSTRUCTION_FILES) {
  const candidate = join(root, file);
  const resolved = await realpath(candidate);
  if (!isPathInside(root, resolved)) continue;
  const raw = await readFile(resolved, 'utf8');
  out.push({ file, scope, text: ..., truncated: ... });
}

realpath is used only for the containment check; the entry is still recorded under the original name, so a CLAUDE.md symlinked to AGENTS.md resolves to the same file, is read again, and is appended again.

Measured against a checkout with that symlink, by calling buildWorkspaceInstructionsPromptFragment on the repo root:

without symlink   blocks = project/AGENTS.md
with symlink      blocks = project/AGENTS.md, project/CLAUDE.md

Both copies count against MAX_WORKSPACE_INSTRUCTIONS_PROMPT_CHARS (14000). With a file near the 6000-character per-file cap, the duplicate consumes most of the workspace-instruction budget, and any genuinely different file in the same directory is what gets dropped.

This is not an exotic setup. Claude Code's documentation recommends exactly it:

Claude Code reads CLAUDE.md, not AGENTS.md. […] A symlink also works if you don't need to add Claude-specific content: ln -s AGENTS.md CLAUDE.md

apache/airflow ships that symlink, as does deepseek-ai/deepseek-harness.

The @AGENTS.md import form that the same documentation recommends fails differently: Maka does not expand imports, so a one-line CLAUDE.md containing @AGENTS.md is injected as the literal text @AGENTS.md. Harmless in size, but it is an instruction that means nothing.

How to reproduce

  1. In any workspace with an AGENTS.md, add ln -s AGENTS.md CLAUDE.md.
  2. Call buildWorkspaceInstructionsPromptFragment(cwd).
  3. The fragment contains two <workspace-instructions> blocks with identical bodies.

Environment

  • Maka commit: 729839ed8
  • Surface: any — the fragment is shared by Desktop, TUI and CLI

Logs, screenshots, or additional context

How other agents avoid this

Three distinct strategies, and Maka currently uses none of them:

Agent Files read Conflict resolution
Claude Code CLAUDE.md only not possible
Codex CLI AGENTS.md only not possible
Gemini CLI GEMINI.md only not possible
opencode AGENTS.md, CLAUDE.md first match wins — "if you have both AGENTS.md and CLAUDE.md, only AGENTS.md is used"
Pi 5 candidate names first match wins
DeepSeek Harness several content-digest deduplication

Pi's loader returns on the first existing candidate per directory (packages/coding-agent/src/core/resource-loader.ts), and carries a separate guard against a linked worktree applying the same context twice — the same class of problem.

Suggested direction

DeepSeek Harness is the closest analogue: it reads several names and ships the CLAUDE.md symlink, and reconciles them by content rather than by path. From packages/context/agent-instructions/src/files.ts:

Drop later candidates whose trimmed content duplicates an earlier sibling in the same directory. Different directories never collapse even when identical; within one directory the earliest candidate in discovery order is kept and its original bytes are rendered. A candidate that symlinks a sibling resolves to the same content and collapses here like any byte-identical real file.

Deduplicating on the trimmed content digest, scoped to one directory, looks like the right shape for Maka too:

  • it catches the symlink and two byte-identical real files, which a realpath check would miss;
  • it keeps different directories independent, so the global/project layering still works;
  • it changes nothing for the common case of a single instruction file.

First-match-wins would also work and is simpler, but it silently discards a CLAUDE.md that genuinely differs from AGENTS.md, which is a supported thing to have. Deduplication only drops what is redundant.

Investigated with AI assistance (Claude Code); Maka behaviour measured on the commit above, other agents' behaviour read from their published docs and source.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workinggood first issueGood for newcomershelp wantedExtra attention is needed

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions