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
- In any workspace with an
AGENTS.md, add ln -s AGENTS.md CLAUDE.md.
- Call
buildWorkspaceInstructionsPromptFragment(cwd).
- 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.
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.
readWorkspaceInstructionsreads every candidate name in a directory and never compares the results:realpathis used only for the containment check; the entry is still recorded under the original name, so aCLAUDE.mdsymlinked toAGENTS.mdresolves to the same file, is read again, and is appended again.Measured against a checkout with that symlink, by calling
buildWorkspaceInstructionsPromptFragmenton the repo root: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:
apache/airflowships that symlink, as doesdeepseek-ai/deepseek-harness.The
@AGENTS.mdimport form that the same documentation recommends fails differently: Maka does not expand imports, so a one-lineCLAUDE.mdcontaining@AGENTS.mdis injected as the literal text@AGENTS.md. Harmless in size, but it is an instruction that means nothing.How to reproduce
AGENTS.md, addln -s AGENTS.md CLAUDE.md.buildWorkspaceInstructionsPromptFragment(cwd).<workspace-instructions>blocks with identical bodies.Environment
729839ed8Logs, screenshots, or additional context
How other agents avoid this
Three distinct strategies, and Maka currently uses none of them:
CLAUDE.mdonlyAGENTS.mdonlyGEMINI.mdonlyAGENTS.md,CLAUDE.mdAGENTS.mdandCLAUDE.md, onlyAGENTS.mdis used"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.mdsymlink, and reconciles them by content rather than by path. Frompackages/context/agent-instructions/src/files.ts:Deduplicating on the trimmed content digest, scoped to one directory, looks like the right shape for Maka too:
realpathcheck would miss;First-match-wins would also work and is simpler, but it silently discards a
CLAUDE.mdthat genuinely differs fromAGENTS.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.