{"record":{"id":"d50b0ba1b322b3c8","repo":"multica-ai/multica","slug":"task-local-cli-config-directory-q-escapes-root-q","errorCode":null,"errorMessage":"task-local CLI config directory %q escapes root %q","messagePattern":"task-local CLI config directory %q escapes root %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/internal/cli/config.go","lineNumber":344,"sourceCode":"\t}\n\tif err := os.MkdirAll(dir, dirMode); err != nil {\n\t\treturn fmt.Errorf(\"create CLI config directory: %w\", err)\n\t}\n\tif dirMode == 0o700 {\n\t\troot, _, err := multicaConfigRoot()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"resolve task-local CLI config root: %w\", err)\n\t\t}\n\t\tfor current := dir; ; current = filepath.Dir(current) {\n\t\t\tif err := os.Chmod(current, 0o700); err != nil {\n\t\t\t\treturn fmt.Errorf(\"restrict task-local CLI config directory: %w\", err)\n\t\t\t}\n\t\t\tif current == root {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tparent := filepath.Dir(current)\n\t\t\tif parent == current {\n\t\t\t\treturn fmt.Errorf(\"task-local CLI config directory %q escapes root %q\", dir, root)\n\t\t\t}\n\t\t}\n\t}\n\tdata, err := json.MarshalIndent(cfg, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"encode CLI config: %w\", err)\n\t}\n\n\t// Write to a temp file in the same directory, then rename for atomicity.\n\ttmp, err := os.CreateTemp(dir, \".config-*.json.tmp\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create temp config file: %w\", err)\n\t}\n\ttmpPath := tmp.Name()\n\tif _, err := tmp.Write(append(data, '\\n')); err != nil {\n\t\ttmp.Close()\n\t\tos.Remove(tmpPath)\n\t\treturn fmt.Errorf(\"write temp config file: %w\", err)","sourceCodeStart":326,"sourceCodeEnd":362,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/cli/config.go#L326-L362","documentation":"A defensive invariant inside the task-local permission walk: the code ascends from the config directory via filepath.Dir until it reaches the task root, and if it reaches the filesystem root ('/') without matching the configured MULTICA_TASK_CONFIG_ROOT it aborts. This happens when the resolved config path is not actually under the task root — e.g. the root env var and the profile path disagree, or symlinks make the textual prefix comparison fail.","triggerScenarios":"MULTICA_TASK_CONFIG_ROOT containing a path that is not a lexical prefix of the computed config dir: trailing-slash vs cleaned-path mismatches, a symlinked home directory so the cleaned path never equals the root string, or '..' components surviving in the env var.","commonSituations":"Setting MULTICA_TASK_CONFIG_ROOT=/var/lib/multica/tasks/ (trailing slash) while the config path resolves to /var/lib/multica/tasks/profiles/x; /home pointing at /users via symlink on macOS.","solutions":["Set MULTICA_TASK_CONFIG_ROOT to the cleaned, canonical absolute path with no trailing slash or '..' components (e.g. /var/lib/multica/tasks)","Resolve symlinks first: realpath the intended root and export that value","Verify with a quick check that the config dir path starts with the exported root value plus a path separator","If the escaping is unexpected, inspect CLIConfigPathForProfile's output for the active profile to see which side is wrong"],"exampleFix":"# before\nexport MULTICA_TASK_CONFIG_ROOT=/var/lib/multica/tasks/\n# config dir resolves to /var/lib/multica/tasks/profiles/dev -> root never matches\n\n# after\nexport MULTICA_TASK_CONFIG_ROOT=$(realpath /var/lib/multica/tasks)","handlingStrategy":"validation","validationCode":"root := filepath.Clean(os.Getenv(\"MULTICA_TASK_CONFIG_ROOT\"))\nif resolved, err := filepath.EvalSymlinks(root); err == nil {\n\tos.Setenv(\"MULTICA_TASK_CONFIG_ROOT\", resolved)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Export the canonicalized (realpath) task root with no trailing slash","Avoid symlinking the task root","Add a startup assertion that the computed config path is under the exported root"],"tags":["paths","environment","invariant","go"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}