Skip to main content

STARTUP_INSTRUCTIONS_MANAGED_MARKER

Constant STARTUP_INSTRUCTIONS_MANAGED_MARKER 

Source
const STARTUP_INSTRUCTIONS_MANAGED_MARKER: &str = "<!-- agentmux:managed-startup-instructions -->";
Expand description

Materialize generated_content (the composed Soul+AgentMD+Bundle+Skills CLAUDE.md body build_config_files already produced) against whatever is already on disk at base_path/CLAUDE.md, WITHOUT ever overwriting a foreign file’s content:

  • No file yet, or the existing file starts with CLAUDE_MD_MANAGED_MARKER: AgentMux owns it — write CLAUDE.md directly (marker + generated_content), exactly as every call site did unconditionally before this function existed. This is the one path that still hard-fails on an I/O error (propagated via ?, same as every call site’s pre-existing behavior) — AgentMux’s own config file failing to write is a real launch-blocking problem.
  • Existing file present WITHOUT the marker, or present but unreadable (permission denied, non-UTF-8 content, transient I/O error — a read_to_string error here must never collapse into “no file yet,” or an unreadable foreign file gets silently clobbered, defeating this whole function’s purpose; reagent P0 + codex P1 on PR #2747): treated identically as foreign. CLAUDE.md itself is never written to again. generated_content instead goes to AGENTMUX_MEMORY_FILENAME (always safe to regenerate — it’s 100% AgentMux’s own content), pulled in via a single @import line appended to the real CLAUDE.md — offered at most once per working directory (see CLAUDE_MD_OWNERSHIP_MARKER_PATH’s doc comment). If the file is unreadable specifically, the @import offer is skipped for this launch (nothing to safely append to/check idempotency against) — retried on the next launch.

Everything in the foreign-file branch is best-effort, in the same spirit as write_managed_skill_file_manifest and unlike the AgentMux-owned branch above: an I/O failure writing the side file, appending the import line, or writing the ownership marker is logged and this function still returns Ok(()) — losing one of these only means the import line isn’t offered (or the side file isn’t refreshed) on this one launch, not a correctness issue serious enough to fail the whole agent launch (codex P1 on PR #2747: the original version used ? throughout this branch, contradicting this doc comment and propagating a side-file write failure into a hard launch failure).

The @import append is a true O_APPEND write (OpenOptions::append(true)), never a read-modify-write of the whole file — if the foreign file changes between this function’s read and the append (edited by the user or another process mid-launch), a full-file rewrite would silently discard that intervening edit; a true append can only ever add bytes at whatever the end happens to be (codex P2 on PR #2747).

Every new path this function writes (AGENTMUX_MEMORY_FILENAME, CLAUDE_MD_OWNERSHIP_MARKER_PATH) is resolved through crate::backend::base::safe_join_within_base and verified against crate::backend::base::verify_no_symlink_escape before any write — both constants are fixed, not user-controllable, but .claude/ itself could exist as a symlink escaping the working directory, and without this check a write would silently follow it outside the selected project (codex P1 on PR #2747, same defense-in-depth the other config-file write loops already apply to their own paths).

First line AgentMux writes on any non-CLAUDE.md startup-instructions file it fully owns (freshly created, or one it wrote on a prior launch) — mirrors CLAUDE_MD_MANAGED_MARKER’s role for CLAUDE.md, generic across filenames since AGENTS.md/GEMINI.md/QWEN.md/pi’s APPEND_SYSTEM.md don’t each need distinct marker text. An HTML comment renders invisibly in every markdown viewer and every one of these providers reads its instructions file as plain text fed into a prompt, so a leading comment line is universally harmless regardless of provider — no per-provider syntax needed for the marker itself (only the @import-equivalent side-file fallback CLAUDE.md also gets would need that, and this function deliberately doesn’t attempt it — see the doc comment below).