multica-ai/multica · critical

codex home %s is a symlink; refusing to write %s through it

Error message

codex home %s is a symlink; refusing to write %s through it

What it means

A deliberate security refusal: os.Lstat shows the task home path is now a symlink, so the daemon will not write config through it. Replacing a task home with a symlink is a classic escape attempt — writes meant for the sandboxed home would land at the link target.

Source

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

	}
	return root, nil
}

// 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.

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Recreate the task home as a real directory and re-run prepare
  2. Audit what replaced the directory — treat as a sandbox-escape signal if task code did it
  3. Never point CODEX_HOME for tasks at a symlink
Defensive patterns

Strategy: validation

Validate before calling

if fi, err := os.Lstat(codexHome); err == nil && fi.Mode()&os.ModeSymlink != 0 {
	return fmt.Errorf("refusing to prepare: %s is a symlink", codexHome)
}

Prevention

When it happens

Trigger: A previous or concurrent task replaced the home directory (or an ancestor addressed by path in earlier prepare steps) with a symlink to another location; a misconfigured setup symlinks CODEX_HOME at shared state.

Common situations: A malicious or buggy task linking its home to ~/.ssh or another task's data; users manually symlinking task dirs to share caches.

Related errors


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