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
- Recreate the task home as a real directory and re-run prepare
- Audit what replaced the directory — treat as a sandbox-escape signal if task code did it
- 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
- Treat this error as a security signal, not an operational hiccup — audit task code
- Recreate the home as a real directory before retrying
- Never configure shared symlinks as task homes
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
- create %s directory %s: %w
- codex home %s was replaced while opening it; refusing to wri
- fixed_args entries cannot contain NUL bytes
- command_name cannot contain NUL bytes
- resolve --%s path %q: %w
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/158511d555a6e8ba.
Report an issue: GitHub.