{"record":{"id":"158511d555a6e8ba","repo":"multica-ai/multica","slug":"codex-home-s-is-a-symlink-refusing-to-write-s-t","errorCode":null,"errorMessage":"codex home %s is a symlink; refusing to write %s through it","messagePattern":"codex home (.+?) is a symlink; refusing to write (.+?) through it","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"server/internal/daemon/execenv/codex_home.go","lineNumber":991,"sourceCode":"\t}\n\treturn root, nil\n}\n\n// verifyCodexHomeRoot proves that root is the directory codexHome names right\n// now: not reached through a symlink, and the same directory os.Lstat sees at\n// that path. It is separate from openVerifiedCodexHomeRoot so the swap case can\n// be tested deterministically instead of by racing.\nfunc verifyCodexHomeRoot(root *os.Root, codexHome, key string) error {\n\topened, err := root.Stat(\".\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"stat opened codex home %s: %w\", codexHome, err)\n\t}\n\tcurrent, err := os.Lstat(codexHome)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"stat codex home %s: %w\", codexHome, err)\n\t}\n\tif current.Mode()&os.ModeSymlink != 0 {\n\t\treturn fmt.Errorf(\"codex home %s is a symlink; refusing to write %s through it\", codexHome, key)\n\t}\n\tif !os.SameFile(opened, current) {\n\t\treturn fmt.Errorf(\"codex home %s was replaced while opening it; refusing to write %s through it\", codexHome, key)\n\t}\n\treturn nil\n}\n\n// materialiseInCodexHome writes src to relPath inside codexHome using\n// root-scoped operations, so no symlink below the task home can redirect the\n// daemon's mkdir, remove, or write outside it.\n//\n// This matters because a task home is reused: a prepare can run against a\n// directory a previous task already wrote to. Without the root, a task that\n// replaced an intermediate directory of its own home with a link to somewhere\n// else would have the daemon delete and overwrite the link target on the next\n// prepare. os.Root still allows links that stay inside the task home, which is\n// harmless, and rejects the ones that leave it. The root itself is\n// identity-checked by openVerifiedCodexHomeRoot.","sourceCodeStart":973,"sourceCodeEnd":1009,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/daemon/execenv/codex_home.go#L973-L1009","documentation":"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.","triggerScenarios":"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.","commonSituations":"A malicious or buggy task linking its home to ~/.ssh or another task's data; users manually symlinking task dirs to share caches.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if fi, err := os.Lstat(codexHome); err == nil && fi.Mode()&os.ModeSymlink != 0 {\n\treturn fmt.Errorf(\"refusing to prepare: %s is a symlink\", codexHome)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["security","symlink","codex-home","sandbox-escape"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}