multica-ai/multica · critical

codex home %s was replaced while opening it; refusing to wri

Error message

codex home %s was replaced while opening it; refusing to write %s through it

What it means

os.SameFile compares the opened os.Root handle with the current Lstat result and they differ: the directory at codexHome was swapped (rename) between open and stat. The daemon refuses to write through the stale handle's identity because the path now names something else — same anti-symlink defense extended to atomic directory replacement.

Source

Thrown at server/internal/daemon/execenv/codex_home.go:994

// verifyCodexHomeRoot proves that root is the directory codexHome names right
// now: not reached through a symlink, and the same directory os.Lstat sees at
// that path. It is separate from openVerifiedCodexHomeRoot so the swap case can
// be tested deterministically instead of by racing.
func verifyCodexHomeRoot(root *os.Root, codexHome, key string) error {
	opened, err := root.Stat(".")
	if err != nil {
		return fmt.Errorf("stat opened codex home %s: %w", codexHome, err)
	}
	current, err := os.Lstat(codexHome)
	if err != nil {
		return fmt.Errorf("stat codex home %s: %w", codexHome, err)
	}
	if current.Mode()&os.ModeSymlink != 0 {
		return fmt.Errorf("codex home %s is a symlink; refusing to write %s through it", codexHome, key)
	}
	if !os.SameFile(opened, current) {
		return fmt.Errorf("codex home %s was replaced while opening it; refusing to write %s through it", codexHome, key)
	}
	return nil
}

// materialiseInCodexHome writes src to relPath inside codexHome using
// root-scoped operations, so no symlink below the task home can redirect the
// daemon's mkdir, remove, or write outside it.
//
// This matters because a task home is reused: a prepare can run against a
// directory a previous task already wrote to. Without the root, a task that
// replaced an intermediate directory of its own home with a link to somewhere
// else would have the daemon delete and overwrite the link target on the next
// prepare. os.Root still allows links that stay inside the task home, which is
// harmless, and rejects the ones that leave it. The root itself is
// identity-checked by openVerifiedCodexHomeRoot.
func materialiseInCodexHome(codexHome, relPath, src, key string) error {
	root, err := openVerifiedCodexHomeRoot(codexHome, key)
	if err != nil {

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Re-run the prepare — the next open picks up the current directory
  2. Stop tooling that swaps workspace directories while tasks start
  3. Investigate task code that renames its own home
Defensive patterns

Strategy: retry

Try / catch

if err := prepareCodexHome(...); err != nil {
	if strings.Contains(err.Error(), "was replaced while opening") {
		err = prepareCodexHome(...) // next open sees the new directory
	}
}

Prevention

When it happens

Trigger: A rename swapping the home directory concurrently with openVerifiedCodexHomeRoot; a task that replaces its own home via rename during a previous prepare.

Common situations: Task code atomically rotating its home; external tooling re-provisioning workspace directories mid-prepare.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/23f750ca30241583. Report an issue: GitHub.