{"record":{"id":"23f750ca30241583","repo":"multica-ai/multica","slug":"codex-home-s-was-replaced-while-opening-it-refus","errorCode":null,"errorMessage":"codex home %s was replaced while opening it; refusing to write %s through it","messagePattern":"codex home (.+?) was replaced while opening it; refusing to write (.+?) through it","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"server/internal/daemon/execenv/codex_home.go","lineNumber":994,"sourceCode":"\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.\nfunc materialiseInCodexHome(codexHome, relPath, src, key string) error {\n\troot, err := openVerifiedCodexHomeRoot(codexHome, key)\n\tif err != nil {","sourceCodeStart":976,"sourceCodeEnd":1012,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/daemon/execenv/codex_home.go#L976-L1012","documentation":"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.","triggerScenarios":"A rename swapping the home directory concurrently with openVerifiedCodexHomeRoot; a task that replaces its own home via rename during a previous prepare.","commonSituations":"Task code atomically rotating its home; external tooling re-provisioning workspace directories mid-prepare.","solutions":["Re-run the prepare — the next open picks up the current directory","Stop tooling that swaps workspace directories while tasks start","Investigate task code that renames its own home"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := prepareCodexHome(...); err != nil {\n\tif strings.Contains(err.Error(), \"was replaced while opening\") {\n\t\terr = prepareCodexHome(...) // next open sees the new directory\n\t}\n}","preventionTips":["Stop tooling that renames/swaps workspace dirs during prepare","Alert on repeated occurrences — they indicate a hostile or broken task"],"tags":["security","race-condition","codex-home","toctou"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}